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. `GOOSEFS_MASTER_ADDR` environment variable
50 /// 2. `goosefs.master.rpc.addresses` / `goosefs.master.hostname` in
51 /// `goosefs-site.properties`
52 /// 3. This field (from the builder, the OpenDAL config map, or the URI
53 /// authority of `goosefs://host:port/path`)
54 ///
55 /// A site file that declares masters therefore outranks this field: the
56 /// file carries the deployment's whole HA master list, which a single URI
57 /// authority cannot express. Set `GOOSEFS_MASTER_ADDR` to override a
58 /// deployed site file for one process.
59 ///
60 /// `build()` fails with `ConfigInvalid` when **none** of the above
61 /// supplies a master address.
62 pub master_addr: Option<String>,
63
64 /// Block size in bytes for new files (default: 64 MiB).
65 pub block_size: Option<u64>,
66
67 /// Chunk size in bytes for streaming RPCs (default: 1 MiB).
68 pub chunk_size: Option<u64>,
69
70 /// Default write type for new files.
71 ///
72 /// Supported values: `"must_cache"`, `"try_cache"`, `"cache_through"`,
73 /// `"through"`, `"async_through"`. Matching is case-insensitive.
74 /// `build()` fails with `ConfigInvalid` when the value is not one of
75 /// these.
76 /// Default: `"must_cache"`.
77 pub write_type: Option<String>,
78
79 /// Authentication type.
80 ///
81 /// Supported values: `"nosasl"`, `"simple"`.
82 /// Default: `"simple"` — PLAIN SASL with username.
83 /// `"nosasl"` — skip authentication entirely.
84 pub auth_type: Option<String>,
85
86 /// Authentication username.
87 ///
88 /// Used in SIMPLE mode as the login identity.
89 /// Default: current OS user (`$USER` / `$USERNAME`).
90 pub auth_username: Option<String>,
91}
92
93impl Debug for GoosefsConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("GoosefsConfig")
96 .field("root", &self.root)
97 .field("master_addr", &self.master_addr)
98 .field("block_size", &self.block_size)
99 .field("chunk_size", &self.chunk_size)
100 .field("write_type", &self.write_type)
101 .field("auth_type", &self.auth_type)
102 .field("auth_username", &self.auth_username)
103 .finish_non_exhaustive()
104 }
105}
106
107impl opendal_core::Configurator for GoosefsConfig {
108 type Builder = GoosefsBuilder;
109
110 fn from_uri(uri: &opendal_core::OperatorUri) -> opendal_core::Result<Self> {
111 let mut map = uri.options().clone();
112 if let Some(authority) = uri.authority() {
113 // goosefs://host:port/path → master_addr = "host:port"
114 map.insert("master_addr".to_string(), authority.to_string());
115 }
116 if let Some(root) = uri.root()
117 && !root.is_empty()
118 {
119 map.insert("root".to_string(), root.to_string());
120 }
121 Self::from_iter(map)
122 }
123
124 fn into_builder(self) -> Self::Builder {
125 GoosefsBuilder { config: self }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use opendal_core::Configurator;
133 use opendal_core::OperatorUri;
134
135 /// `goosefs://host:port/path` must map to `master_addr=host:port` and
136 /// `root=<path without surrounding slashes>`. This mirrors the behavior
137 /// OpenDAL's `Operator::from_uri("goosefs://...")` relies on and is
138 /// exercised indirectly by the behavior test matrix through
139 /// `Operator::via_iter("goosefs", ...)`.
140 #[test]
141 fn from_uri_sets_master_addr_and_root() {
142 let uri = OperatorUri::new(
143 "goosefs://10.0.0.1:9200/data/raw",
144 Vec::<(String, String)>::new(),
145 )
146 .expect("valid uri");
147
148 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
149
150 assert_eq!(cfg.master_addr.as_deref(), Some("10.0.0.1:9200"));
151 // `OperatorUri::new` trims leading/trailing slashes from the path, so
152 // the root stored here is the *inner* part only. Normalization to an
153 // absolute "/…/…/" form happens inside `GoosefsBuilder::build()`.
154 assert_eq!(cfg.root.as_deref(), Some("data/raw"));
155 }
156
157 /// When the URI path is empty (or just `/`), `root` must stay `None` and
158 /// `build()` is expected to fall back to the builder-level default.
159 #[test]
160 fn from_uri_without_path_leaves_root_none() {
161 let uri = OperatorUri::new("goosefs://master:9200", Vec::<(String, String)>::new())
162 .expect("valid uri");
163
164 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
165
166 assert_eq!(cfg.master_addr.as_deref(), Some("master:9200"));
167 assert!(
168 cfg.root.is_none(),
169 "empty path must not produce a root, got: {:?}",
170 cfg.root
171 );
172 }
173
174 /// HA note: since URIs don't allow commas in `host`, HA mode is
175 /// expressed through the extra-option/env-var pathway, not through the
176 /// URI authority. When both are present the URI authority **wins** the
177 /// `master_addr` slot — a user who typed `goosefs://host:port/` must not
178 /// be silently redirected by a stale option map. The full HA list
179 /// therefore reaches `build()` only when the caller goes through
180 /// `from_iter` / `Operator::via_iter` without the URI authority, which is
181 /// covered by [`from_iter_picks_up_every_known_field`], or through
182 /// `goosefs-site.properties` / `GOOSEFS_MASTER_ADDR`, which outrank
183 /// `master_addr` inside `build()`.
184 #[test]
185 fn from_uri_authority_overrides_extra_master_addr() {
186 let uri = OperatorUri::new(
187 "goosefs://host1:9200/",
188 vec![(
189 "master_addr".into(),
190 "host1:9200,host2:9200,host3:9200".into(),
191 )],
192 )
193 .expect("valid uri");
194
195 let cfg = GoosefsConfig::from_uri(&uri).expect("from_uri should succeed");
196
197 assert_eq!(
198 cfg.master_addr.as_deref(),
199 Some("host1:9200"),
200 "URI authority must win over extra-options master_addr to \
201 prevent stale env/option values from silently shadowing the \
202 user-typed URI"
203 );
204 }
205
206 /// `from_iter` is the path taken by `Operator::via_iter(scheme, env_map)`
207 /// — the main entry point used by the behavior test harness (it reads
208 /// `OPENDAL_GOOSEFS_*` env vars into a HashMap).
209 #[test]
210 fn from_iter_picks_up_every_known_field() {
211 use std::collections::HashMap;
212
213 let mut map = HashMap::new();
214 map.insert("root".into(), "/tmp/opendal/".into());
215 map.insert("master_addr".into(), "127.0.0.1:9200".into());
216 map.insert("write_type".into(), "cache_through".into());
217 map.insert("auth_type".into(), "simple".into());
218 map.insert("auth_username".into(), "opendal".into());
219
220 let cfg = GoosefsConfig::from_iter(map).expect("from_iter should succeed");
221
222 assert_eq!(cfg.root.as_deref(), Some("/tmp/opendal/"));
223 assert_eq!(cfg.master_addr.as_deref(), Some("127.0.0.1:9200"));
224 assert_eq!(cfg.write_type.as_deref(), Some("cache_through"));
225 assert_eq!(cfg.auth_type.as_deref(), Some("simple"));
226 assert_eq!(cfg.auth_username.as_deref(), Some("opendal"));
227 }
228}