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}