use dialoguer::{Input, Select};
use crate::server::config::SecretsConfig;
use crate::server::error::AppError;
use super::SecretBackend;
#[allow(clippy::vec_init_then_push, unused_variables)]
pub async fn prompt_secrets_backend(
default_secret_name: &str,
default_keyring_service: &str,
) -> Result<SecretsConfig, AppError> {
#[allow(unused_mut)]
let mut backends: Vec<&str> = Vec::new();
#[cfg(feature = "keyring")]
match super::ensure_keyring() {
Ok(()) => backends.push("OS Keyring (default)"),
Err(e) => {
eprintln!();
eprintln!(" The OS keyring is not offered: {e}");
eprintln!();
}
}
#[cfg(feature = "aws-secrets")]
backends.push("AWS Secrets Manager");
#[cfg(feature = "gcp-secrets")]
backends.push("GCP Secret Manager");
#[cfg(feature = "azure-secrets")]
backends.push("Azure Key Vault");
#[cfg(feature = "vault-secrets")]
backends.push("HashiCorp Vault");
#[cfg(feature = "k8s-secrets")]
backends.push("Kubernetes Secret");
if backends.is_empty() {
return Err(super::no_secure_store(
"no secure secrets backend is compiled into this binary and usable on this host",
));
}
let chosen = if backends.len() == 1 {
eprintln!(" Using {} for secrets storage.", backends[0]);
backends[0]
} else {
let idx = Select::new()
.with_prompt("Secrets storage backend")
.items(&backends)
.default(0)
.interact()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
backends[idx]
};
let mut secrets_config = SecretsConfig::default();
match chosen {
#[cfg(feature = "aws-secrets")]
s if s.starts_with("AWS") => {
let region: String = Input::new()
.with_prompt("AWS region (leave empty for default)")
.default(String::new())
.allow_empty(true)
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let region_opt = if region.is_empty() {
None
} else {
Some(region)
};
let name = pick_or_input_name(
"AWS",
default_secret_name,
vti_secrets::discovery::list_aws_secrets(region_opt.as_deref())
.await
.map_err(super::from_vti),
)?;
secrets_config.backend = Some(SecretBackend::Aws);
secrets_config.aws_secret_name = Some(name);
secrets_config.aws_region = region_opt;
}
#[cfg(feature = "gcp-secrets")]
s if s.starts_with("GCP") => {
let project: String = Input::new()
.with_prompt("GCP project ID")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let name = pick_or_input_name(
"GCP",
default_secret_name,
vti_secrets::discovery::list_gcp_secrets(&project)
.await
.map_err(super::from_vti),
)?;
secrets_config.backend = Some(SecretBackend::Gcp);
secrets_config.gcp_project = Some(project);
secrets_config.gcp_secret_name = Some(name);
}
#[cfg(feature = "azure-secrets")]
s if s.starts_with("Azure") => {
let vault_url: String = Input::new()
.with_prompt("Azure Key Vault URL (e.g. https://my-vault.vault.azure.net/)")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let name = pick_or_input_name(
"Azure Key Vault",
default_secret_name,
vti_secrets::discovery::list_azure_secrets(&vault_url)
.await
.map_err(super::from_vti),
)?;
secrets_config.backend = Some(SecretBackend::Azure);
secrets_config.azure_vault_url = Some(vault_url);
secrets_config.azure_secret_name = Some(name);
}
#[cfg(feature = "vault-secrets")]
s if s.starts_with("HashiCorp") => {
let addr: String = Input::new()
.with_prompt("Vault address (e.g. https://vault.example.com:8200)")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let secret_path: String = Input::new()
.with_prompt("KV v2 secret path (e.g. webvh/server-secrets)")
.default(default_secret_name.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let kv_mount: String = Input::new()
.with_prompt("KV v2 mount path")
.default("secret".to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let auth_methods = ["kubernetes", "token", "approle"];
let auth_idx = Select::new()
.with_prompt("Vault auth method")
.items(auth_methods.as_slice())
.default(0)
.interact()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let auth_method = auth_methods[auth_idx];
match auth_method {
"kubernetes" => {
let role: String = Input::new()
.with_prompt("Vault Kubernetes auth role")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
secrets_config.vault_k8s_role = Some(role);
}
"token" => {
let token: String = Input::new()
.with_prompt("Vault token (leave empty to use the VAULT_TOKEN env var)")
.default(String::new())
.allow_empty(true)
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
if !token.is_empty() {
secrets_config.vault_token = Some(token);
}
}
_ => {
let role_id: String = Input::new()
.with_prompt("AppRole role_id")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let secret_id: String = Input::new()
.with_prompt("AppRole secret_id")
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
secrets_config.vault_approle_role_id = Some(role_id);
secrets_config.vault_approle_secret_id = Some(secret_id);
}
}
secrets_config.backend = Some(SecretBackend::Vault);
secrets_config.vault_addr = Some(addr);
secrets_config.vault_secret_path = Some(secret_path);
secrets_config.vault_kv_mount = kv_mount;
secrets_config.vault_auth_method = auth_method.to_string();
}
#[cfg(feature = "k8s-secrets")]
s if s.starts_with("Kubernetes") => {
let name: String = Input::new()
.with_prompt("Kubernetes Secret name")
.default(default_secret_name.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
let namespace: String = Input::new()
.with_prompt("Kubernetes namespace (leave empty for the pod's default namespace)")
.default(String::new())
.allow_empty(true)
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
secrets_config.backend = Some(SecretBackend::Kubernetes);
secrets_config.k8s_secret_name = Some(name);
if !namespace.is_empty() {
secrets_config.k8s_namespace = Some(namespace);
}
}
_ => {
secrets_config.backend = Some(SecretBackend::Keyring);
let service: String = Input::new()
.with_prompt("Keyring service name")
.default(default_keyring_service.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
secrets_config.keyring_service = service;
}
}
Ok(secrets_config)
}
#[cfg(any(
feature = "aws-secrets",
feature = "gcp-secrets",
feature = "azure-secrets"
))]
fn pick_or_input_name(
backend_label: &str,
default_secret_name: &str,
list_result: Result<Vec<String>, AppError>,
) -> Result<String, AppError> {
const ENTER_NEW: &str = "Enter a new name…";
let prompt_label = format!("{backend_label} secret name");
match list_result {
Ok(existing) if !existing.is_empty() => {
let mut items: Vec<String> = existing;
items.push(ENTER_NEW.to_string());
let idx = Select::new()
.with_prompt(&prompt_label)
.items(&items)
.default(0)
.interact()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
if items[idx] == ENTER_NEW {
let name: String = Input::new()
.with_prompt(format!("New {backend_label} secret name"))
.default(default_secret_name.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
Ok(name)
} else {
Ok(items.remove(idx))
}
}
Ok(_) => {
eprintln!(" No existing secrets found in {backend_label} — you'll create a new one.");
let name: String = Input::new()
.with_prompt(&prompt_label)
.default(default_secret_name.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
Ok(name)
}
Err(e) => {
eprintln!(
" Could not list existing {backend_label} secrets ({e}); falling back to manual entry."
);
let name: String = Input::new()
.with_prompt(&prompt_label)
.default(default_secret_name.to_string())
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
Ok(name)
}
}
}