tsafe-cli 1.0.21

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
//! Non-core `tsafe ssm-pull` handler — import parameters from AWS SSM Parameter Store.

use anyhow::{Context as _, Result};
use colored::Colorize;
use tsafe_cli::tsafe_aws::{pull_ssm_parameters, AwsConfig, AwsCredentials, AwsError};
use tsafe_core::{audit::AuditEntry, events::emit_event};

use crate::helpers::*;
use tsafe_cli::cli::PullOnError;

pub(crate) fn cmd_ssm_pull(
    profile: &str,
    region: Option<&str>,
    path: Option<&str>,
    overwrite: bool,
    on_error: PullOnError,
) -> Result<()> {
    cmd_ssm_pull_ns(profile, region, path, overwrite, on_error, None)
}

/// Inner implementation that supports an optional namespace prefix (ADR-012).
///
/// When `ns` is `Some("prod")`, every parameter name returned from SSM is
/// stored as `prod.PARAM_NAME` in the local vault.
pub(crate) fn cmd_ssm_pull_ns(
    profile: &str,
    region: Option<&str>,
    path: Option<&str>,
    overwrite: bool,
    on_error: PullOnError,
    ns: Option<&str>,
) -> Result<()> {
    let cfg = match region {
        Some(r) => {
            let endpoint = format!("https://ssm.{r}.amazonaws.com");
            AwsConfig::with_endpoint(r, endpoint)
        }
        None => {
            let mut cfg = AwsConfig::from_env().with_context(|| {
                "AWS region is not configured\n\
                 \n  Fix:  export AWS_DEFAULT_REGION=us-east-1  (or pass --region)\
                 \n  Help: tsafe explain pull-auth"
            })?;
            // Override endpoint to SSM (default from_env builds Secrets Manager URL)
            cfg.endpoint = format!("https://ssm.{}.amazonaws.com", cfg.region);
            cfg
        }
    };

    let params = match pull_ssm_parameters(
        &cfg,
        &|| AwsCredentials::from_env_or_imds().map_err(|e| AwsError::Auth(format!("{e}"))),
        path,
    )
    .with_context(|| {
        "failed to pull parameters from AWS SSM Parameter Store\n\
         \n  Credential setup: tsafe explain pull-auth\
         \n  Required policy:  ssm:GetParametersByPath + kms:Decrypt (for SecureString)"
    }) {
        Ok(params) => params,
        Err(err) => match on_error {
            PullOnError::FailAll => return Err(err),
            PullOnError::SkipFailed | PullOnError::WarnOnly => {
                eprintln!("{} SSM pull failed: {err}", "!".yellow());
                return Ok(());
            }
        },
    };

    if params.is_empty() {
        println!(
            "{} No parameters found in SSM Parameter Store matching the path filter",
            "i".blue()
        );
        return Ok(());
    }

    let mut vault = open_vault(profile)?;
    let mut imported = 0usize;
    let mut skipped = 0usize;

    for (raw_key, value) in &params {
        // ADR-012: apply per-source namespace prefix when declared in the manifest.
        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, "ssm-pull", None))
        .ok();
    emit_event(profile, "ssm-pull", None);
    println!(
        "{} Imported {imported} parameter(s) from SSM Parameter Store (region: {}){}",
        "".green(),
        cfg.region,
        if skipped > 0 {
            format!(" ({skipped} skipped — use --overwrite to replace)")
        } else {
            String::new()
        }
    );
    Ok(())
}