use clap::{Parser, Subcommand};
use std::collections::HashMap;
#[derive(Parser, Debug)]
#[command(name = "seal")]
#[command(version)]
#[command(about = "Create Kubernetes SealedSecrets with a single secure command")]
#[command(
long_about = "Seal simplifies the creation of Kubernetes SealedSecrets by handling temporary file\n\
management, cleanup, and kubeseal integration automatically.\n\n\
Instead of manually creating plaintext secrets and sealing them, Seal does\n\
everything in one secure command with guaranteed cleanup.\n\n\
For more information: https://github.com/yourusername/seal"
)]
#[command(author = "Seal Contributors")]
#[command(after_help = "EXAMPLES:\n \
# Create sealed secret with flags\n \
seal create --name db-creds --namespace prod \\\n \
--from-literal username=admin \\\n \
--from-literal password=secret\n\n \
# Create sealed secret interactively\n \
seal create --interactive\n\n \
# Pipe output to file\n \
seal create --name app-key --namespace default \\\n \
--from-literal token=xyz > sealed-secret.yaml\n\n \
# Apply directly to cluster\n \
seal create -n db-creds -N prod -l user=admin -l pass=secret | kubectl apply -f -")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Create(CreateCommand),
}
#[derive(Parser, Debug)]
#[command(about = "Create a SealedSecret from key-value pairs")]
#[command(long_about = "Create a SealedSecret from key-value pairs.\n\n\
This command creates a plaintext Kubernetes Secret, seals it using kubeseal,\n\
and outputs the encrypted SealedSecret YAML. All temporary files are automatically\n\
cleaned up with no traces left behind.")]
#[command(after_help = "EXAMPLES:\n \
# Basic secret creation\n \
seal create -n db-password -N production \\\n \
-l username=admin -l password=secret123\n\n \
# With labels and annotations\n \
seal create -n api-key -N default -l KEY=abc123 \\\n \
-L app=backend -L env=staging \\\n \
-A owner=platform-team\n\n \
# Interactive mode (guided prompts)\n \
seal create --interactive\n\n \
# Save to file\n \
seal create -n app-secret -N prod -l TOKEN=xyz > sealed-secret.yaml\n\n \
# Apply directly to cluster\n \
seal create -n db-creds -N prod -l user=admin | kubectl apply -f -\n\n\
NOTES:\n \
- The kubeseal binary must be installed and in PATH\n \
- A valid kubeconfig with active context is required\n \
- Temporary files are automatically cleaned up (no traces left)\n \
- Secret values are never logged or persisted\n \
- Cluster context is displayed before sealing (safety feature)")]
pub struct CreateCommand {
#[arg(short, long, value_name = "NAME")]
pub name: String,
#[arg(short = 'N', long, value_name = "NAMESPACE")]
pub namespace: String,
#[arg(short = 'l', long = "from-literal", value_name = "KEY=VALUE")]
pub from_literal: Vec<String>,
#[arg(short = 'L', long = "label", value_name = "KEY=VALUE")]
pub labels: Vec<String>,
#[arg(short = 'A', long = "annotation", value_name = "KEY=VALUE")]
pub annotations: Vec<String>,
#[arg(short, long, default_value = "yaml", value_name = "FORMAT")]
pub output: String,
#[arg(short, long)]
pub interactive: bool,
}
impl CreateCommand {
fn parse_key_values(items: &[String]) -> Result<HashMap<String, String>, anyhow::Error> {
let mut map = HashMap::new();
for item in items {
let parts: Vec<&str> = item.splitn(2, '=').collect();
if parts.len() != 2 {
anyhow::bail!("Invalid format '{}': expected KEY=VALUE", item);
}
map.insert(parts[0].to_string(), parts[1].to_string());
}
Ok(map)
}
pub fn to_secret_request(&self) -> Result<crate::models::SecretRequest, anyhow::Error> {
let data_map = Self::parse_key_values(&self.from_literal)?;
if data_map.is_empty() {
anyhow::bail!("At least one --from-literal KEY=VALUE pair is required");
}
let mut data = Vec::new();
for (key, value) in data_map {
data.push(crate::models::SecretKeyValue::new(key, value)?);
}
let labels = Self::parse_key_values(&self.labels)?;
let annotations = Self::parse_key_values(&self.annotations)?;
crate::models::SecretRequest::new(
self.name.clone(),
self.namespace.clone(),
labels,
annotations,
data,
)
.map_err(|e| anyhow::anyhow!(e))
}
pub fn validate(&self) -> Result<(), anyhow::Error> {
if !self.interactive {
if self.name.is_empty() {
anyhow::bail!("--name is required (or use --interactive mode)");
}
if self.namespace.is_empty() {
anyhow::bail!("--namespace is required (or use --interactive mode)");
}
if self.from_literal.is_empty() {
anyhow::bail!(
"At least one --from-literal KEY=VALUE is required (or use --interactive mode)"
);
}
}
if self.output != "yaml" && self.output != "json" {
anyhow::bail!("Output format must be 'yaml' or 'json'");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key_values() -> anyhow::Result<()> {
let items = vec!["key1=value1".to_string(), "key2=value2".to_string()];
let map = CreateCommand::parse_key_values(&items)?;
assert_eq!(map.get("key1"), Some(&"value1".to_string()));
assert_eq!(map.get("key2"), Some(&"value2".to_string()));
assert_eq!(map.len(), 2);
Ok(())
}
#[test]
fn test_parse_key_values_with_equals_in_value() -> anyhow::Result<()> {
let items = vec!["key=value=with=equals".to_string()];
let map = CreateCommand::parse_key_values(&items)?;
assert_eq!(map.get("key"), Some(&"value=with=equals".to_string()));
Ok(())
}
#[test]
fn test_parse_key_values_invalid_format() {
let items = vec!["invalid_no_equals".to_string()];
let result = CreateCommand::parse_key_values(&items);
assert!(result.is_err());
}
}