opendal_service_goosefs/config.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19
20use serde::Deserialize;
21use serde::Serialize;
22
23use super::backend::GoosefsBuilder;
24
25/// Config for GooseFS service support.
26///
27/// GooseFS is a distributed caching file system,
28/// accessed via native gRPC protocol (not REST Proxy).
29#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
30#[serde(default)]
31#[non_exhaustive]
32pub struct GoosefsConfig {
33 /// Root path of this backend.
34 ///
35 /// All operations will happen under this root.
36 /// Default to `/` if not set.
37 pub root: Option<String>,
38
39 /// Master address(es) in `host:port` format.
40 ///
41 /// For single master: `"10.0.0.1:9200"`
42 /// For HA (comma-separated): `"10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200"`
43 ///
44 /// When multiple addresses are provided, the client uses
45 /// `PollingMasterInquireClient` to discover the Primary Master automatically.
46 ///
47 /// Resolution precedence at `build()` time (highest → lowest), following
48 /// [goosefs-sdk `docs/CLIENT_CONFIGURATION.md`](https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md) §1:
49 /// 1. This field (when set on the builder / OpenDAL config map)
50 /// 2. `GOOSEFS_MASTER_ADDR` environment variable
51 /// 3. `goosefs.master.rpc.addresses` / `goosefs.master.hostname` in
52 /// `goosefs-site.properties`
53 ///
54 /// `build()` fails with `ConfigInvalid` only when **none** of the above
55 /// supplies a master address.
56 pub master_addr: Option<String>,
57
58 /// Block size in bytes for new files (default: 64 MiB).
59 pub block_size: Option<u64>,
60
61 /// Chunk size in bytes for streaming RPCs (default: 1 MiB).
62 pub chunk_size: Option<u64>,
63
64 /// Default write type for new files.
65 ///
66 /// Supported values: `"must_cache"`, `"cache_through"`, `"through"`, `"async_through"`.
67 /// Default: `"must_cache"`.
68 pub write_type: Option<String>,
69
70 /// Authentication type.
71 ///
72 /// Supported values: `"nosasl"`, `"simple"`.
73 /// Default: `"simple"` — PLAIN SASL with username.
74 /// `"nosasl"` — skip authentication entirely.
75 pub auth_type: Option<String>,
76
77 /// Authentication username.
78 ///
79 /// Used in SIMPLE mode as the login identity.
80 /// Default: current OS user (`$USER` / `$USERNAME`).
81 pub auth_username: Option<String>,
82}
83
84impl Debug for GoosefsConfig {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("GoosefsConfig")
87 .field("root", &self.root)
88 .field("master_addr", &self.master_addr)
89 .field("block_size", &self.block_size)
90 .field("chunk_size", &self.chunk_size)
91 .field("write_type", &self.write_type)
92 .field("auth_type", &self.auth_type)
93 .field("auth_username", &self.auth_username)
94 .finish_non_exhaustive()
95 }
96}
97
98impl opendal_core::Configurator for GoosefsConfig {
99 type Builder = GoosefsBuilder;
100
101 fn from_uri(uri: &opendal_core::OperatorUri) -> opendal_core::Result<Self> {
102 let mut map = uri.options().clone();
103 if let Some(authority) = uri.authority() {
104 // goosefs://host:port/path → master_addr = "host:port"
105 map.insert("master_addr".to_string(), authority.to_string());
106 }
107 if let Some(root) = uri.root()
108 && !root.is_empty()
109 {
110 map.insert("root".to_string(), root.to_string());
111 }
112 Self::from_iter(map)
113 }
114
115 fn into_builder(self) -> Self::Builder {
116 GoosefsBuilder { config: self }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use opendal_core::Configurator;
124 use opendal_core::OperatorUri;
125
126 /// `goosefs://host:port/path` must map to `master_addr=host:port` and
127 /// `root=<path without surrounding slashes>`. This mirrors the behavior
128 /// OpenDAL's `Operator::from_uri("goosefs://...")` relies on and is
129 /// exercised indirectly by the behavior test matrix through
130 /// `Operator::via_iter("goosefs", ...)`.
131 #[test]
132 fn from_uri_sets_master_addr_and_root() {
133 let uri = OperatorUri::new(
134 "goosefs://10.0.0.1:9200/data/raw",
135 Vec::<(String, String)>::new(),
136 )
137 .expect("valid uri");
138
139 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
140
141 assert_eq!(cfg.master_addr.as_deref(), Some("10.0.0.1:9200"));
142 // `OperatorUri::new` trims leading/trailing slashes from the path, so
143 // the root stored here is the *inner* part only. Normalization to an
144 // absolute "/…/…/" form happens inside `GoosefsBuilder::build()`.
145 assert_eq!(cfg.root.as_deref(), Some("data/raw"));
146 }
147
148 /// When the URI path is empty (or just `/`), `root` must stay `None` and
149 /// `build()` is expected to fall back to the builder-level default.
150 #[test]
151 fn from_uri_without_path_leaves_root_none() {
152 let uri = OperatorUri::new("goosefs://master:9200", Vec::<(String, String)>::new())
153 .expect("valid uri");
154
155 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
156
157 assert_eq!(cfg.master_addr.as_deref(), Some("master:9200"));
158 assert!(
159 cfg.root.is_none(),
160 "empty path must not produce a root, got: {:?}",
161 cfg.root
162 );
163 }
164
165 /// HA note: since URIs don't allow commas in `host`, HA mode is
166 /// expressed through the extra-option/env-var pathway, not through the
167 /// URI authority. When both are present the URI authority **wins** —
168 /// this is the deliberate contract of `from_uri` (it ensures a user who
169 /// typed `goosefs://host:port/` can't be silently overridden by a stale
170 /// env var). The full HA list therefore reaches `build()` only when the
171 /// caller goes through `from_iter` / `Operator::via_iter` without the
172 /// URI authority, which is covered by [`from_iter_picks_up_every_known_field`].
173 #[test]
174 fn from_uri_authority_overrides_extra_master_addr() {
175 let uri = OperatorUri::new(
176 "goosefs://host1:9200/",
177 vec![(
178 "master_addr".into(),
179 "host1:9200,host2:9200,host3:9200".into(),
180 )],
181 )
182 .expect("valid uri");
183
184 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
185
186 assert_eq!(
187 cfg.master_addr.as_deref(),
188 Some("host1:9200"),
189 "URI authority must win over extra-options master_addr to \
190 prevent stale env/option values from silently shadowing the \
191 user-typed URI"
192 );
193 }
194
195 /// `from_iter` is the path taken by `Operator::via_iter(scheme, env_map)`
196 /// — the main entry point used by the behavior test harness (it reads
197 /// `OPENDAL_GOOSEFS_*` env vars into a HashMap).
198 #[test]
199 fn from_iter_picks_up_every_known_field() {
200 use std::collections::HashMap;
201
202 let mut map = HashMap::new();
203 map.insert("root".into(), "/tmp/opendal/".into());
204 map.insert("master_addr".into(), "127.0.0.1:9200".into());
205 map.insert("write_type".into(), "cache_through".into());
206 map.insert("auth_type".into(), "simple".into());
207 map.insert("auth_username".into(), "opendal".into());
208
209 let cfg = GoosefsConfig::from_iter(map).expect("from_iter should succeed");
210
211 assert_eq!(cfg.root.as_deref(), Some("/tmp/opendal/"));
212 assert_eq!(cfg.master_addr.as_deref(), Some("127.0.0.1:9200"));
213 assert_eq!(cfg.write_type.as_deref(), Some("cache_through"));
214 assert_eq!(cfg.auth_type.as_deref(), Some("simple"));
215 assert_eq!(cfg.auth_username.as_deref(), Some("opendal"));
216 }
217}