Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yml = "0.0.12"
toml = "0.8"
toml_edit = "0.22"
apollo-parser = "0.8.5"
tower-mcp-types = "0.12.0"
regex = "1"
Expand Down
6 changes: 6 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,12 @@ Driver implementation settings live in the TOML driver tables. See
`docs/reference/gateway-config.mdx` for worked per-driver examples and RFC
0003 for the full schema.

`openshell-gateway config set` updates a single resolved TOML file from
repeatable dotted `KEY=VALUE` assignments. It preserves comments, validates
the complete gateway schema, and replaces the file atomically. Local
development tasks can apply overrides to their generated configuration through
this command without changing gateway startup precedence.

`database_url` is env-only and rejected when present in the file
(`OPENSHELL_DB_URL` / `--db-url`).

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pin-project-lite = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tokio-stream = { workspace = true }
sqlx = { workspace = true }
reqwest = { workspace = true }
Expand Down
67 changes: 66 additions & 1 deletion crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct Cli {
enum Commands {
/// Generate mTLS PKI and write Kubernetes Secrets (Helm pre-install hook).
GenerateCerts(certgen::CertgenArgs),
/// Update the gateway TOML configuration.
Config(crate::config_command::ConfigArgs),
}

#[derive(clap::Args, Debug)]
Expand All @@ -49,7 +51,7 @@ struct RunArgs {
/// When set, gateway-wide settings and per-driver tables are read from
/// the file. Gateway command-line flags and `OPENSHELL_*` environment
/// variables continue to take precedence over gateway file values.
#[arg(long, env = "OPENSHELL_GATEWAY_CONFIG")]
#[arg(long, env = "OPENSHELL_GATEWAY_CONFIG", global = true)]
config: Option<PathBuf>,

/// IP address to bind the server, health, and metrics listeners to.
Expand Down Expand Up @@ -228,6 +230,7 @@ pub async fn run_cli() -> Result<()> {

match cli.command {
Some(Commands::GenerateCerts(args)) => certgen::run(args).await,
Some(Commands::Config(args)) => crate::config_command::run(args, cli.run.config),
None => Box::pin(run_from_args(cli.run, matches)).await,
}
}
Expand Down Expand Up @@ -1086,6 +1089,68 @@ mod tests {
));
}

#[test]
fn config_set_uses_explicit_config_path() {
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _guard = EnvVarGuard::remove("OPENSHELL_GATEWAY_CONFIG");
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("custom/gateway.toml");
let path_string = path.to_string_lossy().into_owned();

let cli = Cli::try_parse_from([
"openshell-gateway",
"config",
"set",
"--config",
&path_string,
"openshell.gateway.compute_drivers=[\"podman\"]",
"openshell.gateway.bind_address=0.0.0.0:17670",
])
.expect("config set should parse without runtime arguments");
let Cli { command, run } = cli;
let Some(super::Commands::Config(args)) = command else {
panic!("expected config subcommand");
};

crate::config_command::run(args, run.config).unwrap();

let loaded = crate::config_file::load(&path).unwrap();
assert_eq!(
loaded.openshell.gateway.compute_drivers,
Some(vec!["podman".to_string()])
);
assert_eq!(
loaded.openshell.gateway.bind_address,
Some("0.0.0.0:17670".parse().unwrap())
);
}

#[test]
fn config_set_requires_at_least_one_assignment() {
let error = Cli::try_parse_from(["openshell-gateway", "config", "set"])
.expect_err("config set without an assignment should fail");

assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}

#[test]
fn config_set_rejects_non_assignment_arguments() {
let error = Cli::try_parse_from([
"openshell-gateway",
"config",
"set",
"openshell.gateway.log_level",
])
.expect_err("config set arguments must use KEY=VALUE syntax");

assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
}

#[test]
fn bare_invocation_with_no_db_url_parses_for_runtime_defaults() {
// db_url is Option<String> at the clap level so subcommand parsing
Expand Down
Loading
Loading