Skip to main content

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}