use anyhow::Result; use serde_json::Value; use sqlx::PgPool; use std::collections::HashMap; use crate::commands::search::build_env_map; use crate::output::OutputMode; pub struct InjectArgs<'a> { pub namespace: Option<&'a str>, pub kind: Option<&'a str>, pub name: Option<&'a str>, pub tags: &'a [String], /// Prefix to prepend to every variable name. Empty string means no prefix. pub prefix: &'a str, pub output: OutputMode, } pub struct RunArgs<'a> { pub namespace: Option<&'a str>, pub kind: Option<&'a str>, pub name: Option<&'a str>, pub tags: &'a [String], pub prefix: &'a str, /// The command and its arguments to execute with injected secrets. pub command: &'a [String], } /// Fetch secrets matching the filter and build a flat env map. /// Metadata and secret fields are merged; naming: `_` (uppercased). pub async fn collect_env_map( pool: &PgPool, namespace: Option<&str>, kind: Option<&str>, name: Option<&str>, tags: &[String], prefix: &str, master_key: &[u8; 32], ) -> Result> { if namespace.is_none() && kind.is_none() && name.is_none() && tags.is_empty() { anyhow::bail!( "At least one filter (--namespace, --kind, --name, or --tag) is required for inject/run" ); } let rows = crate::commands::search::fetch_rows(pool, namespace, kind, name, tags, None).await?; if rows.is_empty() { anyhow::bail!("No records matched the given filters."); } let mut map = HashMap::new(); for row in &rows { let row_map = build_env_map(row, prefix, Some(master_key))?; for (k, v) in row_map { map.insert(k, v); } } Ok(map) } /// `inject` command: print env vars to stdout (suitable for `eval $(...)` or export). pub async fn run_inject(pool: &PgPool, args: InjectArgs<'_>, master_key: &[u8; 32]) -> Result<()> { let env_map = collect_env_map( pool, args.namespace, args.kind, args.name, args.tags, args.prefix, master_key, ) .await?; match args.output { OutputMode::Json => { let obj: serde_json::Map = env_map .into_iter() .map(|(k, v)| (k, Value::String(v))) .collect(); println!("{}", serde_json::to_string_pretty(&Value::Object(obj))?); } OutputMode::JsonCompact => { let obj: serde_json::Map = env_map .into_iter() .map(|(k, v)| (k, Value::String(v))) .collect(); println!("{}", serde_json::to_string(&Value::Object(obj))?); } _ => { // Shell-safe KEY=VALUE output, one per line. let mut pairs: Vec<(String, String)> = env_map.into_iter().collect(); pairs.sort_by(|a, b| a.0.cmp(&b.0)); for (k, v) in pairs { println!("{}={}", k, shell_quote(&v)); } } } Ok(()) } /// `run` command: inject secrets into a child process environment and execute. pub async fn run_exec(pool: &PgPool, args: RunArgs<'_>, master_key: &[u8; 32]) -> Result<()> { if args.command.is_empty() { anyhow::bail!( "No command specified. Usage: secrets run [filter flags] -- [args]" ); } let env_map = collect_env_map( pool, args.namespace, args.kind, args.name, args.tags, args.prefix, master_key, ) .await?; tracing::debug!( vars = env_map.len(), cmd = args.command[0].as_str(), "injecting secrets into child process" ); let status = std::process::Command::new(&args.command[0]) .args(&args.command[1..]) .envs(&env_map) .status() .map_err(|e| anyhow::anyhow!("Failed to execute '{}': {}", args.command[0], e))?; if !status.success() { let code = status.code().unwrap_or(1); std::process::exit(code); } Ok(()) } /// Quote a value for safe shell output. Wraps the value in single quotes, /// escaping any single quotes within the value. fn shell_quote(s: &str) -> String { format!("'{}'", s.replace('\'', "'\\''")) }