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. 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}