diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e85a58..cb60a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changes +## [0.20.3] - 2026-06-01 + +### Fixed + +- **`--env-override` upstream resolution with split host/port keys** — when + separate `DB_HOST={host}` and `DB_PORT={port}` overrides are used, the proxy + upstream was assembled from only the first key's current value, producing a + portless address (e.g. `prod-db.example.com` instead of + `prod-db.example.com:5432`) which caused the proxy to fail to connect. + `resolve_upstream` now reads both keys and combines them into `host:port`. + ## [0.20.2] - 2026-06-01 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 7f34fe0..f563aa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3779,7 +3779,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fault-cli" -version = "0.20.2" +version = "0.20.3" dependencies = [ "anyhow", "arc-swap", @@ -3895,7 +3895,7 @@ dependencies = [ [[package]] name = "fault-ebpf-programs" -version = "0.20.2" +version = "0.20.3" dependencies = [ "aya-ebpf", "aya-log-ebpf", diff --git a/Cargo.toml b/Cargo.toml index 52cc580..304e050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["fault-cli", "fault-ebpf-programs"] default-members = ["fault-cli"] [workspace.package] -version = "0.20.2" +version = "0.20.3" edition = "2024" rust-version = "1.85" license-file = "LICENSE" diff --git a/fault-cli/src/inject/k8s/env_override.rs b/fault-cli/src/inject/k8s/env_override.rs index 9c1ee13..73ad673 100644 --- a/fault-cli/src/inject/k8s/env_override.rs +++ b/fault-cli/src/inject/k8s/env_override.rs @@ -170,25 +170,99 @@ pub fn parse_env_overrides(raw: &[String]) -> Result> { // Upstream resolution // --------------------------------------------------------------------------- -/// Read the **current** value of the first override's key from the named -/// resource. This is used in standalone proxy mode to discover the real -/// upstream address (e.g. the current `DB_HOST` value) before we overwrite it. +/// Resolve the real upstream address (`host:port`) from the current values of +/// the overridden keys, before we overwrite them. /// -/// Returns `None` if the key doesn't exist or the resource has no relevant -/// data (e.g. a Deployment container that doesn't carry the key at all). -pub async fn resolve_current_value( +/// Strategy: +/// - If any override template contains `{host}`, read that key's current value +/// as the upstream host. +/// - If any override template contains `{port}`, read that key's current value +/// as the upstream port. +/// - Combine as `host:port`. +/// +/// If no template contains `{port}` (e.g. only `{host}` is used, or it's a +/// full-address template like `DATABASE_URL={host}:{port}`), we try to extract +/// the port from the full-address key's current value, or leave it out and let +/// the proxy default. +/// +/// Falls back to reading the first override's current value verbatim when no +/// templates are present (literal overrides — the user is responsible for +/// knowing the upstream). +pub async fn resolve_upstream( client: Client, ns: &str, + overrides: &[EnvOverride], +) -> Result { + // Find the override whose template carries {host} and the one with {port} + let host_ov = overrides.iter().find(|ov| match &ov.value { + EnvOverrideValue::Template(t) => t.contains("{host}"), + _ => false, + }); + let port_ov = overrides.iter().find(|ov| match &ov.value { + EnvOverrideValue::Template(t) => t.contains("{port}"), + _ => false, + }); + + match (host_ov, port_ov) { + (Some(h), Some(p)) => { + // Separate host and port keys — read both and combine. + let host = read_current_value(&client, ns, h) + .await? + .ok_or_else(|| anyhow::anyhow!( + "Key '{}' not found in {}/{} — cannot determine upstream host.", + h.key, h.kind.as_str(), h.name + ))?; + let port = read_current_value(&client, ns, p) + .await? + .ok_or_else(|| anyhow::anyhow!( + "Key '{}' not found in {}/{} — cannot determine upstream port.", + p.key, p.kind.as_str(), p.name + ))?; + Ok(format!("{}:{}", host.trim(), port.trim())) + } + (Some(h), None) => { + // Only {host} — current value may already be "host:port" or + // just a hostname; use it as-is. + let val = read_current_value(&client, ns, h) + .await? + .ok_or_else(|| anyhow::anyhow!( + "Key '{}' not found in {}/{} — cannot determine upstream.", + h.key, h.kind.as_str(), h.name + ))?; + Ok(val.trim().to_string()) + } + _ => { + // No templates, or templates without {host}/{port} (e.g. literal + // full-URL override). Read the first override's current value + // verbatim — the caller must ensure it is a usable address. + let first = overrides.first().ok_or_else(|| { + anyhow::anyhow!("--env-override list is empty") + })?; + let val = read_current_value(&client, ns, first) + .await? + .ok_or_else(|| anyhow::anyhow!( + "Key '{}' not found in {}/{} — cannot determine upstream.", + first.key, first.kind.as_str(), first.name + ))?; + Ok(val.trim().to_string()) + } + } +} + +/// Read the current value of a single override key from the cluster. +async fn read_current_value( + client: &Client, + ns: &str, ov: &EnvOverride, ) -> Result> { match &ov.kind { WorkloadKind::ConfigMap => { - let api: Api = Api::namespaced(client, ns); + let api: Api = Api::namespaced(client.clone(), ns); let cm = api.get(&ov.name).await?; Ok(cm.data.and_then(|d| d.get(&ov.key).cloned())) } WorkloadKind::Deployment => { - let api: Api = Api::namespaced(client, ns); + let api: Api = Api::namespaced(client.clone(), ns); let d = api.get(&ov.name).await?; let containers = d .spec @@ -210,7 +284,7 @@ pub async fn resolve_current_value( Ok(None) } WorkloadKind::StatefulSet => { - let api: Api = Api::namespaced(client, ns); + let api: Api = Api::namespaced(client.clone(), ns); let ss = api.get(&ov.name).await?; let containers = ss .spec diff --git a/fault-cli/src/main.rs b/fault-cli/src/main.rs index 5f72fbd..cfc1ead 100644 --- a/fault-cli/src/main.rs +++ b/fault-cli/src/main.rs @@ -912,21 +912,16 @@ async fn main() -> Result<()> { if !env_overrides.is_empty() { // --env-override present → standalone outbound proxy mode. // - // The real upstream is the current value of the first - // overridden key, read from the cluster before we patch it. - let first = &env_overrides[0]; - let upstream = - inject::k8s::env_override::resolve_current_value( - kube::Client::try_default().await?, - &cfg.ns, - first, - ) - .await? - .ok_or_else(|| anyhow::anyhow!( - "Key '{}' not found in {}/{} — cannot determine \ - the real upstream for the standalone proxy.", - first.key, first.kind.as_str(), first.name, - ))?; + // Resolve the real upstream host:port from the current + // values of the overridden keys before we patch them. + // Handles split host/port keys by reading both and + // combining. + let upstream = inject::k8s::env_override::resolve_upstream( + kube::Client::try_default().await?, + &cfg.ns, + &env_overrides, + ) + .await?; // Proxy name: --name if given, otherwise // "fault-proxy-"