fix: SSH tunnel leak, cmd_bao injection, discovery cache, DNS async
- Store SSH tunnel child in static Mutex (was dropped immediately) - cmd_bao: use env(1) for VAULT_TOKEN instead of sh -c (no shell injection) - Cache API discovery across kube_apply documents (was per-doc roundtrip) - Replace blocking ToSocketAddrs with tokio::net::lookup_host - Remove double YAML->JSON->string->JSON serialization in kube_apply - ResultExt::ctx now preserves all SunbeamError variants
This commit is contained in:
16
src/error.rs
16
src/error.rs
@@ -190,6 +190,14 @@ impl<T, E: Into<SunbeamError>> ResultExt<T> for std::result::Result<T, E> {
|
|||||||
context: context.to_string(),
|
context: context.to_string(),
|
||||||
source,
|
source,
|
||||||
},
|
},
|
||||||
|
SunbeamError::Secrets(msg) => SunbeamError::Secrets(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Config(msg) => SunbeamError::Config(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Build(msg) => SunbeamError::Build(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Identity(msg) => SunbeamError::Identity(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::ExternalTool { tool, detail } => SunbeamError::ExternalTool {
|
||||||
|
tool,
|
||||||
|
detail: format!("{context}: {detail}"),
|
||||||
|
},
|
||||||
other => SunbeamError::Other(format!("{context}: {other}")),
|
other => SunbeamError::Other(format!("{context}: {other}")),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -212,6 +220,14 @@ impl<T, E: Into<SunbeamError>> ResultExt<T> for std::result::Result<T, E> {
|
|||||||
context,
|
context,
|
||||||
source,
|
source,
|
||||||
},
|
},
|
||||||
|
SunbeamError::Secrets(msg) => SunbeamError::Secrets(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Config(msg) => SunbeamError::Config(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Build(msg) => SunbeamError::Build(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::Identity(msg) => SunbeamError::Identity(format!("{context}: {msg}")),
|
||||||
|
SunbeamError::ExternalTool { tool, detail } => SunbeamError::ExternalTool {
|
||||||
|
tool,
|
||||||
|
detail: format!("{context}: {detail}"),
|
||||||
|
},
|
||||||
other => SunbeamError::Other(format!("{context}: {other}")),
|
other => SunbeamError::Other(format!("{context}: {other}")),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
57
src/kube.rs
57
src/kube.rs
@@ -9,12 +9,14 @@ use kube::{Client, Config};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::OnceLock;
|
use std::sync::{Mutex, OnceLock};
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
static CONTEXT: OnceLock<String> = OnceLock::new();
|
static CONTEXT: OnceLock<String> = OnceLock::new();
|
||||||
static SSH_HOST: OnceLock<String> = OnceLock::new();
|
static SSH_HOST: OnceLock<String> = OnceLock::new();
|
||||||
static KUBE_CLIENT: OnceCell<Client> = OnceCell::const_new();
|
static KUBE_CLIENT: OnceCell<Client> = OnceCell::const_new();
|
||||||
|
static SSH_TUNNEL: Mutex<Option<tokio::process::Child>> = Mutex::new(None);
|
||||||
|
static API_DISCOVERY: OnceCell<kube::discovery::Discovery> = OnceCell::const_new();
|
||||||
|
|
||||||
/// Set the active kubectl context and optional SSH host for production tunnel.
|
/// Set the active kubectl context and optional SSH host for production tunnel.
|
||||||
pub fn set_context(ctx: &str, ssh_host: &str) {
|
pub fn set_context(ctx: &str, ssh_host: &str) {
|
||||||
@@ -55,7 +57,7 @@ pub async fn ensure_tunnel() -> Result<()> {
|
|||||||
|
|
||||||
crate::output::ok(&format!("Opening SSH tunnel to {host}..."));
|
crate::output::ok(&format!("Opening SSH tunnel to {host}..."));
|
||||||
|
|
||||||
let _child = tokio::process::Command::new("ssh")
|
let child = tokio::process::Command::new("ssh")
|
||||||
.args([
|
.args([
|
||||||
"-p",
|
"-p",
|
||||||
"2222",
|
"2222",
|
||||||
@@ -73,6 +75,11 @@ pub async fn ensure_tunnel() -> Result<()> {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.ctx("Failed to spawn SSH tunnel")?;
|
.ctx("Failed to spawn SSH tunnel")?;
|
||||||
|
|
||||||
|
// Store child so it lives for the process lifetime (and can be killed on cleanup)
|
||||||
|
if let Ok(mut guard) = SSH_TUNNEL.lock() {
|
||||||
|
*guard = Some(child);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for tunnel to become available
|
// Wait for tunnel to become available
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
@@ -161,14 +168,8 @@ pub async fn kube_apply(manifest: &str) -> Result<()> {
|
|||||||
Api::all_with(client.clone(), &ar)
|
Api::all_with(client.clone(), &ar)
|
||||||
};
|
};
|
||||||
|
|
||||||
let patch: serde_json::Value = serde_json::from_str(
|
let patch: serde_json::Value =
|
||||||
&serde_json::to_string(
|
serde_yaml::from_str(doc).ctx("Failed to parse YAML to JSON value")?;
|
||||||
&serde_yaml::from_str::<serde_json::Value>(doc)
|
|
||||||
.ctx("Failed to parse YAML to JSON")?,
|
|
||||||
)
|
|
||||||
.ctx("Failed to serialize to JSON")?,
|
|
||||||
)
|
|
||||||
.ctx("Failed to parse JSON")?;
|
|
||||||
|
|
||||||
api.patch(name, &ssapply, &Patch::Apply(patch))
|
api.patch(name, &ssapply, &Patch::Apply(patch))
|
||||||
.await
|
.await
|
||||||
@@ -191,10 +192,14 @@ async fn resolve_api_resource(
|
|||||||
("", api_version) // core API group
|
("", api_version) // core API group
|
||||||
};
|
};
|
||||||
|
|
||||||
let disc = discovery::Discovery::new(client.clone())
|
let disc = API_DISCOVERY
|
||||||
|
.get_or_try_init(|| async {
|
||||||
|
discovery::Discovery::new(client.clone())
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
.ctx("API discovery failed")?;
|
.ctx("API discovery failed")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
for api_group in disc.groups() {
|
for api_group in disc.groups() {
|
||||||
if api_group.name() == group {
|
if api_group.name() == group {
|
||||||
@@ -516,11 +521,9 @@ pub async fn kustomize_build(overlay: &Path, domain: &str, email: &str) -> Resul
|
|||||||
|
|
||||||
/// Resolve the registry host IP for REGISTRY_HOST_IP substitution.
|
/// Resolve the registry host IP for REGISTRY_HOST_IP substitution.
|
||||||
async fn resolve_registry_ip(domain: &str) -> String {
|
async fn resolve_registry_ip(domain: &str) -> String {
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
|
|
||||||
// Try DNS for src.<domain>
|
// Try DNS for src.<domain>
|
||||||
let hostname = format!("src.{domain}:443");
|
let hostname = format!("src.{domain}:443");
|
||||||
if let Ok(mut addrs) = hostname.to_socket_addrs() {
|
if let Ok(mut addrs) = tokio::net::lookup_host(&hostname).await {
|
||||||
if let Some(addr) = addrs.next() {
|
if let Some(addr) = addrs.next() {
|
||||||
return addr.ip().to_string();
|
return addr.ip().to_string();
|
||||||
}
|
}
|
||||||
@@ -537,7 +540,7 @@ async fn resolve_registry_ip(domain: &str) -> String {
|
|||||||
.next()
|
.next()
|
||||||
.unwrap_or(&ssh_host);
|
.unwrap_or(&ssh_host);
|
||||||
let host_lookup = format!("{raw}:443");
|
let host_lookup = format!("{raw}:443");
|
||||||
if let Ok(mut addrs) = host_lookup.to_socket_addrs() {
|
if let Ok(mut addrs) = tokio::net::lookup_host(&host_lookup).await {
|
||||||
if let Some(addr) = addrs.next() {
|
if let Some(addr) = addrs.next() {
|
||||||
return addr.ip().to_string();
|
return addr.ip().to_string();
|
||||||
}
|
}
|
||||||
@@ -593,14 +596,26 @@ pub async fn cmd_bao(bao_args: &[String]) -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.ctx("root-token not found in openbao-keys secret")?;
|
.ctx("root-token not found in openbao-keys secret")?;
|
||||||
|
|
||||||
// Build the command string for sh -c
|
// Build the exec command using env to set VAULT_TOKEN without shell interpretation
|
||||||
let bao_arg_str = bao_args.join(" ");
|
let vault_token_env = format!("VAULT_TOKEN={root_token}");
|
||||||
let bao_cmd = format!("VAULT_TOKEN={root_token} bao {bao_arg_str}");
|
let mut kubectl_args = vec![
|
||||||
|
format!("--context={}", context()),
|
||||||
|
"-n".to_string(),
|
||||||
|
"data".to_string(),
|
||||||
|
"exec".to_string(),
|
||||||
|
ob_pod,
|
||||||
|
"-c".to_string(),
|
||||||
|
"openbao".to_string(),
|
||||||
|
"--".to_string(),
|
||||||
|
"env".to_string(),
|
||||||
|
vault_token_env,
|
||||||
|
"bao".to_string(),
|
||||||
|
];
|
||||||
|
kubectl_args.extend(bao_args.iter().cloned());
|
||||||
|
|
||||||
// Use kubectl for full TTY support
|
// Use kubectl for full TTY support
|
||||||
let status = tokio::process::Command::new("kubectl")
|
let status = tokio::process::Command::new("kubectl")
|
||||||
.arg(format!("--context={}", context()))
|
.args(&kubectl_args)
|
||||||
.args(["-n", "data", "exec", &ob_pod, "-c", "openbao", "--", "sh", "-c", &bao_cmd])
|
|
||||||
.stdin(Stdio::inherit())
|
.stdin(Stdio::inherit())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
|
|||||||
Reference in New Issue
Block a user