Unify control-plane TOML config serialization and shared telemetry settings
This commit is contained in:
4 files changed
+105
-128
No files matched your search
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
|
||||
|
||||
use crate::cli::{InitConfigArgs, ServeArgs};
|
||||
use crate::config::resolve::{load_file_config, normalize_public_url, resolve_path};
|
||||
use crate::config::types::{DEFAULT_CONFIG, WritableConfig, WritableTelemetry};
|
||||
use crate::config::types::{DEFAULT_CONFIG, FileConfig, FileTelemetryConfig};
|
||||
|
||||
pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||
if args.stdout && args.config_file.is_some() {
|
||||
@@ -44,10 +44,7 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||
.unwrap_or_else(|| "<defaults>".to_string())
|
||||
)
|
||||
})?,
|
||||
None => defaults
|
||||
.bind
|
||||
.parse()
|
||||
.expect("static default bind should parse"),
|
||||
None => defaults.bind,
|
||||
},
|
||||
};
|
||||
let public_url = normalize_public_url(
|
||||
@@ -90,29 +87,35 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||
args.bootstrap_enroll_tokens.clone()
|
||||
};
|
||||
|
||||
let body = WritableConfig {
|
||||
data_dir,
|
||||
bind: bind.to_string(),
|
||||
public_url,
|
||||
state_file,
|
||||
command_timeout_ms: args
|
||||
.command_timeout_ms
|
||||
.or(base.command_timeout_ms)
|
||||
.unwrap_or(defaults.command_timeout_ms)
|
||||
.max(1),
|
||||
enroll_token_ttl_seconds: args
|
||||
.enroll_token_ttl_seconds
|
||||
.or(base.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||
.max(1),
|
||||
observation_retention_seconds: args
|
||||
.observation_retention_seconds
|
||||
.or(base.observation_retention_seconds)
|
||||
.unwrap_or(defaults.observation_retention_seconds),
|
||||
pid_file,
|
||||
ui_dist_dir,
|
||||
bootstrap_enroll_tokens,
|
||||
telemetry: WritableTelemetry {
|
||||
let body = FileConfig {
|
||||
data_dir: Some(data_dir),
|
||||
bind: Some(bind.to_string()),
|
||||
public_url: Some(public_url),
|
||||
state_file: Some(state_file),
|
||||
command_timeout_ms: Some(
|
||||
args.command_timeout_ms
|
||||
.or(base.command_timeout_ms)
|
||||
.unwrap_or(defaults.command_timeout.as_millis() as u64)
|
||||
.max(1),
|
||||
),
|
||||
enroll_token_ttl_seconds: Some(
|
||||
args.enroll_token_ttl_seconds
|
||||
.or(base.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl.as_secs())
|
||||
.max(1),
|
||||
),
|
||||
observation_retention_seconds: Some(
|
||||
args.observation_retention_seconds
|
||||
.or(base.observation_retention_seconds)
|
||||
.unwrap_or(defaults.observation_retention.as_secs()),
|
||||
),
|
||||
pid_file: Some(pid_file),
|
||||
ui_dist_dir: Some(ui_dist_dir),
|
||||
// Preserve the previous generated template's omission of an empty list.
|
||||
// Explicit empty lists still round-trip through FileConfig unchanged.
|
||||
bootstrap_enroll_tokens: (!bootstrap_enroll_tokens.is_empty())
|
||||
.then_some(bootstrap_enroll_tokens),
|
||||
telemetry: Some(FileTelemetryConfig {
|
||||
console_filter: base_telemetry
|
||||
.console_filter
|
||||
.or_else(|| defaults.telemetry.console_filter.clone()),
|
||||
@@ -130,16 +133,18 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||
.clone()
|
||||
.or(base_telemetry.otlp_endpoint)
|
||||
.or_else(|| defaults.telemetry.otlp_endpoint.clone()),
|
||||
service_name: args
|
||||
.telemetry_service_name
|
||||
.clone()
|
||||
.or(base_telemetry.service_name)
|
||||
.unwrap_or_else(|| defaults.telemetry.service_name.clone()),
|
||||
json_logs: args
|
||||
.telemetry_json_logs
|
||||
.or(base_telemetry.json_logs)
|
||||
.unwrap_or(defaults.telemetry.json_logs),
|
||||
},
|
||||
service_name: Some(
|
||||
args.telemetry_service_name
|
||||
.clone()
|
||||
.or(base_telemetry.service_name)
|
||||
.unwrap_or_else(|| defaults.telemetry.service_name.clone()),
|
||||
),
|
||||
json_logs: Some(
|
||||
args.telemetry_json_logs
|
||||
.or(base_telemetry.json_logs)
|
||||
.unwrap_or(defaults.telemetry.json_logs),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
let rendered = toml::to_string_pretty(&body).context("failed to render config template")?;
|
||||
@@ -332,7 +337,7 @@ json_logs = true
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["command_timeout_ms"].as_integer(),
|
||||
Some(defaults.command_timeout_ms as i64)
|
||||
Some(defaults.command_timeout.as_millis() as i64)
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["telemetry"]["service_name"].as_str(),
|
||||
|
||||
@@ -33,10 +33,7 @@ impl DaemonConfig {
|
||||
args.config_file.display()
|
||||
)
|
||||
})?,
|
||||
None => defaults
|
||||
.bind
|
||||
.parse()
|
||||
.expect("static default bind should parse"),
|
||||
None => defaults.bind,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -57,21 +54,21 @@ impl DaemonConfig {
|
||||
let command_timeout = Duration::from_millis(
|
||||
args.command_timeout_ms
|
||||
.or(file.command_timeout_ms)
|
||||
.unwrap_or(defaults.command_timeout_ms)
|
||||
.unwrap_or(defaults.command_timeout.as_millis() as u64)
|
||||
.max(1),
|
||||
);
|
||||
|
||||
let enroll_token_ttl = Duration::from_secs(
|
||||
args.enroll_token_ttl_seconds
|
||||
.or(file.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl.as_secs())
|
||||
.max(1),
|
||||
);
|
||||
|
||||
let observation_retention = Duration::from_secs(
|
||||
args.observation_retention_seconds
|
||||
.or(file.observation_retention_seconds)
|
||||
.unwrap_or(defaults.observation_retention_seconds)
|
||||
.unwrap_or(defaults.observation_retention.as_secs())
|
||||
.max(1),
|
||||
);
|
||||
|
||||
@@ -146,7 +143,7 @@ pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<Issue
|
||||
let ttl = Duration::from_secs(
|
||||
args.ttl_seconds
|
||||
.or(file.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||
.unwrap_or(defaults.enroll_token_ttl.as_secs())
|
||||
.max(1),
|
||||
);
|
||||
|
||||
@@ -229,7 +226,8 @@ fn resolve_admin_public_url(
|
||||
}
|
||||
|
||||
fn resolve_telemetry(file: Option<FileTelemetryConfig>) -> TelemetryConfig {
|
||||
let mut out = TelemetryConfig::default();
|
||||
// Service-specific defaults live in the CC template, not the shared type.
|
||||
let mut out = DEFAULT_CONFIG.telemetry.clone();
|
||||
if let Some(file) = file {
|
||||
out.authorization_env = file.authorization_env;
|
||||
out.console_filter = file.console_filter;
|
||||
@@ -344,10 +342,7 @@ mod tests {
|
||||
resolved.state_file,
|
||||
defaults.data_dir.join(&defaults.state_file)
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.command_timeout,
|
||||
std::time::Duration::from_millis(defaults.command_timeout_ms)
|
||||
);
|
||||
assert_eq!(resolved.command_timeout, defaults.command_timeout);
|
||||
assert_eq!(
|
||||
resolved.telemetry.service_name,
|
||||
defaults.telemetry.service_name
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use wakey_core::telemetry::TelemetryConfig;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DaemonConfig {
|
||||
@@ -20,33 +21,9 @@ pub struct DaemonConfig {
|
||||
pub telemetry: TelemetryConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelemetryConfig {
|
||||
pub console_filter: Option<String>,
|
||||
pub otlp_log_filter: Option<String>,
|
||||
pub trace_filter: Option<String>,
|
||||
pub otlp_endpoint: Option<String>,
|
||||
pub authorization_env: Option<String>,
|
||||
pub service_name: String,
|
||||
pub json_logs: bool,
|
||||
}
|
||||
|
||||
impl Default for TelemetryConfig {
|
||||
fn default() -> Self {
|
||||
let defaults = &DEFAULT_CONFIG.telemetry;
|
||||
Self {
|
||||
console_filter: defaults.console_filter.clone(),
|
||||
otlp_log_filter: defaults.otlp_log_filter.clone(),
|
||||
trace_filter: defaults.trace_filter.clone(),
|
||||
otlp_endpoint: defaults.otlp_endpoint.clone(),
|
||||
authorization_env: defaults.authorization_env.clone(),
|
||||
service_name: defaults.service_name.clone(),
|
||||
json_logs: defaults.json_logs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
/// TOML DTO for both loading partial files and generating complete files.
|
||||
/// Options preserve omission for precedence; runtime conversion stays separate.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub(crate) struct FileConfig {
|
||||
pub(crate) data_dir: Option<PathBuf>,
|
||||
pub(crate) bind: Option<String>,
|
||||
@@ -62,8 +39,10 @@ pub(crate) struct FileConfig {
|
||||
pub(crate) telemetry: Option<FileTelemetryConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub(crate) struct FileTelemetryConfig {
|
||||
// Missing fields must remain distinguishable from explicit values when
|
||||
// init-config merges an existing file with CLI overrides and defaults.
|
||||
pub(crate) console_filter: Option<String>,
|
||||
pub(crate) otlp_log_filter: Option<String>,
|
||||
pub(crate) trace_filter: Option<String>,
|
||||
@@ -73,50 +52,19 @@ pub(crate) struct FileTelemetryConfig {
|
||||
pub(crate) json_logs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct WritableConfig {
|
||||
pub(crate) data_dir: PathBuf,
|
||||
pub(crate) bind: String,
|
||||
pub(crate) public_url: String,
|
||||
pub(crate) state_file: PathBuf,
|
||||
pub(crate) command_timeout_ms: u64,
|
||||
pub(crate) enroll_token_ttl_seconds: u64,
|
||||
pub(crate) observation_retention_seconds: u64,
|
||||
pub(crate) pid_file: PathBuf,
|
||||
pub(crate) ui_dist_dir: PathBuf,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub(crate) bootstrap_enroll_tokens: Vec<String>,
|
||||
pub(crate) telemetry: WritableTelemetry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct WritableTelemetry {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) console_filter: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) otlp_log_filter: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) trace_filter: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) otlp_endpoint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) authorization_env: Option<String>,
|
||||
pub(crate) service_name: String,
|
||||
pub(crate) json_logs: bool,
|
||||
}
|
||||
|
||||
pub(crate) static DEFAULT_CONFIG: LazyLock<WritableConfig> = LazyLock::new(|| WritableConfig {
|
||||
// Relative template paths are resolved under data_dir by the runtime loader.
|
||||
pub(crate) static DEFAULT_CONFIG: LazyLock<DaemonConfig> = LazyLock::new(|| DaemonConfig {
|
||||
data_dir: crate::cli::DEFAULT_DATA_DIR.into(),
|
||||
bind: "0.0.0.0:8080".to_string(),
|
||||
bind: SocketAddr::from(([0, 0, 0, 0], 8080)),
|
||||
public_url: "http://127.0.0.1:8080".to_string(),
|
||||
state_file: "state.sqlite3".into(),
|
||||
command_timeout_ms: 30_000,
|
||||
enroll_token_ttl_seconds: 86_400,
|
||||
observation_retention_seconds: 2_592_000,
|
||||
command_timeout: Duration::from_millis(30_000),
|
||||
enroll_token_ttl: Duration::from_secs(86_400),
|
||||
observation_retention: Duration::from_secs(2_592_000),
|
||||
pid_file: "wakey-control-plane.pid".into(),
|
||||
ui_dist_dir: "ui/dist".into(),
|
||||
bootstrap_enroll_tokens: Vec::new(),
|
||||
telemetry: WritableTelemetry {
|
||||
telemetry: TelemetryConfig {
|
||||
console_filter: None,
|
||||
otlp_log_filter: None,
|
||||
trace_filter: None,
|
||||
@@ -139,3 +87,41 @@ pub struct StateAccessSettings {
|
||||
pub state_file: PathBuf,
|
||||
pub public_url: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{FileConfig, FileTelemetryConfig};
|
||||
|
||||
#[test]
|
||||
fn file_dto_round_trip_preserves_omission_and_explicit_values() {
|
||||
let omitted: FileConfig = toml::from_str("").unwrap();
|
||||
assert_eq!(omitted, FileConfig::default());
|
||||
assert_eq!(toml::to_string(&omitted).unwrap(), "");
|
||||
|
||||
let explicit = FileConfig {
|
||||
bootstrap_enroll_tokens: Some(Vec::new()),
|
||||
observation_retention_seconds: Some(0),
|
||||
telemetry: Some(FileTelemetryConfig {
|
||||
json_logs: Some(false),
|
||||
console_filter: Some("info".into()),
|
||||
otlp_log_filter: Some("debug".into()),
|
||||
trace_filter: Some("info".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let rendered = toml::to_string_pretty(&explicit).unwrap();
|
||||
let restored: FileConfig = toml::from_str(&rendered).unwrap();
|
||||
assert_eq!(restored, explicit);
|
||||
assert_ne!(restored, omitted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_token_alias_serializes_with_canonical_name() {
|
||||
let file: FileConfig = toml::from_str("enroll_tokens = []").unwrap();
|
||||
assert_eq!(file.bootstrap_enroll_tokens, Some(Vec::new()));
|
||||
let rendered = toml::to_string(&file).unwrap();
|
||||
assert!(rendered.contains("bootstrap_enroll_tokens = []"));
|
||||
assert!(!rendered.starts_with("enroll_tokens"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user