Skip to main content

d_engine_core/config/
mod.rs

1//! Configuration management module for distributed Raft consensus engine.
2//!
3//! Provides hierarchical configuration loading and validation with:
4//! - Default values as code base
5//! - Environment variable overrides
6//! - Configuration file support
7//! - Component-wise validation
8mod cluster;
9use std::fmt::Debug;
10use std::path::Path;
11mod lease;
12mod network;
13mod raft;
14mod retry;
15mod storage;
16mod tls;
17pub use cluster::*;
18use config::ConfigError;
19pub use lease::*;
20pub use network::*;
21pub use raft::*;
22pub use retry::*;
23pub use storage::*;
24pub use tls::*;
25#[cfg(test)]
26mod config_test;
27#[cfg(test)]
28mod lease_test;
29#[cfg(test)]
30mod network_test;
31#[cfg(test)]
32mod raft_test;
33#[cfg(test)]
34mod tls_test;
35use std::env;
36
37use config::Config;
38use config::Environment;
39use config::File;
40use serde::Deserialize;
41use serde::Serialize;
42
43use crate::Error;
44use crate::Result;
45
46/// Main configuration container for Raft consensus engine components
47///
48/// Combines all subsystem configurations with hierarchical override support:
49/// 1. Default values from code implementation
50/// 2. Configuration file specified by `CONFIG_PATH`
51/// 3. Environment variables (highest priority)
52#[derive(Serialize, Deserialize, Clone, Default)]
53pub struct RaftNodeConfig {
54    /// Cluster topology and node configuration
55    pub cluster: ClusterConfig,
56    /// Network communication parameters
57    pub network: NetworkConfig,
58    /// Core Raft algorithm parameters
59    pub raft: RaftConfig,
60    /// Storage engine infrastructure configuration
61    pub storage: StorageConfig,
62    /// Retry policies for distributed operations
63    pub retry: RetryPolicies,
64    /// TLS/SSL security configuration
65    pub tls: TlsConfig,
66}
67impl Debug for RaftNodeConfig {
68    fn fmt(
69        &self,
70        f: &mut std::fmt::Formatter<'_>,
71    ) -> std::fmt::Result {
72        f.debug_struct("RaftNodeConfig").field("cluster", &self.cluster).finish()
73    }
74}
75impl RaftNodeConfig {
76    /// Loads configuration from hierarchical sources without validation.
77    ///
78    /// Configuration sources are merged in the following order (later sources override earlier):
79    /// 1. Type defaults (lowest priority)
80    /// 2. Configuration file from `CONFIG_PATH` environment variable (if set)
81    /// 3. Environment variables with `RAFT__` prefix (highest priority)
82    ///
83    /// # Note
84    /// This method does NOT validate the configuration. Validation is deferred to allow
85    /// further overrides via `with_override_config()`. Callers MUST call `validate()`
86    /// before using the configuration.
87    ///
88    /// # Returns
89    /// Merged configuration instance or error if config file parsing fails.
90    ///
91    /// # Examples
92    /// ```ignore
93    /// // Load with default values only
94    /// let cfg = RaftNodeConfig::new()?.validate()?;
95    ///
96    /// // Load with config file and environment variables
97    /// std::env::set_var("CONFIG_PATH", "config/cluster.toml");
98    /// std::env::set_var("RAFT__CLUSTER__NODE_ID", "100");
99    /// let cfg = RaftNodeConfig::new()?.validate()?;
100    ///
101    /// // Apply runtime overrides
102    /// let cfg = RaftNodeConfig::new()?
103    ///     .with_override_config("custom.toml")?
104    ///     .validate()?;
105    /// ```
106    pub fn new() -> Result<Self> {
107        let mut builder = Config::builder().add_source(Config::try_from(&Self::default())?);
108
109        if let Ok(config_path) = env::var("CONFIG_PATH") {
110            builder = builder.add_source(File::with_name(&config_path).required(true));
111        }
112
113        builder = builder.add_source(
114            Environment::with_prefix("RAFT")
115                .separator("__")
116                .ignore_empty(true)
117                .try_parsing(true),
118        );
119
120        let config: Self = builder.build()?.try_deserialize()?;
121        Ok(config) // No validation - deferred to validate()
122    }
123
124    /// Applies additional configuration overrides from file without validation.
125    ///
126    /// Merging order (later sources override earlier):
127    /// 1. Current configuration values
128    /// 2. New configuration file
129    /// 3. Latest environment variables (highest priority)
130    ///
131    /// # Note
132    /// This method does NOT validate the configuration. Callers MUST call `validate()`
133    /// after all overrides are applied.
134    ///
135    /// # Example
136    /// ```ignore
137    /// let cfg = RaftNodeConfig::new()?
138    ///     .with_override_config("runtime_overrides.toml")?
139    ///     .validate()?;
140    /// ```
141    pub fn with_override_config(
142        &self,
143        path: &str,
144    ) -> Result<Self> {
145        let config: Self = Config::builder()
146            .add_source(Config::try_from(self)?)
147            .add_source(File::with_name(path))
148            .add_source(
149                Environment::with_prefix("RAFT")
150                    .separator("__")
151                    .ignore_empty(true)
152                    .try_parsing(true),
153            )
154            .build()?
155            .try_deserialize()?;
156        Ok(config) // No validation - deferred to validate()
157    }
158
159    /// Validates configuration and returns validated instance.
160    ///
161    /// Consumes self and performs validation of all subsystems. Must be called
162    /// after all configuration overrides to ensure the final config is valid.
163    ///
164    /// # Returns
165    /// Validated configuration or error if validation fails.
166    ///
167    /// # Errors
168    /// Returns validation errors from any subsystem:
169    /// - Invalid port bindings
170    /// - Conflicting node IDs
171    /// - Expired certificates
172    /// - Invalid directory paths
173    ///
174    /// # Example
175    /// ```ignore
176    /// let config = RaftNodeConfig::new()?
177    ///     .with_override_config("app.toml")?
178    ///     .validate()?; // Validation happens here
179    /// ```
180    pub fn validate(self) -> Result<Self> {
181        self.cluster.validate()?;
182        self.raft.validate()?;
183        self.network.validate()?;
184        self.storage.validate()?;
185        self.tls.validate()?;
186        self.retry.validate()?;
187        Ok(self)
188    }
189
190    /// Checks if this node is configured as a learner (not a voter)
191    ///
192    /// A learner node has role=Learner. Only learners can join via JoinCluster RPC.
193    /// Voters have role=Follower/Candidate/Leader and are added via config change (AddNode).
194    ///
195    /// Note: Status (Promotable/ReadOnly/Active) is separate from role.
196    /// This method checks **role only**.
197    pub fn is_learner(&self) -> bool {
198        use d_engine_proto::common::NodeRole;
199
200        self.cluster
201            .initial_cluster
202            .iter()
203            .find(|n| n.id == self.cluster.node_id)
204            .map(|n| n.role == NodeRole::Learner as i32)
205            .unwrap_or(false)
206    }
207}
208
209/// Ensures directory path is valid and writable
210pub(super) fn validate_directory(
211    path: &Path,
212    name: &str,
213) -> Result<()> {
214    if path.as_os_str().is_empty() {
215        return Err(Error::Config(ConfigError::Message(format!(
216            "{name} path cannot be empty"
217        ))));
218    }
219
220    #[cfg(not(test))]
221    {
222        use std::fs;
223        // Check directory existence or create ability
224        if !path.exists() {
225            fs::create_dir_all(path).map_err(|e| {
226                Error::Config(ConfigError::Message(format!(
227                    "Failed to create {} directory at {}: {}",
228                    name,
229                    path.display(),
230                    e
231                )))
232            })?;
233        }
234
235        // Check write permissions
236        let test_file = path.join(".permission_test");
237        fs::write(&test_file, b"test").map_err(|e| {
238            Error::Config(ConfigError::Message(format!(
239                "No write permission in {} directory {}: {}",
240                name,
241                path.display(),
242                e
243            )))
244        })?;
245        fs::remove_file(&test_file).ok();
246    }
247
248    Ok(())
249}