1use clap::{Parser, Subcommand};
4use std::collections::HashMap;
5
6#[derive(Parser, Debug)]
7#[command(name = "seal")]
8#[command(version)]
9#[command(about = "Create Kubernetes SealedSecrets with a single secure command")]
10#[command(
11 long_about = "Seal simplifies the creation of Kubernetes SealedSecrets by handling temporary file\n\
12 management, cleanup, and kubeseal integration automatically.\n\n\
13 Instead of manually creating plaintext secrets and sealing them, Seal does\n\
14 everything in one secure command with guaranteed cleanup.\n\n\
15 For more information: https://github.com/yourusername/seal"
16)]
17#[command(author = "Seal Contributors")]
18#[command(after_help = "EXAMPLES:\n \
19 # Create sealed secret with flags\n \
20 seal create --name db-creds --namespace prod \\\n \
21 --from-literal username=admin \\\n \
22 --from-literal password=secret\n\n \
23 # Create sealed secret interactively\n \
24 seal create --interactive\n\n \
25 # Pipe output to file\n \
26 seal create --name app-key --namespace default \\\n \
27 --from-literal token=xyz > sealed-secret.yaml\n\n \
28 # Apply directly to cluster\n \
29 seal create -n db-creds -N prod -l user=admin -l pass=secret | kubectl apply -f -")]
30pub struct Cli {
31 #[command(subcommand)]
32 pub command: Commands,
33
34 #[arg(short, long, global = true)]
36 pub verbose: bool,
37}
38
39#[derive(Subcommand, Debug)]
40pub enum Commands {
41 Create(CreateCommand),
43}
44
45#[derive(Parser, Debug)]
46#[command(about = "Create a SealedSecret from key-value pairs")]
47#[command(long_about = "Create a SealedSecret from key-value pairs.\n\n\
48 This command creates a plaintext Kubernetes Secret, seals it using kubeseal,\n\
49 and outputs the encrypted SealedSecret YAML. All temporary files are automatically\n\
50 cleaned up with no traces left behind.")]
51#[command(after_help = "EXAMPLES:\n \
52 # Basic secret creation\n \
53 seal create -n db-password -N production \\\n \
54 -l username=admin -l password=secret123\n\n \
55 # With labels and annotations\n \
56 seal create -n api-key -N default -l KEY=abc123 \\\n \
57 -L app=backend -L env=staging \\\n \
58 -A owner=platform-team\n\n \
59 # Interactive mode (guided prompts)\n \
60 seal create --interactive\n\n \
61 # Save to file\n \
62 seal create -n app-secret -N prod -l TOKEN=xyz > sealed-secret.yaml\n\n \
63 # Apply directly to cluster\n \
64 seal create -n db-creds -N prod -l user=admin | kubectl apply -f -\n\n\
65NOTES:\n \
66 - The kubeseal binary must be installed and in PATH\n \
67 - A valid kubeconfig with active context is required\n \
68 - Temporary files are automatically cleaned up (no traces left)\n \
69 - Secret values are never logged or persisted\n \
70 - Cluster context is displayed before sealing (safety feature)")]
71pub struct CreateCommand {
72 #[arg(short, long, value_name = "NAME")]
74 pub name: String,
75
76 #[arg(short = 'N', long, value_name = "NAMESPACE")]
78 pub namespace: String,
79
80 #[arg(short = 'l', long = "from-literal", value_name = "KEY=VALUE")]
82 pub from_literal: Vec<String>,
83
84 #[arg(short = 'L', long = "label", value_name = "KEY=VALUE")]
86 pub labels: Vec<String>,
87
88 #[arg(short = 'A', long = "annotation", value_name = "KEY=VALUE")]
90 pub annotations: Vec<String>,
91
92 #[arg(short, long, default_value = "yaml", value_name = "FORMAT")]
94 pub output: String,
95
96 #[arg(short, long)]
98 pub interactive: bool,
99}
100
101impl CreateCommand {
102 fn parse_key_values(items: &[String]) -> Result<HashMap<String, String>, anyhow::Error> {
104 let mut map = HashMap::new();
105
106 for item in items {
107 let parts: Vec<&str> = item.splitn(2, '=').collect();
108 if parts.len() != 2 {
109 anyhow::bail!("Invalid format '{}': expected KEY=VALUE", item);
110 }
111
112 map.insert(parts[0].to_string(), parts[1].to_string());
113 }
114
115 Ok(map)
116 }
117
118 pub fn to_secret_request(&self) -> Result<crate::models::SecretRequest, anyhow::Error> {
120 let data_map = Self::parse_key_values(&self.from_literal)?;
122
123 if data_map.is_empty() {
124 anyhow::bail!("At least one --from-literal KEY=VALUE pair is required");
125 }
126
127 let mut data = Vec::new();
129 for (key, value) in data_map {
130 data.push(crate::models::SecretKeyValue::new(key, value)?);
131 }
132
133 let labels = Self::parse_key_values(&self.labels)?;
135 let annotations = Self::parse_key_values(&self.annotations)?;
136
137 crate::models::SecretRequest::new(
139 self.name.clone(),
140 self.namespace.clone(),
141 labels,
142 annotations,
143 data,
144 )
145 .map_err(|e| anyhow::anyhow!(e))
146 }
147
148 pub fn validate(&self) -> Result<(), anyhow::Error> {
150 if !self.interactive {
151 if self.name.is_empty() {
153 anyhow::bail!("--name is required (or use --interactive mode)");
154 }
155
156 if self.namespace.is_empty() {
157 anyhow::bail!("--namespace is required (or use --interactive mode)");
158 }
159
160 if self.from_literal.is_empty() {
161 anyhow::bail!(
162 "At least one --from-literal KEY=VALUE is required (or use --interactive mode)"
163 );
164 }
165 }
166
167 if self.output != "yaml" && self.output != "json" {
169 anyhow::bail!("Output format must be 'yaml' or 'json'");
170 }
171
172 Ok(())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_parse_key_values() -> anyhow::Result<()> {
182 let items = vec!["key1=value1".to_string(), "key2=value2".to_string()];
183 let map = CreateCommand::parse_key_values(&items)?;
184
185 assert_eq!(map.get("key1"), Some(&"value1".to_string()));
186 assert_eq!(map.get("key2"), Some(&"value2".to_string()));
187 assert_eq!(map.len(), 2);
188
189 Ok(())
190 }
191
192 #[test]
193 fn test_parse_key_values_with_equals_in_value() -> anyhow::Result<()> {
194 let items = vec!["key=value=with=equals".to_string()];
195 let map = CreateCommand::parse_key_values(&items)?;
196
197 assert_eq!(map.get("key"), Some(&"value=with=equals".to_string()));
198
199 Ok(())
200 }
201
202 #[test]
203 fn test_parse_key_values_invalid_format() {
204 let items = vec!["invalid_no_equals".to_string()];
205 let result = CreateCommand::parse_key_values(&items);
206
207 assert!(result.is_err());
208 }
209}