use crate::errors::{SecParError, format_sdk_error};
use crate::opt::{GlobalOpts, SecCommand};
use crate::specs::SecretsSpec;
use crate::ui::{
build_secrets_table, confirm_action, confirm_delete, new_spinner, print_aborted,
print_env_context, print_info, print_success, print_value, select_from_list,
};
use aws_sdk_secretsmanager::Client;
use color_eyre::Result;
use tracing::debug;
use uuid::Uuid;
pub async fn list_secrets(client: &Client) -> Result<Vec<(String, String, String)>, SecParError> {
match client.list_secrets().send().await {
Ok(output) => {
let rows = output
.secret_list()
.iter()
.map(|s| {
let name = s.name().unwrap_or("unknown").to_owned();
let arn = s.arn().unwrap_or("").to_owned();
let last_changed = s
.last_changed_date()
.map(|d| d.to_string())
.unwrap_or_default();
(name, arn, last_changed)
})
.collect();
Ok(rows)
}
Err(e) => {
debug!("Error: {:?}", e.to_string());
Err(SecParError::AwsSdk(format_sdk_error(&e)))
}
}
}
pub async fn describe_secret(client: &Client, name: &str) -> Result<(), SecParError> {
match client.describe_secret().secret_id(name).send().await {
Ok(output) => {
println!("ℹ️ Secret details");
println!(" Name : {}", output.name().unwrap_or("-"));
println!(" ARN : {}", output.arn().unwrap_or("-"));
println!(" Description : {}", output.description().unwrap_or("-"));
println!(
" Last Changed : {}",
output
.last_changed_date()
.map(|d| d.to_string())
.as_deref()
.unwrap_or("-")
);
println!(
" Last Accessed : {}",
output
.last_accessed_date()
.map(|d| d.to_string())
.as_deref()
.unwrap_or("-")
);
println!(
" Rotation : {}",
if output.rotation_enabled().unwrap_or(false) {
"enabled"
} else {
"disabled"
}
);
Ok(())
}
Err(e) => {
debug!("Error: {:?}", e.to_string());
Err(SecParError::AwsSdk(format_sdk_error(&e)))
}
}
}
pub async fn delete_secret(client: &Client, name: &str, force: bool) -> Result<(), SecParError> {
match client
.delete_secret()
.secret_id(name)
.force_delete_without_recovery(force)
.send()
.await
{
Ok(_) => Ok(()),
Err(e) => {
debug!("Error: {:?}", e.to_string());
Err(SecParError::AwsSdk(format_sdk_error(&e)))
}
}
}
pub async fn retrieve_secret(client: &Client, name: &str) -> Result<String, SecParError> {
match client.get_secret_value().secret_id(name).send().await {
Ok(output) => match output.secret_string() {
Some(secret) => Ok(secret.to_owned()),
None => Err(SecParError::MissingValue(format!(
"secret value is empty for {name}"
))),
},
Err(e) => {
debug!("Error: {:?}", e.to_string());
Err(SecParError::AwsSdk(format_sdk_error(&e)))
}
}
}
pub async fn save_secret(client: &Client, name: &str, secret: &str) -> Result<String, SecParError> {
match client
.create_secret()
.name(name)
.secret_string(secret)
.client_request_token(Uuid::new_v4().to_string())
.send()
.await
{
Ok(output) => match output.arn() {
Some(arn) => Ok(arn.to_owned()),
None => Err(SecParError::MissingValue(format!(
"secret ARN is missing for {name}"
))),
},
Err(e) => {
debug!("Error: {:?}", e.to_string());
Err(SecParError::AwsSdk(format_sdk_error(&e)))
}
}
}
pub async fn apply_secrets(client: &Client, path: &std::path::Path) -> Result<(), SecParError> {
let spec = SecretsSpec::new(path)?;
let total = spec.secrets.len();
for (i, entry) in spec.secrets.iter().enumerate() {
match save_secret(client, &entry.name, &entry.secret).await {
Ok(arn) => {
print_info(&format!(
"[{}/{}] created '{}' → {arn}",
i + 1,
total,
entry.name
));
}
Err(SecParError::AwsSdk(ref msg))
if msg.to_lowercase().contains("already exists")
|| msg.to_lowercase().contains("resourceexistsexception") =>
{
print_info(&format!(
"[{}/{}] '{}' already exists — skipped",
i + 1,
total,
entry.name
));
}
Err(e) => return Err(e),
}
}
Ok(())
}
async fn resolve_secret_name(client: &Client, name: Option<&str>) -> Result<String, SecParError> {
if let Some(n) = name {
return Ok(n.to_owned());
}
let spinner = new_spinner("Fetching secrets for selection…");
let rows = list_secrets(client).await?;
spinner.finish_and_clear();
let names: Vec<String> = rows.into_iter().map(|(n, _, _)| n).collect();
select_from_list("Select a secret", &names)
}
pub async fn process_sec_command(command: &SecCommand, opts: &GlobalOpts) -> Result<()> {
let shared_config = crate::cli::load_shared_config(opts).await;
let client = Client::new(&shared_config);
match command {
SecCommand::List {} => {
let spinner = new_spinner("🔍 Listing secrets…");
let rows = list_secrets(&client).await?;
spinner.finish_and_clear();
if rows.is_empty() {
print_info("No secrets found.");
} else {
let count = rows.len();
let refs: Vec<(&str, &str, &str)> = rows
.iter()
.map(|(n, a, l)| (n.as_str(), a.as_str(), l.as_str()))
.collect();
println!("{}", build_secrets_table(&refs));
print_info(&format!("{count} secret(s) found."));
}
}
SecCommand::Get { name } => {
let resolved = resolve_secret_name(&client, name.as_deref()).await?;
let spinner = new_spinner(&format!("🔍 Retrieving secret '{resolved}'…"));
let secret = retrieve_secret(&client, &resolved).await?;
spinner.finish_and_clear();
print_value(&resolved, &secret);
}
SecCommand::Describe { name } => {
let resolved = resolve_secret_name(&client, name.as_deref()).await?;
let spinner = new_spinner(&format!("🔍 Describing secret '{resolved}'…"));
let result = describe_secret(&client, &resolved).await;
spinner.finish_and_clear();
result?;
}
SecCommand::Create { name, secret, yes } => {
if !yes {
print_env_context(&crate::cli::env_context(opts));
if !confirm_action(&format!("Create secret '{name}'?"))? {
print_aborted();
return Ok(());
}
}
let spinner = new_spinner(&format!("✨ Creating secret '{name}'…"));
let arn = save_secret(&client, name, secret).await?;
spinner.finish_and_clear();
print_success(&format!("Secret '{name}' created."));
print_info(&format!("ARN: {arn}"));
}
SecCommand::Delete { name, force } => {
let resolved = resolve_secret_name(&client, name.as_deref()).await?;
if !force {
print_env_context(&crate::cli::env_context(opts));
if !confirm_delete(&resolved)? {
print_aborted();
return Ok(());
}
}
let spinner = new_spinner(&format!("🗑️ Deleting secret '{resolved}'…"));
delete_secret(&client, &resolved, *force).await?;
spinner.finish_and_clear();
print_success(&format!(
"Secret '{resolved}' deleted{}.",
if *force { " (force)" } else { "" }
));
}
SecCommand::Apply { path, yes } => {
if !yes {
print_env_context(&crate::cli::env_context(opts));
if !confirm_action(&format!("Apply secrets from '{}'?", path.display()))? {
print_aborted();
return Ok(());
}
}
let spinner = new_spinner(&format!("📂 Applying secrets from '{}'…", path.display()));
spinner.finish_and_clear();
apply_secrets(&client, path).await?;
print_success("Secrets applied successfully.");
}
}
Ok(())
}