Add safe configuration commands
This commit is contained in:
parent
c20cc47914
commit
443602a30b
7 changed files with 432 additions and 3 deletions
|
|
@ -2,7 +2,7 @@ use anyhow::{Context, Result, bail};
|
|||
use base64::Engine;
|
||||
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
||||
use clap_complete::{Shell, generate};
|
||||
use geth_config::GethPaths;
|
||||
use geth_config::{ConfigKey, GethConfig, GethPaths};
|
||||
use geth_control::{ControlRequest, ControlResponse, SyncStreamStatus};
|
||||
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
||||
use std::io::{Read, Write, stdout};
|
||||
|
|
@ -422,6 +422,11 @@ pub enum Command {
|
|||
#[command(subcommand)]
|
||||
command: DaemonCommand,
|
||||
},
|
||||
/// Inspect, validate, and safely edit daemon configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
command: ConfigCommand,
|
||||
},
|
||||
/// Show daemon, storage, Iroh, and backend health
|
||||
Status,
|
||||
/// Inspect or trigger peer synchronization
|
||||
|
|
@ -526,6 +531,48 @@ pub enum GuideTopic {
|
|||
SmokeTest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ConfigCommand {
|
||||
/// Print the selected config.toml path
|
||||
Path,
|
||||
/// Print the selected config and its effective values
|
||||
Show,
|
||||
/// Parse and validate the complete selected config
|
||||
Validate,
|
||||
/// Safely update one supported setting and validate the result
|
||||
Set {
|
||||
#[arg(value_enum)]
|
||||
key: ConfigKeyArg,
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub enum ConfigKeyArg {
|
||||
#[value(name = "iroh.relay_mode")]
|
||||
IrohRelayMode,
|
||||
#[value(name = "iroh.relay_map")]
|
||||
IrohRelayMap,
|
||||
#[value(name = "iroh.local_discovery")]
|
||||
IrohLocalDiscovery,
|
||||
#[value(name = "sync.live_sync_enabled")]
|
||||
SyncLiveSyncEnabled,
|
||||
#[value(name = "sync.live_sync_interval_ms")]
|
||||
SyncLiveSyncIntervalMs,
|
||||
}
|
||||
|
||||
impl From<ConfigKeyArg> for ConfigKey {
|
||||
fn from(value: ConfigKeyArg) -> Self {
|
||||
match value {
|
||||
ConfigKeyArg::IrohRelayMode => Self::IrohRelayMode,
|
||||
ConfigKeyArg::IrohRelayMap => Self::IrohRelayMap,
|
||||
ConfigKeyArg::IrohLocalDiscovery => Self::IrohLocalDiscovery,
|
||||
ConfigKeyArg::SyncLiveSyncEnabled => Self::SyncLiveSyncEnabled,
|
||||
ConfigKeyArg::SyncLiveSyncIntervalMs => Self::SyncLiveSyncIntervalMs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(after_long_help = DAEMON_AFTER_HELP)]
|
||||
pub enum DaemonCommand {
|
||||
|
|
@ -1541,6 +1588,8 @@ fn argument_help(path: &str, id: &str) -> Option<&'static str> {
|
|||
("geth cas conflict resolve", "resolution") => {
|
||||
Some("Resolution: keep-local, accept-remote, keep-both, or manual")
|
||||
}
|
||||
("geth config set", "key") => Some("Supported dotted configuration key"),
|
||||
("geth config set", "value") => Some("New value for the selected setting"),
|
||||
("geth node enroll request", "capabilities") => {
|
||||
Some("Requested RESOURCE=CAPABILITY pair; repeat to request more than one")
|
||||
}
|
||||
|
|
@ -1678,6 +1727,9 @@ async fn run_inner(cli: Cli) -> Result<()> {
|
|||
Command::Guide { topic } => {
|
||||
print_guide(topic, cli.json || cli.jsonl)?;
|
||||
}
|
||||
Command::Config { command } => {
|
||||
run_config_command(&paths, command, cli.json || cli.jsonl)?;
|
||||
}
|
||||
Command::Init {
|
||||
admin_key,
|
||||
signing_key,
|
||||
|
|
@ -1840,6 +1892,124 @@ async fn run_inner(cli: Cli) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn run_config_command(paths: &GethPaths, command: ConfigCommand, json: bool) -> Result<()> {
|
||||
let path = paths.config_file();
|
||||
match command {
|
||||
ConfigCommand::Path => {
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"type": "config-path",
|
||||
"path": path,
|
||||
"exists": path.exists(),
|
||||
}))?
|
||||
);
|
||||
} else {
|
||||
println!("{}", path.display());
|
||||
}
|
||||
}
|
||||
ConfigCommand::Show => {
|
||||
let exists = path.exists();
|
||||
let text = if exists {
|
||||
std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read config at {}", path.display()))?
|
||||
} else {
|
||||
GethConfig::default_toml().to_owned()
|
||||
};
|
||||
let config = GethConfig::parse(&text)
|
||||
.with_context(|| format!("validate config at {}", path.display()))?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"type": "config",
|
||||
"path": path,
|
||||
"source": if exists { "file" } else { "built-in-defaults" },
|
||||
"raw": text,
|
||||
"effective": config_json(&config),
|
||||
}))?
|
||||
);
|
||||
} else {
|
||||
println!("# path: {}", path.display());
|
||||
if !exists {
|
||||
println!("# source: built-in defaults (file does not exist yet)");
|
||||
}
|
||||
print!("{text}");
|
||||
if !text.ends_with('\n') {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Validate => {
|
||||
let exists = path.exists();
|
||||
let config = GethConfig::load(&path)
|
||||
.with_context(|| format!("validate config at {}", path.display()))?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"type": "config-validation",
|
||||
"path": path,
|
||||
"valid": true,
|
||||
"source": if exists { "file" } else { "built-in-defaults" },
|
||||
"effective": config_json(&config),
|
||||
}))?
|
||||
);
|
||||
} else if exists {
|
||||
println!("valid config: {}", path.display());
|
||||
} else {
|
||||
println!(
|
||||
"valid built-in defaults; config file does not exist yet: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
ConfigCommand::Set { key, value } => {
|
||||
let key = ConfigKey::from(key);
|
||||
let config = GethConfig::set(&path, key, &value)
|
||||
.with_context(|| format!("set {} in {}", key.as_str(), path.display()))?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"type": "config-updated",
|
||||
"path": path,
|
||||
"key": key.as_str(),
|
||||
"value": value,
|
||||
"restart_required": true,
|
||||
"effective": config_json(&config),
|
||||
}))?
|
||||
);
|
||||
} else {
|
||||
println!("updated {} in {}", key.as_str(), path.display());
|
||||
println!("restart the daemon for the change to take effect");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_json(config: &GethConfig) -> serde_json::Value {
|
||||
let relay_maps = config
|
||||
.iroh
|
||||
.relay_maps
|
||||
.iter()
|
||||
.map(|(name, map)| (name.clone(), map.urls.clone()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
serde_json::json!({
|
||||
"iroh": {
|
||||
"relay_mode": config.iroh.relay_mode.label(),
|
||||
"relay_maps": relay_maps,
|
||||
"local_discovery": config.iroh.local_discovery,
|
||||
},
|
||||
"sync": {
|
||||
"live_sync_enabled": config.sync.live_sync_enabled,
|
||||
"live_sync_interval_ms": config.sync.live_sync_interval_ms,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_ephemeral_daemon(json: bool) -> Result<()> {
|
||||
let (home, paths, node) = create_ephemeral_node()?;
|
||||
if json {
|
||||
|
|
@ -2724,6 +2894,7 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
},
|
||||
},
|
||||
Command::Guide { .. }
|
||||
| Command::Config { .. }
|
||||
| Command::Init { .. }
|
||||
| Command::Daemon { .. }
|
||||
| Command::Backup { .. }
|
||||
|
|
@ -4853,6 +5024,31 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_commands_parse_and_update_an_isolated_home() {
|
||||
for command in ["path", "show", "validate"] {
|
||||
let parsed = Cli::try_parse_from(["geth", "config", command]);
|
||||
assert!(parsed.is_ok(), "config {command} should parse: {parsed:?}");
|
||||
}
|
||||
let parsed =
|
||||
Cli::try_parse_from(["geth", "config", "set", "sync.live_sync_interval_ms", "500"]);
|
||||
assert!(parsed.is_ok(), "config set should parse: {parsed:?}");
|
||||
|
||||
let home = tempfile::tempdir().expect("temporary geth home");
|
||||
let paths = GethPaths::from_home(home.path());
|
||||
run_config_command(
|
||||
&paths,
|
||||
ConfigCommand::Set {
|
||||
key: ConfigKeyArg::SyncLiveSyncIntervalMs,
|
||||
value: "500".to_owned(),
|
||||
},
|
||||
false,
|
||||
)
|
||||
.expect("set config");
|
||||
let config = GethConfig::load(&paths.config_file()).expect("load config");
|
||||
assert_eq!(config.sync.live_sync_interval_ms, 500);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ephemeral_daemon_rejects_an_explicit_persistent_home() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
|
|
|
|||
Loading…
Reference in a new issue