config generation: slight cli ux improvements

This commit is contained in:
lda
2026-04-28 23:37:52 +07:00 Verified
parent 91e49a5806
commit 806cc25655
2 changed files with 147 additions and 14 deletions
+6 -2
View File
@@ -90,6 +90,9 @@ pub struct InitConfigArgs {
#[arg(long = "config-file", alias = "config")] #[arg(long = "config-file", alias = "config")]
pub config_file: Option<PathBuf>, pub config_file: Option<PathBuf>,
#[arg(long)]
pub from_config: Option<PathBuf>,
#[arg(long, conflicts_with = "config_file")] #[arg(long, conflicts_with = "config_file")]
pub stdout: bool, pub stdout: bool,
@@ -126,8 +129,8 @@ pub struct InitConfigArgs {
#[arg(long)] #[arg(long)]
pub telemetry_service_name: Option<String>, pub telemetry_service_name: Option<String>,
#[arg(long)] #[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub telemetry_json_logs: bool, pub telemetry_json_logs: Option<bool>,
#[arg(long)] #[arg(long)]
pub force: bool, pub force: bool,
@@ -238,6 +241,7 @@ pub struct StateStatsArgs {
pub target: AdminTargetArgs, pub target: AdminTargetArgs,
} }
// not the greatest thing ive used, this and the migration code gets deleted shortly anyways
#[derive(Args)] #[derive(Args)]
pub struct ImportSledStateArgs { pub struct ImportSledStateArgs {
#[arg(long)] #[arg(long)]
+141 -12
View File
@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crate::cli::{InitConfigArgs, ServeArgs}; use crate::cli::{InitConfigArgs, ServeArgs};
use crate::config::resolve::{normalize_public_url, resolve_path}; use crate::config::resolve::{load_file_config, normalize_public_url, resolve_path};
use crate::config::types::{WritableConfig, WritableTelemetry}; use crate::config::types::{WritableConfig, WritableTelemetry};
pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> { pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
@@ -21,56 +21,104 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
); );
} }
let bind = args.bind.unwrap_or_else(|| { let base = if let Some(from_config) = &args.from_config {
"0.0.0.0:8080" if !from_config.exists() {
.parse() anyhow::bail!("base config {} does not exist", from_config.display());
.expect("static default bind should parse") }
}); load_file_config(from_config)?
} else {
Default::default()
};
let base_telemetry = base.telemetry.unwrap_or_default();
let bind = match args.bind {
Some(bind) => bind,
None => match base.bind {
Some(ref bind) => bind.parse().with_context(|| {
format!(
"invalid bind address `{bind}` in base config {}",
args.from_config
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "<defaults>".to_string())
)
})?,
None => "0.0.0.0:8080"
.parse()
.expect("static default bind should parse"),
},
};
let public_url = normalize_public_url( let public_url = normalize_public_url(
args.public_url args.public_url
.as_deref() .as_deref()
.or(base.public_url.as_deref())
.unwrap_or("http://127.0.0.1:8080"), .unwrap_or("http://127.0.0.1:8080"),
); );
let data_dir = args let data_dir = args
.data_dir .data_dir
.clone() .clone()
.or(base.data_dir)
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_DATA_DIR)); .unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_DATA_DIR));
let state_file_raw = args let state_file_raw = args
.state_file .state_file
.clone() .clone()
.or(base.state_file)
.unwrap_or_else(|| PathBuf::from("state.sqlite3")); .unwrap_or_else(|| PathBuf::from("state.sqlite3"));
let state_file = resolve_path(&data_dir, state_file_raw); let state_file = resolve_path(&data_dir, state_file_raw);
let pid_file_raw = args let pid_file_raw = args
.pid_file .pid_file
.clone() .clone()
.or(base.pid_file)
.unwrap_or_else(|| PathBuf::from("wakey-control-plane.pid")); .unwrap_or_else(|| PathBuf::from("wakey-control-plane.pid"));
let pid_file = resolve_path(&data_dir, pid_file_raw); let pid_file = resolve_path(&data_dir, pid_file_raw);
let ui_dist_dir = args let ui_dist_dir = args
.ui_dist_dir .ui_dist_dir
.clone() .clone()
.or(base.ui_dist_dir)
.unwrap_or_else(|| PathBuf::from("ui/dist")); .unwrap_or_else(|| PathBuf::from("ui/dist"));
let bootstrap_enroll_tokens = if args.bootstrap_enroll_tokens.is_empty() {
base.bootstrap_enroll_tokens.unwrap_or_default()
} else {
args.bootstrap_enroll_tokens.clone()
};
let body = WritableConfig { let body = WritableConfig {
data_dir, data_dir,
bind: bind.to_string(), bind: bind.to_string(),
public_url, public_url,
state_file, state_file,
command_timeout_ms: args.command_timeout_ms.unwrap_or(30_000).max(1), command_timeout_ms: args
enroll_token_ttl_seconds: args.enroll_token_ttl_seconds.unwrap_or(86_400).max(1), .command_timeout_ms
.or(base.command_timeout_ms)
.unwrap_or(30_000)
.max(1),
enroll_token_ttl_seconds: args
.enroll_token_ttl_seconds
.or(base.enroll_token_ttl_seconds)
.unwrap_or(86_400)
.max(1),
pid_file, pid_file,
ui_dist_dir, ui_dist_dir,
bootstrap_enroll_tokens: args.bootstrap_enroll_tokens.clone(), bootstrap_enroll_tokens,
telemetry: WritableTelemetry { telemetry: WritableTelemetry {
otlp_endpoint: args.telemetry_otlp_endpoint.clone(), otlp_endpoint: args
.telemetry_otlp_endpoint
.clone()
.or(base_telemetry.otlp_endpoint),
service_name: args service_name: args
.telemetry_service_name .telemetry_service_name
.clone() .clone()
.or(base_telemetry.service_name)
.unwrap_or_else(|| "wakey-control-plane".to_string()), .unwrap_or_else(|| "wakey-control-plane".to_string()),
json_logs: args.telemetry_json_logs, json_logs: args
.telemetry_json_logs
.or(base_telemetry.json_logs)
.unwrap_or(false),
}, },
}; };
@@ -108,12 +156,93 @@ pub fn bootstrap_config_if_missing(args: &ServeArgs) -> Result<bool> {
command_timeout_ms: args.command_timeout_ms, command_timeout_ms: args.command_timeout_ms,
enroll_token_ttl_seconds: args.enroll_token_ttl_seconds, enroll_token_ttl_seconds: args.enroll_token_ttl_seconds,
bootstrap_enroll_tokens: args.bootstrap_enroll_tokens.clone(), bootstrap_enroll_tokens: args.bootstrap_enroll_tokens.clone(),
from_config: None,
telemetry_otlp_endpoint: None, telemetry_otlp_endpoint: None,
telemetry_service_name: None, telemetry_service_name: None,
telemetry_json_logs: false, telemetry_json_logs: None,
force: false, force: false,
}; };
write_init_config(&init)?; write_init_config(&init)?;
Ok(true) Ok(true)
} }
#[cfg(test)]
mod tests {
use super::write_init_config;
use crate::cli::InitConfigArgs;
fn temp_test_dir(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"wakey-cc-init-config-{name}-{}",
std::process::id()
))
}
#[test]
fn init_config_from_config_preserves_base_and_applies_overrides() {
let dir = temp_test_dir("from-config");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
let base = dir.join("base.toml");
let out = dir.join("out.toml");
std::fs::write(
&base,
r#"
data_dir = "/tmp/wakey-base"
bind = "127.0.0.1:9090"
public_url = "https://old.example/"
state_file = "old.sqlite3"
command_timeout_ms = 1234
enroll_token_ttl_seconds = 5678
pid_file = "old.pid"
ui_dist_dir = "/opt/wakey/ui"
bootstrap_enroll_tokens = ["old-token"]
[telemetry]
otlp_endpoint = "http://otel.example"
service_name = "old-service"
json_logs = true
"#,
)
.expect("write base config");
write_init_config(&InitConfigArgs {
config_file: Some(out.clone()),
from_config: Some(base),
stdout: false,
data_dir: None,
bind: None,
public_url: Some("https://new.example/".to_string()),
state_file: None,
pid_file: None,
ui_dist_dir: None,
command_timeout_ms: Some(999),
enroll_token_ttl_seconds: None,
bootstrap_enroll_tokens: Vec::new(),
telemetry_otlp_endpoint: None,
telemetry_service_name: None,
telemetry_json_logs: None,
force: false,
})
.expect("write derived config");
let rendered = std::fs::read_to_string(&out).expect("read derived config");
let parsed: toml::Value = toml::from_str(&rendered).expect("parse derived config");
assert_eq!(parsed["public_url"].as_str(), Some("https://new.example"));
assert_eq!(parsed["bind"].as_str(), Some("127.0.0.1:9090"));
assert_eq!(parsed["command_timeout_ms"].as_integer(), Some(999));
assert_eq!(parsed["enroll_token_ttl_seconds"].as_integer(), Some(5678));
assert_eq!(
parsed["state_file"].as_str(),
Some("/tmp/wakey-base/old.sqlite3")
);
assert_eq!(parsed["telemetry"]["json_logs"].as_bool(), Some(true));
assert_eq!(
parsed["telemetry"]["service_name"].as_str(),
Some("old-service")
);
let _ = std::fs::remove_dir_all(&dir);
}
}