From 22059911807158aae9617415a916cf1156558b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Thu, 11 Jun 2026 16:11:17 +0800 Subject: [PATCH] refactor(config): extract server config model (#3351) --- Cargo.lock | 2 + crates/config/Cargo.toml | 3 + crates/config/src/lib.rs | 2 + crates/config/src/server_config.rs | 229 +++++++++++++++++++ crates/ecstore/Cargo.toml | 2 +- crates/ecstore/src/config/com.rs | 2 +- crates/ecstore/src/config/mod.rs | 180 ++------------- docs/architecture/compat-cleanup-register.md | 6 + docs/architecture/migration-progress.md | 124 +++++----- 9 files changed, 330 insertions(+), 220 deletions(-) create mode 100644 crates/config/src/server_config.rs diff --git a/Cargo.lock b/Cargo.lock index 4d6a23020..08e2c333b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9206,6 +9206,8 @@ name = "rustfs-config" version = "1.0.0-beta.8" dependencies = [ "const-str", + "serde", + "serde_json", ] [[package]] diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index faa664826..df48d49f6 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -26,6 +26,8 @@ categories = ["web-programming", "development-tools", "config"] [dependencies] const-str = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } [lints] workspace = true @@ -37,6 +39,7 @@ constants = ["dep:const-str"] notify = ["dep:const-str", "constants"] observability = ["constants"] opa = ["constants"] +server-config-model = ["constants", "dep:serde", "dep:serde_json"] [lib] doctest = false diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index ed7c1b203..a270febe0 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -68,3 +68,5 @@ pub mod notify; pub mod observability; #[cfg(feature = "opa")] pub mod opa; +#[cfg(feature = "server-config-model")] +pub mod server_config; diff --git a/crates/config/src/server_config.rs b/crates/config/src/server_config.rs new file mode 100644 index 000000000..7cca122b5 --- /dev/null +++ b/crates/config/src/server_config.rs @@ -0,0 +1,229 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::{LazyLock, OnceLock}; + +use crate::{COMMENT_KEY, DEFAULT_DELIMITER}; + +pub static DEFAULT_KVS: LazyLock>> = LazyLock::new(OnceLock::new); + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct KV { + pub key: String, + pub value: String, + #[serde(default, alias = "hiddenIfEmpty")] + pub hidden_if_empty: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct KVS(pub Vec); + +impl Default for KVS { + fn default() -> Self { + Self::new() + } +} + +impl KVS { + pub fn new() -> Self { + KVS(Vec::new()) + } + + pub fn get(&self, key: &str) -> String { + if let Some(v) = self.lookup(key) { v } else { "".to_owned() } + } + + pub fn lookup(&self, key: &str) -> Option { + for kv in self.0.iter() { + if kv.key.as_str() == key { + return Some(kv.value.clone()); + } + } + + None + } + + /// Check if KVS is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns a list of all keys for the current KVS. + /// If the "comment" key does not exist, it will be added. + pub fn keys(&self) -> Vec { + let mut found_comment = false; + let mut keys: Vec = self + .0 + .iter() + .map(|kv| { + if kv.key == COMMENT_KEY { + found_comment = true; + } + kv.key.clone() + }) + .collect(); + + if !found_comment { + keys.push(COMMENT_KEY.to_owned()); + } + + keys + } + + /// Insert or update a pair of key/values in KVS + pub fn insert(&mut self, key: String, value: String) { + for kv in self.0.iter_mut() { + if kv.key == key { + kv.value = value; + return; + } + } + self.0.push(KV { + key, + value, + hidden_if_empty: false, + }); + } + + /// Merge all entries from another KVS to the current instance + pub fn extend(&mut self, other: KVS) { + for KV { key, value, .. } in other.0.into_iter() { + self.insert(key, value); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config(pub HashMap>); + +impl Default for Config { + fn default() -> Self { + Self::new() + } +} + +impl Config { + pub fn new() -> Self { + let mut cfg = Config(HashMap::new()); + cfg.set_defaults(); + + cfg + } + + pub fn get_value(&self, sub_sys: &str, key: &str) -> Option { + if let Some(m) = self.0.get(sub_sys) { + m.get(key).cloned() + } else { + None + } + } + + pub fn set_defaults(&mut self) { + if let Some(defaults) = DEFAULT_KVS.get() { + for (k, v) in defaults.iter() { + if !self.0.contains_key(k) { + let mut default = HashMap::new(); + default.insert(DEFAULT_DELIMITER.to_owned(), v.clone()); + self.0.insert(k.clone(), default); + } else if !self.0[k].contains_key(DEFAULT_DELIMITER) + && let Some(m) = self.0.get_mut(k) + { + m.insert(DEFAULT_DELIMITER.to_owned(), v.clone()); + } + } + } + } + + pub fn unmarshal(data: &[u8]) -> Result { + let m: HashMap> = serde_json::from_slice(data)?; + let mut cfg = Config(m); + cfg.set_defaults(); + Ok(cfg) + } + + pub fn marshal(&self) -> Result, serde_json::Error> { + let data = serde_json::to_vec(&self.0)?; + Ok(data) + } + + pub fn merge(&self) -> Config { + // TODO: merge default + self.clone() + } +} + +pub fn register_default_kvs(kvs: HashMap) { + let mut p = HashMap::new(); + for (k, v) in kvs { + p.insert(k, v); + } + + let _ = DEFAULT_KVS.set(p); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kvs_preserves_lookup_insert_extend_and_keys_behavior() { + let mut kvs = KVS::new(); + assert!(kvs.is_empty()); + + kvs.insert("first".to_string(), "1".to_string()); + kvs.insert("first".to_string(), "2".to_string()); + kvs.extend(KVS(vec![KV { + key: "second".to_string(), + value: "3".to_string(), + hidden_if_empty: true, + }])); + + assert_eq!(kvs.get("first"), "2"); + assert_eq!(kvs.lookup("second"), Some("3".to_string())); + assert_eq!(kvs.get("missing"), ""); + assert!(kvs.keys().contains(&COMMENT_KEY.to_string())); + } + + #[test] + fn kv_hidden_if_empty_accepts_legacy_camel_case_alias() { + let kvs: KVS = serde_json::from_str(r#"[{"key":"token","value":"","hiddenIfEmpty":true}]"#) + .expect("legacy hiddenIfEmpty alias should deserialize"); + + assert!(kvs.0[0].hidden_if_empty); + } + + #[test] + fn config_marshal_unmarshal_preserves_internal_json_shape() { + let mut kvs = KVS::new(); + kvs.insert("standard".to_string(), "EC:4".to_string()); + let cfg = Config(HashMap::from([( + "storage_class".to_string(), + HashMap::from([(DEFAULT_DELIMITER.to_string(), kvs)]), + )])); + + let data = cfg.marshal().expect("config should marshal"); + let loaded = Config::unmarshal(&data).expect("config should unmarshal"); + + assert_eq!( + loaded + .get_value("storage_class", DEFAULT_DELIMITER) + .expect("storage class should exist") + .get("standard"), + "EC:4" + ); + assert_eq!(loaded.merge(), loaded); + } +} diff --git a/crates/ecstore/Cargo.toml b/crates/ecstore/Cargo.toml index 687b53e11..e9ec0bbaa 100644 --- a/crates/ecstore/Cargo.toml +++ b/crates/ecstore/Cargo.toml @@ -43,7 +43,7 @@ rustfs-signer.workspace = true rustfs-storage-api.workspace = true rustfs-tls-runtime.workspace = true rustfs-checksums.workspace = true -rustfs-config = { workspace = true, features = ["constants", "notify", "audit"] } +rustfs-config = { workspace = true, features = ["constants", "notify", "audit", "server-config-model"] } rustfs-credentials = { workspace = true } rustfs-common.workspace = true rustfs-policy.workspace = true diff --git a/crates/ecstore/src/config/com.rs b/crates/ecstore/src/config/com.rs index 19e343aef..b8061c521 100644 --- a/crates/ecstore/src/config/com.rs +++ b/crates/ecstore/src/config/com.rs @@ -2697,7 +2697,7 @@ mod tests { // Verify the decoded config has "storage_class" (with underscore) subsystem let kvs = decoded - .get_value(STORAGE_CLASS_SUB_SYS, crate::config::DEFAULT_DELIMITER) + .get_value(STORAGE_CLASS_SUB_SYS, rustfs_config::DEFAULT_DELIMITER) .expect("decoded config should have storage_class subsystem"); assert_eq!(kvs.get("standard"), "EC:4", "standard should be EC:4"); assert_eq!(kvs.get("rrs"), "EC:2", "rrs should be EC:2"); diff --git a/crates/ecstore/src/config/mod.rs b/crates/ecstore/src/config/mod.rs index 7c0de7491..10df571bb 100644 --- a/crates/ecstore/src/config/mod.rs +++ b/crates/ecstore/src/config/mod.rs @@ -24,8 +24,6 @@ pub mod storageclass; use crate::error::Result; use crate::store::ECStore; use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate}; -use rustfs_config::COMMENT_KEY; -use rustfs_config::DEFAULT_DELIMITER; use rustfs_config::HEAL_SUB_SYS; use rustfs_config::audit::{ AUDIT_AMQP_SUB_SYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_MYSQL_SUB_SYS, AUDIT_NATS_SUB_SYS, AUDIT_POSTGRES_SUB_SYS, @@ -36,14 +34,15 @@ use rustfs_config::notify::{ NOTIFY_POSTGRES_SUB_SYS, NOTIFY_PULSAR_SUB_SYS, NOTIFY_REDIS_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS, }; use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::LazyLock; -use std::sync::{Arc, OnceLock, RwLock}; +use std::sync::{Arc, RwLock}; + +// RUSTFS_COMPAT_TODO(CFG-004): keep old rustfs_ecstore::config model import paths while server-config model consumers migrate. Remove after all consumers import Config, KV, KVS, DEFAULT_KVS, and register_default_kvs from rustfs_config::server_config. +pub use rustfs_config::server_config::{Config, DEFAULT_KVS, KV, KVS, register_default_kvs}; pub static GLOBAL_STORAGE_CLASS: LazyLock> = LazyLock::new(|| RwLock::new(storageclass::Config::default())); -pub static DEFAULT_KVS: LazyLock>> = LazyLock::new(OnceLock::new); pub static GLOBAL_SERVER_CONFIG: LazyLock>> = LazyLock::new(|| RwLock::new(None)); pub static GLOBAL_CONFIG_SYS: LazyLock = LazyLock::new(ConfigSys::new); @@ -100,158 +99,6 @@ pub async fn try_migrate_server_config(api: Arc) { com::try_migrate_server_config(api).await } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct KV { - pub key: String, - pub value: String, - #[serde(default, alias = "hiddenIfEmpty")] - pub hidden_if_empty: bool, -} - -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct KVS(pub Vec); - -impl Default for KVS { - fn default() -> Self { - Self::new() - } -} - -impl KVS { - pub fn new() -> Self { - KVS(Vec::new()) - } - pub fn get(&self, key: &str) -> String { - if let Some(v) = self.lookup(key) { v } else { "".to_owned() } - } - pub fn lookup(&self, key: &str) -> Option { - for kv in self.0.iter() { - if kv.key.as_str() == key { - return Some(kv.value.clone()); - } - } - - None - } - - ///Check if KVS is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Returns a list of all keys for the current KVS. - /// If the "comment" key does not exist, it will be added. - pub fn keys(&self) -> Vec { - let mut found_comment = false; - let mut keys: Vec = self - .0 - .iter() - .map(|kv| { - if kv.key == COMMENT_KEY { - found_comment = true; - } - kv.key.clone() - }) - .collect(); - - if !found_comment { - keys.push(COMMENT_KEY.to_owned()); - } - - keys - } - - /// Insert or update a pair of key/values in KVS - pub fn insert(&mut self, key: String, value: String) { - for kv in self.0.iter_mut() { - if kv.key == key { - kv.value = value; - return; - } - } - self.0.push(KV { - key, - value, - hidden_if_empty: false, - }); - } - - /// Merge all entries from another KVS to the current instance - pub fn extend(&mut self, other: KVS) { - for KV { key, value, .. } in other.0.into_iter() { - self.insert(key, value); - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Config(pub HashMap>); - -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - -impl Config { - pub fn new() -> Self { - let mut cfg = Config(HashMap::new()); - cfg.set_defaults(); - - cfg - } - - pub fn get_value(&self, sub_sys: &str, key: &str) -> Option { - if let Some(m) = self.0.get(sub_sys) { - m.get(key).cloned() - } else { - None - } - } - - pub fn set_defaults(&mut self) { - if let Some(defaults) = DEFAULT_KVS.get() { - for (k, v) in defaults.iter() { - if !self.0.contains_key(k) { - let mut default = HashMap::new(); - default.insert(DEFAULT_DELIMITER.to_owned(), v.clone()); - self.0.insert(k.clone(), default); - } else if !self.0[k].contains_key(DEFAULT_DELIMITER) - && let Some(m) = self.0.get_mut(k) - { - m.insert(DEFAULT_DELIMITER.to_owned(), v.clone()); - } - } - } - } - - pub fn unmarshal(data: &[u8]) -> Result { - let m: HashMap> = serde_json::from_slice(data)?; - let mut cfg = Config(m); - cfg.set_defaults(); - Ok(cfg) - } - - pub fn marshal(&self) -> Result> { - let data = serde_json::to_vec(&self.0)?; - Ok(data) - } - - pub fn merge(&self) -> Config { - // TODO: merge default - self.clone() - } -} - -pub fn register_default_kvs(kvs: HashMap) { - let mut p = HashMap::new(); - for (k, v) in kvs { - p.insert(k, v); - } - - let _ = DEFAULT_KVS.set(p); -} - pub fn init() { let mut kvs = HashMap::new(); // Load storageclass default configuration @@ -328,4 +175,23 @@ mod tests { assert_eq!(heal_kvs.get(HEAL_BITROT_CYCLE), DEFAULT_HEAL_BITROT_CYCLE_SECS.to_string()); } + + #[test] + fn old_config_model_path_reexports_moved_types() { + let mut kvs = crate::config::KVS::new(); + kvs.insert("key".to_string(), "value".to_string()); + let cfg = crate::config::Config(HashMap::from([( + "subsys".to_string(), + HashMap::from([(DEFAULT_DELIMITER.to_string(), kvs)]), + )])); + let moved_cfg: rustfs_config::server_config::Config = cfg; + + assert_eq!( + moved_cfg + .get_value("subsys", DEFAULT_DELIMITER) + .expect("subsys should exist") + .get("key"), + "value" + ); + } } diff --git a/docs/architecture/compat-cleanup-register.md b/docs/architecture/compat-cleanup-register.md index 9ccbbcd52..c6c165b85 100644 --- a/docs/architecture/compat-cleanup-register.md +++ b/docs/architecture/compat-cleanup-register.md @@ -30,6 +30,12 @@ for later deletion. - Why: old `StorageAPI::new_ns_lock` callers must keep compiling while namespace-lock-only consumers migrate to NamespaceLocking. - Removal condition: remove after all namespace-lock-only consumers depend on NamespaceLocking and StorageAPI no longer owns namespace lock capability. - Status: planned cleanup. +- `RUSTFS_COMPAT_TODO(CFG-004)` + - Task: `CFG-004` + - File: `crates/ecstore/src/config/mod.rs` + - Why: old `rustfs_ecstore::config` model import paths must keep compiling while server-config model consumers migrate. + - Removal condition: remove after all consumers import Config, KV, KVS, DEFAULT_KVS, and register_default_kvs from rustfs_config::server_config. + - Status: planned cleanup. ## Review Checklist diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index 7be55f334..7a235b03d 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,17 +5,18 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-table-catalog-bounds` -- Baseline: `origin/main` at `2ead90d31bd713b3b36812ed718bd44e28e522d2` -- PR type for this branch: `dependency-migration` +- Branch: `overtrue/arch-config-model-extraction` +- Baseline: `origin/main` at `dd5035916e62c031d40ecdf9363763a881517abb` +- PR type for this branch: `api-extraction` - Runtime behavior changes: none. -- Rust code changes: add a narrow `NamespaceLocking` operation-group trait and - narrow the table catalog object backend away from full `StorageAPI` when it - only needs object I/O, object operations, listing, and namespace locking. +- Rust code changes: move the pure server-config model (`Config`, `KV`, `KVS`, + `DEFAULT_KVS`, and `register_default_kvs`) into + `rustfs_config::server_config`, keep the old `rustfs_ecstore::config` model + path as a temporary re-export, and leave persistence helpers/global server + config state in ECStore. - CI/script changes: none. -- Docs changes: record API-011 completion, the current table catalog - bound-narrowing context, API-012 compatibility cleanup marker, verification - evidence, and expert review outcomes. +- Docs changes: record API-012 completion, current CFG-003/CFG-004 extraction + context, compatibility cleanup marker, and verification evidence. ## Phase 0 Tasks @@ -78,13 +79,16 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block `rustfs-config` as the target package, `server_config` as the future model module, allowed dependencies, forbidden dependencies, preserved shape, and extraction verification gates. -- [ ] `CFG-003` Move pure model definitions. - - Next boundary: move only `Config`, `KV`, `KVS`, and default-registration - surface into `rustfs-config`; keep persistence helpers and global - server-config state in `ecstore`. -- [ ] `CFG-004` Keep old `ecstore::config::*` compatibility path. - - Required compatibility: source must contain `RUSTFS_COMPAT_TODO(CFG-004)` - and a matching cleanup-register entry. +- [~] `CFG-003` Move pure model definitions. + - Current branch: move only `Config`, `KV`, `KVS`, and + default-registration surface into `rustfs-config`; keep persistence helpers + and global server-config state in `ecstore`. + - Must preserve: tuple struct shapes, serde alias behavior, default + application, internal JSON shape, and existing persisted config semantics. +- [~] `CFG-004` Keep old `ecstore::config::*` compatibility path. + - Current branch: re-export moved model types and default-registration + surface from `rustfs_ecstore::config` with `RUSTFS_COMPAT_TODO(CFG-004)` + and cleanup-register coverage. ## Phase 1 Security Governance Tasks @@ -263,11 +267,12 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block required quality/architecture, migration-preservation, and testing/verification review passed. -- [~] `API-012` Narrow table catalog object backend bounds. - - Current branch slice: add a narrow `NamespaceLocking` operation-group trait - as a compatibility facade over `StorageAPI::new_ns_lock`, then narrow - `EcStoreTableCatalogObjectBackend` from full `StorageAPI` to `ObjectIO`, - `ObjectOperations`, `ListOperations`, and `NamespaceLocking`. +- [x] `API-012` Narrow table catalog object backend bounds. + - Completed slice: `rustfs/rustfs#3350` added a narrow `NamespaceLocking` + operation-group trait as a compatibility facade over + `StorageAPI::new_ns_lock`, then narrowed `EcStoreTableCatalogObjectBackend` + from full `StorageAPI` to `ObjectIO`, `ObjectOperations`, + `ListOperations`, and `NamespaceLocking`. - Acceptance: table catalog object backend contracts express the actual object read/write, metadata/delete, list, and namespace-lock capabilities they need, while table catalog store logic and lock behavior remain @@ -308,70 +313,67 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Next PRs -1. `dependency-migration`: narrow table catalog object backend bounds away - from full `StorageAPI` by depending only on object I/O, object operations, - listing, and namespace locking. -2. `api-extraction`: move only the pure server-config model into - rustfs-config as CFG-003. -3. `api-extraction`: keep the old rustfs_ecstore::config::* path with +1. `api-extraction`: move only the pure server-config model into rustfs-config + as CFG-003 and keep the old rustfs_ecstore::config::* path with RUSTFS_COMPAT_TODO(CFG-004) and cleanup-register coverage. -4. `consumer-migration`: migrate external consumers one group at a time only +2. `consumer-migration`: migrate external consumers one group at a time only after the model path and compatibility shim are stable. -5. `security-change`: make Local KMS unsafe defaults explicit development +3. `security-change`: make Local KMS unsafe defaults explicit development opt-ins or production failures in KMSD-002. -6. `security-change`: make Vault unsafe defaults explicit development opt-ins +4. `security-change`: make Vault unsafe defaults explicit development opt-ins or production failures in KMSD-003. ## Pre-Push Review Log | Expert | Status | Notes | |---|---|---| -| Quality/architecture | pass | Confirmed the Rust diff adds only the narrow `NamespaceLocking` facade, tracks retained `StorageAPI::new_ns_lock` with `RUSTFS_COMPAT_TODO(API-012)`, and narrows table catalog backend bounds; no table catalog method body, lock implementation, object operation, or hot-path behavior changes. | -| Migration preservation | pass | Confirmed the blanket facade keeps existing `StorageAPI::new_ns_lock` compatibility and table catalog object paths, optimistic preconditions, pagination, missing-object mapping, and write-lock behavior remain unchanged. | -| Testing/verification | pass | Confirmed focused compile/tests, migration guards, diff hygiene, and added-line risk scan are sufficient for this dependency-boundary slice before push; full pre-commit is skipped under the current larger-granularity instruction. | +| Quality/architecture | pass | Confirmed this stays a pure model extraction into `rustfs_config::server_config`; persistence helpers, global state, runtime consumers, startup wiring, and storage hot paths remain in ECStore or unchanged. | +| Migration preservation | pass | Confirmed the old `rustfs_ecstore::config` model path remains available through `RUSTFS_COMPAT_TODO(CFG-004)`, while tuple shapes, serde alias behavior, defaults, marshal/unmarshal, and persisted JSON shape are preserved. | +| Testing/verification | pass | Confirmed focused config/model tests, compile checks, dependency tree, migration guards, diff hygiene, and added-line risk scan are sufficient before push; full pre-commit is skipped under the current larger-granularity instruction. | ## Verification Notes Passed: -- `cargo fmt --all`. -- `cargo check -p rustfs-ecstore -p rustfs --lib`. - `cargo fmt --all --check`. -- `cargo test -p rustfs table_catalog --lib`; 84 passed. +- `cargo check -p rustfs-config --features server-config-model`. +- `cargo check -p rustfs-config`. +- `cargo check -p rustfs-ecstore`. +- `cargo check -p rustfs-config -p rustfs-ecstore -p rustfs --lib`. +- `cargo check -p rustfs-targets -p rustfs-notify -p rustfs-audit -p rustfs-iam -p rustfs-scanner -p rustfs --lib`. +- `cargo test -p rustfs-config --features server-config-model server_config --lib`; 3 passed. +- `cargo test -p rustfs-ecstore config --lib`; 60 passed. - `./scripts/check_architecture_migration_rules.sh`. - `./scripts/check_layer_dependencies.sh`. - `./scripts/check_metrics_migration_refs.sh`. - `./scripts/check_unsafe_code_allowances.sh`. - `git diff --check`. -- Rust code-quality scan on changed `.rs` files, plus added-line scan for - unwrap/expect, numeric casts, `Result<_, String>`, `Box`, - println/eprintln, and `Ordering::Relaxed`; broad full-file matches are - pre-existing touched-file patterns, and the added-line scan found no new - risky code patterns. +- `cargo tree -p rustfs-config --edges normal --features server-config-model`. +- Added-line risk scan found no production `unwrap`/`expect`, lossy numeric + casts, stringly public errors, boxed dynamic errors, stdout/stderr printing, + or relaxed atomic ordering. Notes: -- Full pre-commit is intentionally skipped when the focused tests and guards +- Full pre-commit may be skipped if focused tests, compile checks, and guards pass, per the current instruction to increase PR granularity. -- This slice changes trait contracts, imports, and generic bounds only; table - catalog helper bodies, object paths, optimistic write preconditions, list - pagination, missing-object mapping, and write-lock behavior are unchanged. -- `StorageAPI::new_ns_lock` intentionally remains in place for compatibility; - the new `NamespaceLocking` facade only lets narrower consumers state the - capability they actually use. -- The retained old `StorageAPI::new_ns_lock` surface is marked with - `RUSTFS_COMPAT_TODO(API-012)` and registered in - [`compat-cleanup-register.md`](compat-cleanup-register.md). +- This slice moves only the pure server-config model and default-registration + surface. ECStore retains persistence helpers, ConfigSys, global server-config + state, storage-class global state, startup wiring, and all runtime consumers. +- The old rustfs_ecstore::config model path intentionally remains as a + temporary compatibility re-export with `RUSTFS_COMPAT_TODO(CFG-004)` and a + matching cleanup-register entry. +- A focused ECStore test proves the old path re-exports the moved model type; + rustfs-config tests cover KVS behavior, legacy hiddenIfEmpty alias + compatibility, and marshal/unmarshal internal JSON shape. ## Handoff Notes -- Keep this API-012 slice as a `dependency-migration` PR that only adds the - narrow namespace-locking facade and narrows table catalog backend bounds. -- Do not remove `StorageAPI` itself, object operation traits, or - `StorageAPI::new_ns_lock` in this PR. -- Do not move traits into `rustfs-storage-api` or introduce additional - compatibility shims in this PR. -- Do not alter table catalog object paths, metadata pointer semantics, - optimistic write preconditions, object listing pagination, missing-object - handling, namespace write-lock acquisition, scanner/heal/replication/config - persistence paths, object APIs, or storage hot-path consumers in this PR. +- Keep this CFG-003/CFG-004 slice as an `api-extraction` PR that only moves the + pure server-config model to `rustfs_config::server_config` and keeps old + `rustfs_ecstore::config` model import paths compiling. +- Do not move `ConfigSys`, `GLOBAL_SERVER_CONFIG`, storage-class global state, + `read_config_without_migrate`, `save_server_config`, config-object helpers, + startup wiring, runtime consumers, or storage persistence logic in this PR. +- Do not migrate external consumers in this PR; consumer migration starts after + the new model path and compatibility shim are merged. - Do not add temporary compatibility code unless a matching `RUSTFS_COMPAT_TODO()` marker and cleanup-register entry are added.