diff --git a/Cargo.lock b/Cargo.lock index b412467610..a6364f025f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4220,6 +4220,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite 0.26.2", "toml", + "toml_edit", "tonic", "tower 0.5.3", "tower-http 0.6.8", diff --git a/Cargo.toml b/Cargo.toml index 7081fe97e2..3be8771afb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/architecture/gateway.md b/architecture/gateway.md index cca6599281..387ea5879b 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -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`). diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 4c4f289ed6..3143ec7b6f 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -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 } diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 269b9f40e9..6095059aaf 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -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)] @@ -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, /// IP address to bind the server, health, and metrics listeners to. @@ -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, } } @@ -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 at the clap level so subcommand parsing diff --git a/crates/openshell-server/src/config_command.rs b/crates/openshell-server/src/config_command.rs new file mode 100644 index 0000000000..328e762d32 --- /dev/null +++ b/crates/openshell-server/src/config_command.rs @@ -0,0 +1,402 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! CLI handlers for updating the gateway TOML configuration. + +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use clap::{Args, Subcommand}; +use miette::{IntoDiagnostic, Result, WrapErr}; +use tempfile::NamedTempFile; +use toml_edit::{DocumentMut, Item, Table, value}; + +use crate::{config_file, defaults}; + +#[derive(Debug, Args)] +pub struct ConfigArgs { + #[command(subcommand)] + command: ConfigCommand, +} + +#[derive(Debug, Subcommand)] +enum ConfigCommand { + /// Update fields in the gateway TOML configuration. + Set(SetArgs), +} + +#[derive(Debug, Args)] +struct SetArgs { + /// Dotted TOML key and value to set. May be repeated. + #[arg(required = true, value_name = "KEY=VALUE")] + assignments: Vec, +} + +#[derive(Clone, Debug)] +struct Assignment { + key: Vec, + value: Item, +} + +impl FromStr for Assignment { + type Err = String; + + fn from_str(input: &str) -> std::result::Result { + let (raw_key, raw_value) = input.split_once('=').ok_or_else(|| { + format!("invalid assignment '{input}': expected a dotted KEY=VALUE argument") + })?; + + let key = raw_key + .split('.') + .map(|component| { + if component.is_empty() + || !component + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-')) + { + return Err(format!( + "invalid config key '{raw_key}': use dot-separated TOML bare keys" + )); + } + Ok(component.to_string()) + }) + .collect::, _>>()?; + + Ok(Self { + key, + value: parse_assignment_value(raw_value.trim()), + }) + } +} + +pub fn run(args: ConfigArgs, explicit_path: Option) -> Result<()> { + match args.command { + ConfigCommand::Set(settings) => { + let path = explicit_path.map_or_else(defaults::default_gateway_config_path, Ok)?; + set(&path, &settings)?; + println!("updated gateway configuration: {}", path.display()); + println!("Restart the gateway service for changes to take effect."); + } + } + Ok(()) +} + +fn set(path: &Path, settings: &SetArgs) -> Result<()> { + let write_path = resolve_write_path(path)?; + let original = match fs::read_to_string(&write_path) { + Ok(contents) => contents, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(err) => { + return Err(err) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read gateway config '{}'", path.display())); + } + }; + + let mut document = if original.trim().is_empty() { + DocumentMut::new() + } else { + original + .parse::() + .into_diagnostic() + .wrap_err_with(|| format!("failed to parse gateway config '{}'", path.display()))? + }; + + let openshell = ensure_table(document.as_table_mut(), "openshell")?; + if !openshell.contains_key("version") { + openshell.insert("version", value(i64::from(config_file::SCHEMA_VERSION))); + } + + for assignment in &settings.assignments { + apply_assignment(&mut document, assignment)?; + } + + let rendered = document.to_string(); + config_file::parse(&rendered, path).map_err(|err| miette::miette!("{err}"))?; + write_atomically(&write_path, rendered.as_bytes()) +} + +fn resolve_write_path(path: &Path) -> Result { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.file_type().is_symlink() => { + fs::canonicalize(path).into_diagnostic().wrap_err_with(|| { + format!( + "failed to resolve gateway config symlink '{}'", + path.display() + ) + }) + } + Ok(_) => Ok(path.to_path_buf()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(path.to_path_buf()), + Err(err) => Err(err) + .into_diagnostic() + .wrap_err_with(|| format!("failed to inspect gateway config '{}'", path.display())), + } +} + +fn parse_assignment_value(raw: &str) -> Item { + let source = format!("value = {raw}"); + source + .parse::() + .ok() + .and_then(|mut document| document.as_table_mut().remove("value")) + .unwrap_or_else(|| value(raw)) +} + +fn apply_assignment(document: &mut DocumentMut, assignment: &Assignment) -> Result<()> { + let (key, parents) = assignment + .key + .split_last() + .ok_or_else(|| miette::miette!("config assignment key must not be empty"))?; + let mut table = document.as_table_mut(); + for parent in parents { + table = ensure_table(table, parent)?; + } + let existing_decor = table + .get(key) + .and_then(Item::as_value) + .map(|existing| existing.decor().clone()); + let mut replacement = assignment.value.clone(); + if let (Some(decor), Some(value)) = (existing_decor, replacement.as_value_mut()) { + *value.decor_mut() = decor; + } + table.insert(key, replacement); + Ok(()) +} + +fn ensure_table<'a>(parent: &'a mut Table, key: &str) -> Result<&'a mut Table> { + if !parent.contains_key(key) { + parent.insert(key, Item::Table(Table::new())); + } + parent + .get_mut(key) + .and_then(Item::as_table_mut) + .ok_or_else(|| miette::miette!("gateway config key '{key}' must be a TOML table")) +} + +fn write_atomically(path: &Path, contents: &[u8]) -> Result<()> { + let parent = path.parent().ok_or_else(|| { + miette::miette!( + "gateway config path '{}' has no parent directory", + path.display() + ) + })?; + let parent = if parent.as_os_str().is_empty() { + Path::new(".") + } else { + parent + }; + fs::create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create config directory '{}'", parent.display()))?; + + let permissions = fs::metadata(path) + .ok() + .map(|metadata| metadata.permissions()); + let mut temp = NamedTempFile::new_in(parent) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create temporary file in '{}'", parent.display()))?; + temp.write_all(contents) + .into_diagnostic() + .wrap_err("failed to write gateway configuration")?; + temp.as_file() + .sync_all() + .into_diagnostic() + .wrap_err("failed to sync gateway configuration")?; + if let Some(permissions) = permissions { + temp.as_file() + .set_permissions(permissions) + .into_diagnostic() + .wrap_err("failed to preserve gateway config permissions")?; + } + temp.persist(path) + .map_err(|err| err.error) + .into_diagnostic() + .wrap_err_with(|| format!("failed to replace gateway config '{}'", path.display()))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn settings(assignments: &[&str]) -> SetArgs { + SetArgs { + assignments: assignments + .iter() + .map(|assignment| assignment.parse().unwrap()) + .collect(), + } + } + + #[test] + fn set_creates_config_with_typed_values() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("openshell/gateway.toml"); + + set( + &path, + &settings(&[ + "openshell.gateway.compute_drivers=[\"podman\"]", + "openshell.gateway.bind_address=0.0.0.0:17670", + "openshell.gateway.log_level=debug", + "openshell.gateway.grpc_rate_limit_requests=42", + "openshell.gateway.enable_loopback_service_http=false", + "openshell.drivers.vm.vcpus=4", + ]), + ) + .unwrap(); + + let loaded = config_file::load(&path).unwrap(); + assert_eq!(loaded.openshell.version, Some(config_file::SCHEMA_VERSION)); + assert_eq!( + loaded.openshell.gateway.compute_drivers, + Some(vec!["podman".to_string()]) + ); + assert_eq!(loaded.openshell.gateway.log_level.as_deref(), Some("debug")); + assert_eq!(loaded.openshell.gateway.grpc_rate_limit_requests, Some(42)); + assert_eq!( + loaded.openshell.gateway.enable_loopback_service_http, + Some(false) + ); + assert_eq!( + loaded.openshell.drivers["vm"] + .get("vcpus") + .and_then(toml::Value::as_integer), + Some(4) + ); + } + + #[test] + fn set_preserves_comments_and_unrelated_settings() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("gateway.toml"); + fs::write( + &path, + "# keep this comment\n[openshell]\nversion = 1\n\n[openshell.gateway]\nlog_level = \"info\" # keep this inline comment\ncompute_drivers = [\"docker\"]\n", + ) + .unwrap(); + + set( + &path, + &settings(&[ + "openshell.gateway.log_level=debug", + "openshell.gateway.compute_drivers=[\"podman\"]", + ]), + ) + .unwrap(); + + let updated = fs::read_to_string(&path).unwrap(); + assert!(updated.contains("# keep this comment")); + assert!(updated.contains("log_level = \"debug\" # keep this inline comment")); + } + + #[cfg(unix)] + #[test] + fn set_updates_a_symlink_target_without_replacing_the_symlink() { + use std::os::unix::fs::symlink; + + let temp = tempfile::tempdir().unwrap(); + let target = temp.path().join("managed-gateway.toml"); + let path = temp.path().join("gateway.toml"); + fs::write( + &target, + "[openshell]\nversion = 1\n\n[openshell.gateway]\nlog_level = \"info\"\n", + ) + .unwrap(); + symlink("managed-gateway.toml", &path).unwrap(); + + set(&path, &settings(&["openshell.gateway.log_level=debug"])).unwrap(); + + assert!( + fs::symlink_metadata(&path) + .unwrap() + .file_type() + .is_symlink() + ); + let loaded = config_file::load(&target).unwrap(); + assert_eq!(loaded.openshell.gateway.log_level.as_deref(), Some("debug")); + assert_eq!( + fs::read_to_string(&path).unwrap(), + fs::read_to_string(&target).unwrap() + ); + } + + #[cfg(unix)] + #[test] + fn set_rejects_a_dangling_config_symlink() { + use std::os::unix::fs::symlink; + + let temp = tempfile::tempdir().unwrap(); + let target = temp.path().join("missing-gateway.toml"); + let path = temp.path().join("gateway.toml"); + symlink("missing-gateway.toml", &path).unwrap(); + + let error = set(&path, &settings(&["openshell.gateway.log_level=debug"])).unwrap_err(); + + assert!( + format!("{error:?}").contains("failed to resolve gateway config symlink"), + "unexpected error: {error:?}" + ); + assert!( + fs::symlink_metadata(&path) + .unwrap() + .file_type() + .is_symlink() + ); + assert!(!target.exists()); + } + + #[test] + fn later_assignment_to_the_same_key_wins() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("gateway.toml"); + + set( + &path, + &settings(&[ + "openshell.gateway.log_level=info", + "openshell.gateway.log_level=debug", + ]), + ) + .unwrap(); + + let loaded = config_file::load(&path).unwrap(); + assert_eq!(loaded.openshell.gateway.log_level.as_deref(), Some("debug")); + } + + #[test] + fn assignment_requires_key_value_syntax_and_bare_dotted_keys() { + let missing_value = "openshell.gateway.log_level" + .parse::() + .unwrap_err(); + assert!(missing_value.contains("KEY=VALUE")); + + let invalid_key = "openshell.gateway.bad key=value" + .parse::() + .unwrap_err(); + assert!(invalid_key.contains("dot-separated TOML bare keys")); + } + + #[test] + fn validation_failure_does_not_replace_the_config() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("gateway.toml"); + let original = "[openshell]\nversion = 1\n\n[openshell.gateway]\nlog_level = \"info\"\n"; + fs::write(&path, original).unwrap(); + + let error = set( + &path, + &settings(&[ + "openshell.gateway.log_level=debug", + "openshell.gateway.unknown_setting=value", + ]), + ) + .unwrap_err(); + + assert!(error.to_string().contains("unknown field")); + assert_eq!(fs::read_to_string(&path).unwrap(), original); + } +} diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index c4e0cbc959..8c7557f1d7 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -244,10 +244,14 @@ pub fn load(path: &Path) -> Result { path: path.to_path_buf(), source, })?; + parse(&contents, path) +} + +pub(crate) fn parse(contents: &str, path: &Path) -> Result { if contents.trim().is_empty() { return Ok(ConfigFile::default()); } - let file: ConfigFile = toml::from_str(&contents).map_err(|source| ConfigFileError::Parse { + let file: ConfigFile = toml::from_str(contents).map_err(|source| ConfigFileError::Parse { path: path.to_path_buf(), source, })?; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index dc00bc4561..4b8ef0da5e 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -27,6 +27,7 @@ mod auth; pub mod certgen; pub mod cli; mod compute; +mod config_command; pub mod config_file; mod defaults; mod grpc; diff --git a/deploy/man/openshell-gateway.8.md b/deploy/man/openshell-gateway.8.md index 2d584c4ba1..13064b09df 100644 --- a/deploy/man/openshell-gateway.8.md +++ b/deploy/man/openshell-gateway.8.md @@ -14,6 +14,8 @@ openshell-gateway - OpenShell gateway server daemon **openshell-gateway** \[*OPTIONS*\] +**openshell-gateway config set** \[**--config** *PATH*\] *KEY=VALUE*... + # DESCRIPTION **openshell-gateway** is the control-plane server for OpenShell. It @@ -32,6 +34,10 @@ TLS. # OPTIONS +**--config** *PATH* +: Read the gateway TOML configuration from *PATH*. Config subcommands + update this file. Environment: **OPENSHELL_GATEWAY_CONFIG**. + **--bind-address** *IP* : IP address to bind all listeners to. Default: **127.0.0.1**. Environment: **OPENSHELL_BIND_ADDRESS**. @@ -111,6 +117,15 @@ Compute driver settings such as sandbox image, callback endpoint, image pull policy, network name, VM state directory, and guest TLS material are configured in the TOML file passed with **--config**. +# CONFIGURATION COMMANDS + +**config set** *KEY=VALUE*... +: Update one or more dotted keys in the gateway TOML file. The command + preserves comments, validates the resulting schema, and replaces the + file atomically. Later assignments to the same key win. Use + **--config** or **OPENSHELL_GATEWAY_CONFIG** to select a non-default + file. + # SYSTEMD INTEGRATION The package installs a systemd user unit at diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 3dde7643d5..c2e4d46336 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -18,6 +18,37 @@ Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in defa `database_url` is env-only. The loader rejects it when it appears in the file. When `OPENSHELL_DB_URL` is unset, the gateway stores its SQLite database under `$XDG_STATE_HOME/openshell/gateway/openshell.db`. +## Update Configuration from the Command Line + +Use `openshell-gateway config set` with one or more dotted `KEY=VALUE` +assignments: + +```shell +openshell-gateway config set \ + 'openshell.gateway.compute_drivers=["podman"]' \ + openshell.gateway.bind_address=0.0.0.0:17670 +``` + +The command updates `$XDG_CONFIG_HOME/openshell/gateway.toml`, creating the +file and parent directory when needed. Pass `--config ` or set +`OPENSHELL_GATEWAY_CONFIG` to update another file. It preserves comments, +validates the resulting gateway schema, and replaces the file atomically. +Assignments accept TOML strings, integers, booleans, arrays, and tables; +unquoted values that are not valid TOML become strings. Later assignments to +the same key win. + +For the local Docker development gateway, pass assignments through the Mise +task. The task applies them after generating its normal configuration: + +```shell +mise run gateway:docker -- \ + --set openshell.drivers.docker.network_name=openshell-dev \ + --set openshell.drivers.docker.image_pull_policy=Never +``` + +Gateway CLI flags and `OPENSHELL_*` environment variables still take +precedence over values written to the file. + ## Package-Managed Locations Package-managed gateways do not require a TOML file. Create one at the package's optional config location when you need to override built-in defaults. Set `OPENSHELL_GATEWAY_CONFIG` in the launch environment to use a different file. diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index 0c71489697..0aca3fcd30 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -16,6 +16,7 @@ # OPENSHELL_DOCKER_GATEWAY_NAME=my-docker-gateway mise run gateway:docker # OPENSHELL_SANDBOX_NAMESPACE=my-ns mise run gateway:docker # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:docker +# mise run gateway:docker -- --set openshell.drivers.docker.network_name=my-network # # After the gateway is running, point the CLI at it with either: # openshell --gateway docker-dev @@ -32,6 +33,54 @@ SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/san SANDBOX_IMAGE_PULL_POLICY="${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-IfNotPresent}" LOG_LEVEL="${OPENSHELL_LOG_LEVEL:-info}" GATEWAY_BIN="${ROOT}/target/debug/openshell-gateway" +CONFIG_OVERRIDES=() + +usage() { + cat <<'EOF' +Usage: mise run gateway:docker -- [--set KEY=VALUE]... + +Start a local OpenShell gateway backed by Docker. + +Options: + --set KEY=VALUE Override a dotted key in the generated gateway TOML. + Repeat to set multiple values; later values win. + -h, --help Show this help. + +Example: + mise run gateway:docker -- \ + --set openshell.drivers.docker.network_name=openshell-dev \ + --set openshell.drivers.docker.image_pull_policy=Never +EOF +} + +while (( $# > 0 )); do + case "$1" in + --set) + if (( $# < 2 )); then + echo "ERROR: --set requires a KEY=VALUE argument" >&2 + exit 2 + fi + CONFIG_OVERRIDES+=("$2") + shift 2 + ;; + --set=*) + CONFIG_OVERRIDES+=("${1#--set=}") + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + ;; + *) + echo "ERROR: unknown argument '$1'" >&2 + usage >&2 + exit 2 + ;; + esac +done normalize_arch() { case "$1" in @@ -191,6 +240,13 @@ grpc_endpoint = "${GRPC_ENDPOINT}" supervisor_bin = "${SUPERVISOR_BIN}" EOF +if (( ${#CONFIG_OVERRIDES[@]} > 0 )); then + echo "Applying gateway configuration overrides..." + "${GATEWAY_BIN}" config set \ + --config "${CONFIG_PATH}" \ + "${CONFIG_OVERRIDES[@]}" >/dev/null +fi + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}"