|
1 | | -use code0_flow::flow_config::{env_with_default, environment::Environment, mode::Mode}; |
| 1 | +use std::{fmt, path::Path}; |
2 | 2 |
|
3 | | -/// Struct for all relevant `Aquila` startup configurations |
4 | | -pub struct Config { |
5 | | - /// Aquila environment |
6 | | - /// |
7 | | - /// Options: |
8 | | - /// `development` (default) |
9 | | - /// `staging` |
10 | | - /// `production` |
11 | | - pub environment: Environment, |
| 3 | +use config::{Config as ConfigLoader, ConfigError, File}; |
| 4 | +use serde::{Deserialize, Serialize}; |
12 | 5 |
|
13 | | - /// Aquila mode |
14 | | - /// |
15 | | - /// Options: |
16 | | - /// `static` (default) |
17 | | - /// `dynamic` |
18 | | - pub mode: Mode, |
| 6 | +use super::{env::Environment, mode::Mode}; |
19 | 7 |
|
20 | | - /// URL to the NATS Server. |
21 | | - pub nats_url: String, |
| 8 | +const CONFIG_FILE: &str = "aquila"; |
| 9 | +const BACKEND_TOKEN_ENV: &str = "AQUILA_BACKEND_TOKEN"; |
22 | 10 |
|
23 | | - /// Name of the NATS Bucket. |
24 | | - pub nats_bucket: String, |
| 11 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 12 | +#[serde(default)] |
| 13 | +pub struct Config { |
| 14 | + pub environment: Environment, |
| 15 | + pub mode: Mode, |
| 16 | + pub log_level: String, |
| 17 | + pub nats: Nats, |
| 18 | + pub static_config: StaticConfig, |
| 19 | + pub dynamic_config: DynamicConfig, |
| 20 | + pub grpc: Grpc, |
| 21 | + pub runtime_status: RuntimeStatus, |
| 22 | +} |
25 | 23 |
|
26 | | - /// Fallback file to load flows if gRPC & scheduling is disabled. |
27 | | - pub flow_fallback_path: String, |
| 24 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 25 | +#[serde(default)] |
| 26 | +pub struct Nats { |
| 27 | + pub url: String, |
| 28 | + pub bucket: String, |
| 29 | +} |
28 | 30 |
|
29 | | - /// Verification Token required for internal communication |
30 | | - pub runtime_token: String, |
| 31 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 32 | +#[serde(default)] |
| 33 | +pub struct StaticConfig { |
| 34 | + pub flow_path: String, |
| 35 | +} |
31 | 36 |
|
32 | | - /// URL to the `Sagittarius` Server. |
| 37 | +#[derive(Clone, Deserialize, Serialize)] |
| 38 | +#[serde(default)] |
| 39 | +pub struct DynamicConfig { |
33 | 40 | pub backend_url: String, |
| 41 | + pub backend_token: String, |
| 42 | + pub backend_unary_timeout_secs: u64, |
| 43 | +} |
34 | 44 |
|
35 | | - // Port of the `Aquila` Server |
36 | | - pub grpc_port: u16, |
| 45 | +impl std::fmt::Debug for DynamicConfig { |
| 46 | + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 47 | + formatter |
| 48 | + .debug_struct("DynamicConfig") |
| 49 | + .field("backend_url", &self.backend_url) |
| 50 | + .field("backend_token", &"[FILTERED]") |
| 51 | + .field( |
| 52 | + "backend_unary_timeout_secs", |
| 53 | + &self.backend_unary_timeout_secs, |
| 54 | + ) |
| 55 | + .finish() |
| 56 | + } |
| 57 | +} |
37 | 58 |
|
38 | | - // Host of the `Aquila` Server |
39 | | - pub grpc_host: String, |
| 59 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 60 | +#[serde(default)] |
| 61 | +pub struct Grpc { |
| 62 | + pub host: String, |
| 63 | + pub port: u16, |
| 64 | + pub health_service: bool, |
| 65 | +} |
40 | 66 |
|
41 | | - pub with_health_service: bool, |
| 67 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 68 | +#[serde(default)] |
| 69 | +pub struct RuntimeStatus { |
| 70 | + pub not_responding_after_secs: u64, |
| 71 | + pub stopped_after_not_responding_secs: u64, |
| 72 | + pub monitor_interval_secs: u64, |
| 73 | +} |
42 | 74 |
|
43 | | - pub service_config_path: String, |
| 75 | +impl Default for Config { |
| 76 | + fn default() -> Self { |
| 77 | + Self { |
| 78 | + environment: Environment::Development, |
| 79 | + mode: Mode::Static, |
| 80 | + log_level: "debug".into(), |
| 81 | + nats: Nats::default(), |
| 82 | + static_config: StaticConfig::default(), |
| 83 | + dynamic_config: DynamicConfig::default(), |
| 84 | + grpc: Grpc::default(), |
| 85 | + runtime_status: RuntimeStatus::default(), |
| 86 | + } |
| 87 | + } |
| 88 | +} |
44 | 89 |
|
45 | | - /// Runtime heartbeat timeout in seconds before a service is marked as NOT_RESPONDING. |
46 | | - pub runtime_status_not_responding_after_secs: u64, |
| 90 | +impl Default for Nats { |
| 91 | + fn default() -> Self { |
| 92 | + Self { |
| 93 | + url: "nats://localhost:4222".into(), |
| 94 | + bucket: "flow_store".into(), |
| 95 | + } |
| 96 | + } |
| 97 | +} |
47 | 98 |
|
48 | | - /// Additional timeout in seconds after NOT_RESPONDING before a service is marked as STOPPED. |
49 | | - pub runtime_status_stopped_after_not_responding_secs: u64, |
| 99 | +impl Default for StaticConfig { |
| 100 | + fn default() -> Self { |
| 101 | + Self { |
| 102 | + flow_path: "./flowExport.json".into(), |
| 103 | + } |
| 104 | + } |
| 105 | +} |
50 | 106 |
|
51 | | - /// Interval in seconds for the runtime status timeout monitor loop. |
52 | | - pub runtime_status_monitor_interval_secs: u64, |
| 107 | +impl Default for DynamicConfig { |
| 108 | + fn default() -> Self { |
| 109 | + Self { |
| 110 | + backend_url: "http://localhost:50051".into(), |
| 111 | + backend_token: "default_session_token".into(), |
| 112 | + backend_unary_timeout_secs: 5, |
| 113 | + } |
| 114 | + } |
| 115 | +} |
53 | 116 |
|
54 | | - /// Timeout in seconds for unary RPC calls from Aquila to Sagittarius. |
55 | | - pub sagittarius_unary_rpc_timeout_secs: u64, |
| 117 | +impl Default for Grpc { |
| 118 | + fn default() -> Self { |
| 119 | + Self { |
| 120 | + host: "127.0.0.1".into(), |
| 121 | + port: 8081, |
| 122 | + health_service: false, |
| 123 | + } |
| 124 | + } |
56 | 125 | } |
57 | 126 |
|
58 | | -/// Implementation for all relevant `Aquila` startup configurations |
59 | | -/// |
60 | | -/// Behavior: |
61 | | -/// Searches for the env. file at root level. Filename: `.env` |
62 | | -impl Default for Config { |
| 127 | +impl Default for RuntimeStatus { |
63 | 128 | fn default() -> Self { |
64 | | - Self::new() |
| 129 | + Self { |
| 130 | + not_responding_after_secs: 90, |
| 131 | + stopped_after_not_responding_secs: 180, |
| 132 | + monitor_interval_secs: 30, |
| 133 | + } |
65 | 134 | } |
66 | 135 | } |
67 | 136 |
|
68 | 137 | impl Config { |
69 | 138 | pub fn new() -> Self { |
70 | | - Config { |
71 | | - environment: env_with_default("ENVIRONMENT", Environment::Development), |
72 | | - mode: env_with_default("MODE", Mode::STATIC), |
73 | | - nats_url: env_with_default("NATS_URL", String::from("nats://localhost:4222")), |
74 | | - nats_bucket: env_with_default("NATS_BUCKET", String::from("flow_store")), |
75 | | - flow_fallback_path: env_with_default( |
76 | | - "FLOW_FALLBACK_PATH", |
77 | | - String::from("./flowExport.json"), |
78 | | - ), |
79 | | - grpc_port: env_with_default("GRPC_PORT", 8081), |
80 | | - grpc_host: env_with_default("GRPC_HOST", String::from("127.0.0.1")), |
81 | | - with_health_service: env_with_default("WITH_HEALTH_SERVICE", false), |
82 | | - runtime_token: env_with_default("RUNTIME_TOKEN", String::from("default_session_token")), |
83 | | - backend_url: env_with_default( |
84 | | - "SAGITTARIUS_URL", |
85 | | - String::from("http://localhost:50051"), |
86 | | - ), |
87 | | - service_config_path: env_with_default( |
88 | | - "SERVICE_CONFIG_PATH", |
89 | | - String::from("./service.configuration.json"), |
90 | | - ), |
91 | | - runtime_status_not_responding_after_secs: env_with_default( |
92 | | - "RUNTIME_STATUS_NOT_RESPONDING_AFTER_SECS", |
93 | | - 90_u64, |
94 | | - ), |
95 | | - runtime_status_stopped_after_not_responding_secs: env_with_default( |
96 | | - "RUNTIME_STATUS_STOPPED_AFTER_NOT_RESPONDING_SECS", |
97 | | - 180_u64, |
98 | | - ), |
99 | | - runtime_status_monitor_interval_secs: env_with_default( |
100 | | - "RUNTIME_STATUS_MONITOR_INTERVAL_SECS", |
101 | | - 30_u64, |
102 | | - ), |
103 | | - sagittarius_unary_rpc_timeout_secs: env_with_default( |
104 | | - "SAGITTARIUS_UNARY_RPC_TIMEOUT_SECS", |
105 | | - 5_u64, |
106 | | - ), |
| 139 | + Self::try_new() |
| 140 | + .unwrap_or_else(|error| panic!("failed to load Aquila configuration: {error}")) |
| 141 | + } |
| 142 | + |
| 143 | + pub fn try_new() -> Result<Self, ConfigError> { |
| 144 | + Self::try_from_optional_path(None) |
| 145 | + } |
| 146 | + |
| 147 | + pub fn try_from_path(path: impl AsRef<Path>) -> Result<Self, ConfigError> { |
| 148 | + Self::try_from_optional_path(Some(path.as_ref())) |
| 149 | + } |
| 150 | + |
| 151 | + fn try_from_optional_path(path: Option<&Path>) -> Result<Self, ConfigError> { |
| 152 | + let mut builder = |
| 153 | + ConfigLoader::builder().add_source(ConfigLoader::try_from(&Self::default())?); |
| 154 | + |
| 155 | + builder = match path { |
| 156 | + Some(path) => builder.add_source(File::from(path).required(true)), |
| 157 | + None => builder.add_source(File::with_name(CONFIG_FILE).required(false)), |
| 158 | + }; |
| 159 | + |
| 160 | + if let Ok(token) = std::env::var(BACKEND_TOKEN_ENV) { |
| 161 | + builder = builder.set_override("dynamic_config.backend_token", token)?; |
107 | 162 | } |
| 163 | + |
| 164 | + builder.build()?.try_deserialize() |
108 | 165 | } |
109 | 166 |
|
110 | 167 | pub fn is_static(&self) -> bool { |
111 | | - self.mode == Mode::STATIC |
| 168 | + self.mode == Mode::Static |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +impl fmt::Display for Config { |
| 173 | + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 174 | + writeln!(formatter, "Aquila configuration")?; |
| 175 | + writeln!(formatter, " Environment: {}", self.environment)?; |
| 176 | + writeln!(formatter, " Mode: {}", self.mode)?; |
| 177 | + writeln!(formatter, " Log level: {}", self.log_level)?; |
| 178 | + writeln!(formatter, " NATS")?; |
| 179 | + writeln!(formatter, " URL: {}", self.nats.url)?; |
| 180 | + writeln!(formatter, " Bucket: {}", self.nats.bucket)?; |
| 181 | + writeln!(formatter, " gRPC")?; |
| 182 | + writeln!( |
| 183 | + formatter, |
| 184 | + " Address: {}:{}", |
| 185 | + self.grpc.host, self.grpc.port |
| 186 | + )?; |
| 187 | + writeln!( |
| 188 | + formatter, |
| 189 | + " Health service: {}", |
| 190 | + self.grpc.health_service |
| 191 | + )?; |
| 192 | + writeln!(formatter, " Static mode")?; |
| 193 | + writeln!(formatter, " Flow path: {}", self.static_config.flow_path)?; |
| 194 | + writeln!(formatter, " Dynamic mode")?; |
| 195 | + writeln!( |
| 196 | + formatter, |
| 197 | + " Backend URL: {}", |
| 198 | + self.dynamic_config.backend_url |
| 199 | + )?; |
| 200 | + writeln!(formatter, " Backend token: [FILTERED]")?; |
| 201 | + writeln!( |
| 202 | + formatter, |
| 203 | + " Request timeout: {}s", |
| 204 | + self.dynamic_config.backend_unary_timeout_secs |
| 205 | + )?; |
| 206 | + writeln!(formatter, " Runtime status")?; |
| 207 | + writeln!( |
| 208 | + formatter, |
| 209 | + " Not responding after: {}s", |
| 210 | + self.runtime_status.not_responding_after_secs |
| 211 | + )?; |
| 212 | + writeln!( |
| 213 | + formatter, |
| 214 | + " Stopped after: {}s", |
| 215 | + self.runtime_status.stopped_after_not_responding_secs |
| 216 | + )?; |
| 217 | + write!( |
| 218 | + formatter, |
| 219 | + " Monitor interval: {}s", |
| 220 | + self.runtime_status.monitor_interval_secs |
| 221 | + ) |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +#[cfg(test)] |
| 226 | +mod tests { |
| 227 | + use std::sync::Mutex; |
| 228 | + |
| 229 | + use super::Config; |
| 230 | + |
| 231 | + static ENV_LOCK: Mutex<()> = Mutex::new(()); |
| 232 | + |
| 233 | + #[test] |
| 234 | + fn environment_overrides_backend_token() { |
| 235 | + let _guard = ENV_LOCK.lock().expect("environment test lock poisoned"); |
| 236 | + |
| 237 | + // SAFETY: access to these process-wide variables is serialized for this test. |
| 238 | + unsafe { |
| 239 | + std::env::set_var("AQUILA_BACKEND_TOKEN", "environment-token"); |
| 240 | + } |
| 241 | + |
| 242 | + let config = Config::try_new().expect("configuration should load"); |
| 243 | + |
| 244 | + // SAFETY: access to these process-wide variables is serialized for this test. |
| 245 | + unsafe { |
| 246 | + std::env::remove_var("AQUILA_BACKEND_TOKEN"); |
| 247 | + } |
| 248 | + |
| 249 | + assert_eq!(config.dynamic_config.backend_token, "environment-token"); |
| 250 | + } |
| 251 | + |
| 252 | + #[test] |
| 253 | + fn debug_output_filters_backend_token() { |
| 254 | + let mut config = Config::default(); |
| 255 | + config.dynamic_config.backend_token = "super-secret".into(); |
| 256 | + |
| 257 | + let output = format!("{config:#?}"); |
| 258 | + |
| 259 | + assert!(output.contains("[FILTERED]")); |
| 260 | + assert!(!output.contains("super-secret")); |
| 261 | + } |
| 262 | + |
| 263 | + #[test] |
| 264 | + fn display_output_is_readable_and_filters_backend_token() { |
| 265 | + let mut config = Config::default(); |
| 266 | + config.dynamic_config.backend_token = "super-secret".into(); |
| 267 | + |
| 268 | + let output = config.to_string(); |
| 269 | + |
| 270 | + assert!(output.starts_with("Aquila configuration\n")); |
| 271 | + assert!(output.contains(" Environment: development")); |
| 272 | + assert!(output.contains(" Address: 127.0.0.1:8081")); |
| 273 | + assert!(output.contains(" Request timeout: 5s")); |
| 274 | + assert!(output.contains(" Backend token: [FILTERED]")); |
| 275 | + assert!(!output.contains("super-secret")); |
| 276 | + assert!(!output.contains("Config {")); |
112 | 277 | } |
113 | 278 | } |
0 commit comments