Skip to content

Commit eca3def

Browse files
committed
feat: new config system
1 parent 4155bc0 commit eca3def

2 files changed

Lines changed: 256 additions & 109 deletions

File tree

src/configuration/config.rs

Lines changed: 248 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,278 @@
1-
use code0_flow::flow_config::{env_with_default, environment::Environment, mode::Mode};
1+
use std::{fmt, path::Path};
22

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};
125

13-
/// Aquila mode
14-
///
15-
/// Options:
16-
/// `static` (default)
17-
/// `dynamic`
18-
pub mode: Mode,
6+
use super::{env::Environment, mode::Mode};
197

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";
2210

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+
}
2523

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+
}
2830

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+
}
3136

32-
/// URL to the `Sagittarius` Server.
37+
#[derive(Clone, Deserialize, Serialize)]
38+
#[serde(default)]
39+
pub struct DynamicConfig {
3340
pub backend_url: String,
41+
pub backend_token: String,
42+
pub backend_unary_timeout_secs: u64,
43+
}
3444

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+
}
3758

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+
}
4066

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+
}
4274

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+
}
4489

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+
}
4798

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+
}
50106

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+
}
53116

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+
}
56125
}
57126

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 {
63128
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+
}
65134
}
66135
}
67136

68137
impl Config {
69138
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)?;
107162
}
163+
164+
builder.build()?.try_deserialize()
108165
}
109166

110167
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 {"));
112277
}
113278
}

0 commit comments

Comments
 (0)