use crate::pty::CommandExt as _;
use akv_cli::{cache::ClientCache, ErrorKind, Result};
use azure_security_keyvault_secrets::{ResourceId, SecretClient};
use clap::Parser;
use std::{
collections::HashMap,
env,
io::{BufRead, BufReader},
path::PathBuf,
process::{exit, Command},
sync::Arc,
};
use tokio::sync::Mutex;
use tracing::Level;
use crate::credential;
const MASK: &str = "<concealed by akv>";
#[derive(Debug, Parser)]
#[command(arg_required_else_help = true)]
pub struct Args {
#[arg(long, value_name = "PATH")]
env_file: Vec<PathBuf>,
#[arg(long)]
no_masking: bool,
#[arg(
allow_hyphen_values = true,
hide = true,
required = true,
trailing_var_arg = true
)]
args: Vec<String>,
}
impl Args {
#[tracing::instrument(level = Level::INFO, skip(self), err)]
pub async fn run(&self) -> Result<()> {
for path in &self.env_file {
dotenvy::from_path_override(path)?;
}
let credential = credential()?;
let cache = ClientCache::new();
let secrets = Arc::new(Mutex::new(HashMap::<String, String>::new()));
for (name, value) in env::vars_os() {
let Ok(value) = value.into_string() else {
continue;
};
let Ok(id) = value.parse::<ResourceId>() else {
continue;
};
tracing::debug!("replacing environment variable {name:?} from {value}");
let mut secrets = secrets.lock().await;
if let Some(secret) = secrets.get(&value) {
env::set_var(name, secret.as_str());
continue;
}
let client = cache
.get(&id.vault_url, |endpoint| {
SecretClient::new(endpoint, credential.clone(), None)
})
.await?;
let secret = client
.get_secret(&id.name, id.version.as_deref().unwrap_or_default(), None)
.await?
.into_body()
.await?;
tracing::debug!("retrieved {:?}", &secret);
let Some(secret) = secret.value else {
continue;
};
env::set_var(name, secret.as_str());
secrets.insert(value, secret);
}
let secrets: Vec<String> = secrets
.lock()
.await
.values()
.map(ToOwned::to_owned)
.collect();
if self.no_masking {
let mut args = self.args.iter();
let program = args.next().ok_or_else(|| {
akv_cli::Error::with_message(ErrorKind::InvalidData, "command required")
})?;
let mut cmd = Command::new(program);
let mut process = cmd.args(args).spawn()?;
if let Some(code) = process.wait()?.code() {
exit(code);
}
return Ok(());
}
let mut args = self.args.iter();
let program = args.next().ok_or_else(|| {
akv_cli::Error::with_message(ErrorKind::InvalidData, "command required")
})?;
let mut cmd = Command::new(program);
cmd.args(args);
let (mut process, pty) = cmd.spawn_pty()?;
let pipe = tokio::spawn({
let pty = pty.clone();
async move {
let reader = BufReader::new(pty);
let lines = reader.lines().fuse();
for line in lines {
let Ok(line) = line else {
continue;
};
let masked = mask_secrets(&line, &secrets);
println!("{masked}");
}
}
});
let status = process.wait();
drop(pty);
let _ = pipe.await;
if let Some(code) = status?.code() {
exit(code);
}
Ok(())
}
}
fn mask_secrets(line: &str, secrets: &Vec<String>) -> String {
let mut masked = line.to_string();
for secret in secrets {
masked = masked.replace(secret, MASK);
}
masked
}