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}