Skip to main content

seal/cli/
interactive.rs

1//! Interactive prompt mode for creating sealed secrets
2//!
3//! Provides a guided, user-friendly interface for users who prefer
4//! step-by-step prompts over command-line flags.
5
6use crate::models::{SecretKeyValue, SecretRequest};
7use crate::ValidationError;
8use dialoguer::{Confirm, Input, Password};
9use std::collections::HashMap;
10
11/// Run interactive mode to collect secret information from user
12///
13/// This function guides the user through creating a sealed secret
14/// by prompting for each required and optional field.
15///
16/// # Returns
17///
18/// Returns a `SecretRequest` with all user-provided information,
19/// or an error if the user cancels (Ctrl+C) or provides invalid input.
20///
21/// # Errors
22///
23/// Returns error if:
24/// - User cancels with Ctrl+C
25/// - Input validation fails
26/// - I/O error occurs during prompting
27pub fn interactive_create() -> Result<SecretRequest, anyhow::Error> {
28    eprintln!("\n=== Interactive Sealed Secret Creation ===\n");
29
30    // Prompt for secret name
31    let name: String = Input::new()
32        .with_prompt("Secret name (lowercase alphanumeric, hyphens, dots)")
33        .validate_with(|input: &String| -> Result<(), &str> {
34            crate::models::validate_secret_name(input).map_err(|_| {
35                "Invalid secret name. Must follow Kubernetes naming rules:\n  \
36                 - lowercase alphanumeric, hyphens, or dots\n  \
37                 - must start and end with alphanumeric\n  \
38                 - max 253 characters"
39            })
40        })
41        .interact_text()?;
42
43    // Prompt for namespace
44    let namespace: String = Input::new()
45        .with_prompt("Namespace (lowercase alphanumeric with hyphens)")
46        .validate_with(|input: &String| -> Result<(), &str> {
47            crate::models::validate_namespace(input).map_err(|_| {
48                "Invalid namespace. Must follow Kubernetes naming rules:\n  \
49                 - lowercase alphanumeric with hyphens\n  \
50                 - must start and end with alphanumeric\n  \
51                 - max 63 characters"
52            })
53        })
54        .interact_text()?;
55
56    // Collect key-value pairs
57    eprintln!("\n--- Secret Data ---");
58    eprintln!("Enter key-value pairs (leave key empty to finish)\n");
59
60    let mut data = Vec::new();
61    loop {
62        let key: String = Input::new()
63            .with_prompt("Secret key")
64            .allow_empty(true)
65            .interact_text()?;
66
67        if key.is_empty() {
68            if data.is_empty() {
69                eprintln!("⚠️  At least one key-value pair is required");
70                continue;
71            }
72            break;
73        }
74
75        // Validate key
76        if let Err(e) = crate::models::validate_key_name(&key) {
77            eprintln!("⚠️  Invalid key: {}", e);
78            eprintln!("   Keys must start with letter or underscore, followed by alphanumeric or underscores\n");
79            continue;
80        }
81
82        // Check for duplicate keys
83        if data.iter().any(|kv: &SecretKeyValue| kv.key == key) {
84            eprintln!("⚠️  Key '{}' already exists\n", key);
85            continue;
86        }
87
88        // Prompt for value (masked)
89        let value: String = Password::new()
90            .with_prompt(format!("Secret value for '{}'", key))
91            .interact()?;
92
93        if value.is_empty() {
94            eprintln!("⚠️  Value cannot be empty\n");
95            continue;
96        }
97
98        data.push(SecretKeyValue::new(key, value)?);
99        eprintln!("✓ Added\n");
100    }
101
102    // Optional: Add labels
103    eprintln!("\n--- Labels (Optional) ---");
104    let add_labels = Confirm::new()
105        .with_prompt("Add labels to the secret?")
106        .default(false)
107        .interact()?;
108
109    let mut labels = HashMap::new();
110    if add_labels {
111        eprintln!("Enter labels (leave key empty to finish)\n");
112        loop {
113            let key: String = Input::new()
114                .with_prompt("Label key")
115                .allow_empty(true)
116                .interact_text()?;
117
118            if key.is_empty() {
119                break;
120            }
121
122            let value: String = Input::new()
123                .with_prompt(format!("Label value for '{}'", key))
124                .interact_text()?;
125
126            labels.insert(key, value);
127            eprintln!("✓ Added\n");
128        }
129    }
130
131    // Optional: Add annotations
132    eprintln!("\n--- Annotations (Optional) ---");
133    let add_annotations = Confirm::new()
134        .with_prompt("Add annotations to the secret?")
135        .default(false)
136        .interact()?;
137
138    let mut annotations = HashMap::new();
139    if add_annotations {
140        eprintln!("Enter annotations (leave key empty to finish)\n");
141        loop {
142            let key: String = Input::new()
143                .with_prompt("Annotation key")
144                .allow_empty(true)
145                .interact_text()?;
146
147            if key.is_empty() {
148                break;
149            }
150
151            let value: String = Input::new()
152                .with_prompt(format!("Annotation value for '{}'", key))
153                .interact_text()?;
154
155            annotations.insert(key, value);
156            eprintln!("✓ Added\n");
157        }
158    }
159
160    // Display summary and confirm
161    eprintln!("\n=== Summary ===");
162    eprintln!("Name: {}", name);
163    eprintln!("Namespace: {}", namespace);
164    eprintln!(
165        "Keys: {}",
166        data.iter()
167            .map(|kv| kv.key.as_str())
168            .collect::<Vec<_>>()
169            .join(", ")
170    );
171
172    if !labels.is_empty() {
173        eprintln!(
174            "Labels: {}",
175            labels
176                .iter()
177                .map(|(k, v)| format!("{}={}", k, v))
178                .collect::<Vec<_>>()
179                .join(", ")
180        );
181    } else {
182        eprintln!("Labels: none");
183    }
184
185    if !annotations.is_empty() {
186        eprintln!(
187            "Annotations: {}",
188            annotations
189                .iter()
190                .map(|(k, v)| format!("{}={}", k, v))
191                .collect::<Vec<_>>()
192                .join(", ")
193        );
194    } else {
195        eprintln!("Annotations: none");
196    }
197
198    eprintln!();
199    let confirmed = Confirm::new()
200        .with_prompt("Create SealedSecret with this configuration?")
201        .default(true)
202        .interact()?;
203
204    if !confirmed {
205        return Err(anyhow::anyhow!("Cancelled by user"));
206    }
207
208    // Create SecretRequest
209    SecretRequest::new(name, namespace, labels, annotations, data)
210        .map_err(|e: ValidationError| anyhow::anyhow!("Validation error: {}", e))
211}
212
213#[cfg(test)]
214mod tests {
215    // Note: Interactive tests require user input simulation
216    // These tests verify the structure is correct
217    // Full integration tests should use expect-style testing
218
219    #[test]
220    fn test_interactive_create_function_exists() {
221        // Verify the function signature compiles
222        // Real testing requires input simulation tools like expectrl
223        // This is a placeholder test to verify the module structure
224    }
225}