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(),
|
||||
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}")),
|
||||
}
|
||||
})
|
||||
@@ -212,6 +220,14 @@ impl<T, E: Into<SunbeamError>> ResultExt<T> for std::result::Result<T, E> {
|
||||
context,
|
||||
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}")),
|
||||
}
|
||||
})
|
||||
|
||||
57
src/kube.rs
57
src/kube.rs
@@ -9,12 +9,14 @@ use kube::{Client, Config};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static CONTEXT: OnceLock<String> = OnceLock::new();
|
||||
static SSH_HOST: OnceLock<String> = OnceLock::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.
|
||||
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}..."));
|
||||
|
||||
let _child = tokio::process::Command::new("ssh")
|
||||
let child = tokio::process::Command::new("ssh")
|
||||
.args([
|
||||
"-p",
|
||||
"2222",
|
||||
@@ -73,6 +75,11 @@ pub async fn ensure_tunnel() -> Result<()> {
|
||||
.spawn()
|
||||
.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
|
||||
for _ in 0..20 {
|
||||
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)
|
||||
};
|
||||
|
||||
let patch: serde_json::Value = serde_json::from_str(
|
||||
&serde_json::to_string(
|
||||
&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")?;
|
||||
let patch: serde_json::Value =
|
||||
serde_yaml::from_str(doc).ctx("Failed to parse YAML to JSON value")?;
|
||||
|
||||
api.patch(name, &ssapply, &Patch::Apply(patch))
|
||||
.await
|
||||
@@ -191,10 +192,14 @@ async fn resolve_api_resource(
|
||||
("", 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()
|
||||
.await
|
||||
.ctx("API discovery failed")?;
|
||||
.ctx("API discovery failed")
|
||||
})
|
||||
.await?;
|
||||
|
||||
for api_group in disc.groups() {
|
||||
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.
|
||||
async fn resolve_registry_ip(domain: &str) -> String {
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
// Try DNS for src.<domain>
|
||||
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() {
|
||||
return addr.ip().to_string();
|
||||
}
|
||||
@@ -537,7 +540,7 @@ async fn resolve_registry_ip(domain: &str) -> String {
|
||||
.next()
|
||||
.unwrap_or(&ssh_host);
|
||||
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() {
|
||||
return addr.ip().to_string();
|
||||
}
|
||||
@@ -593,14 +596,26 @@ pub async fn cmd_bao(bao_args: &[String]) -> Result<()> {
|
||||
.await
|
||||
.ctx("root-token not found in openbao-keys secret")?;
|
||||
|
||||
// Build the command string for sh -c
|
||||
let bao_arg_str = bao_args.join(" ");
|
||||
let bao_cmd = format!("VAULT_TOKEN={root_token} bao {bao_arg_str}");
|
||||
// Build the exec command using env to set VAULT_TOKEN without shell interpretation
|
||||
let vault_token_env = format!("VAULT_TOKEN={root_token}");
|
||||
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
|
||||
let status = tokio::process::Command::new("kubectl")
|
||||
.arg(format!("--context={}", context()))
|
||||
.args(["-n", "data", "exec", &ob_pod, "-c", "openbao", "--", "sh", "-c", &bao_cmd])
|
||||
.args(&kubectl_args)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
|
||||
Reference in New Issue
Block a user