use anyhow::{Context, Result};
use colored::Colorize;
use tsafe_azure::{acquire_token, pull_secrets, KvConfig};
use tsafe_core::{audit::AuditEntry, events::emit_event};
use crate::helpers::*;
use tsafe_cli::cli::PullOnError;
const AKV_TEST_LOCAL_URL_ENV: &str = "TSAFE_AKV_TEST_LOCAL_URL";
const AKV_TEST_TOKEN_ENV: &str = "TSAFE_AKV_TEST_TOKEN";
fn is_allowed_local_test_url(url: &str) -> bool {
url.starts_with("http://127.0.0.1:")
|| url.starts_with("http://localhost:")
|| url.starts_with("http://[::1]:")
}
fn local_test_override() -> Result<Option<(KvConfig, String)>> {
#[cfg(debug_assertions)]
{
if let Ok(raw_url) = std::env::var(AKV_TEST_LOCAL_URL_ENV) {
let url = raw_url.trim();
if !is_allowed_local_test_url(url) {
anyhow::bail!(
"{AKV_TEST_LOCAL_URL_ENV} must use http://127.0.0.1:<port>, http://localhost:<port>, or http://[::1]:<port>"
);
}
let token = std::env::var(AKV_TEST_TOKEN_ENV).with_context(|| {
format!("{AKV_TEST_TOKEN_ENV} must be set when {AKV_TEST_LOCAL_URL_ENV} is used")
})?;
return Ok(Some((
KvConfig {
vault_url: url.to_string(),
},
token,
)));
}
}
Ok(None)
}
pub(crate) fn cmd_kv_pull(
profile: &str,
prefix: Option<&str>,
overwrite: bool,
on_error: PullOnError,
) -> Result<()> {
cmd_kv_pull_ns(profile, prefix, overwrite, on_error, None)
}
pub(crate) fn cmd_kv_pull_ns(
profile: &str,
prefix: Option<&str>,
overwrite: bool,
on_error: PullOnError,
ns: Option<&str>,
) -> Result<()> {
let (cfg, test_token) = match local_test_override()? {
Some((cfg, token)) => (cfg, Some(token)),
None => (
KvConfig::from_env().context("Key Vault is not configured — set TSAFE_AKV_URL")?,
None,
),
};
let secrets = match pull_secrets(
&cfg,
&|| match &test_token {
Some(token) => Ok(token.clone()),
None => acquire_token(),
},
prefix,
)
.context("failed to pull secrets from Azure Key Vault")
{
Ok(secrets) => secrets,
Err(err) => match on_error {
PullOnError::FailAll => return Err(err),
PullOnError::SkipFailed | PullOnError::WarnOnly => {
eprintln!("{} Azure Key Vault pull failed: {err}", "!".yellow());
return Ok(());
}
},
};
if secrets.is_empty() {
println!(
"{} No secrets found in Key Vault matching the filter",
"i".blue()
);
return Ok(());
}
let mut vault = open_vault(profile)?;
let mut imported = 0usize;
let mut skipped = 0usize;
for (raw_key, value) in &secrets {
let key = match ns {
Some(prefix) => format!("{prefix}.{raw_key}"),
None => raw_key.clone(),
};
let exists = vault.list().contains(&key.as_str());
if exists && !overwrite {
skipped += 1;
continue;
}
vault.set(&key, value, std::collections::HashMap::new())?;
imported += 1;
}
audit(profile)
.append(&AuditEntry::success(profile, "kv-pull", None))
.ok();
emit_event(profile, "kv-pull", None);
println!(
"{} Imported {imported} secret(s) from Key Vault '{}'{}",
"✓".green(),
cfg.vault_url,
if skipped > 0 {
format!(" ({skipped} skipped — use --overwrite to replace)")
} else {
String::new()
}
);
Ok(())
}