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