syncable_cli/wizard/
environment_creation.rs

1//! Environment creation wizard for deployment targets
2//!
3//! Interactive wizard that guides users through creating a new environment
4//! with target type selection (Kubernetes or Cloud Runner).
5
6use crate::platform::api::client::PlatformApiClient;
7use crate::platform::api::types::{ClusterSummary, Environment};
8use crate::wizard::provider_selection::get_provider_deployment_statuses;
9use crate::wizard::render::{display_step_header, wizard_render_config};
10use colored::Colorize;
11use inquire::{InquireError, Select, Text};
12
13/// Environment type for the API
14/// "cluster" = Kubernetes cluster
15/// "cloud" = Cloud Runner (serverless)
16#[derive(Debug, Clone, PartialEq, Eq)]
17enum EnvironmentType {
18    Cluster,
19    Cloud,
20}
21
22impl EnvironmentType {
23    fn as_str(&self) -> &'static str {
24        match self {
25            EnvironmentType::Cluster => "cluster",
26            EnvironmentType::Cloud => "cloud",
27        }
28    }
29
30    fn display_name(&self) -> &'static str {
31        match self {
32            EnvironmentType::Cluster => "Kubernetes",
33            EnvironmentType::Cloud => "Cloud Runner",
34        }
35    }
36}
37
38/// Result of environment creation wizard
39#[derive(Debug)]
40pub enum EnvironmentCreationResult {
41    /// Environment created successfully
42    Created(Environment),
43    /// User cancelled the wizard
44    Cancelled,
45    /// An error occurred
46    Error(String),
47}
48
49/// Run the environment creation wizard
50///
51/// Guides user through:
52/// 1. Choosing environment name
53/// 2. Selecting target type (Kubernetes or Cloud Runner)
54/// 3. If Kubernetes: selecting a cluster
55pub async fn create_environment_wizard(
56    client: &PlatformApiClient,
57    project_id: &str,
58) -> EnvironmentCreationResult {
59    display_step_header(
60        1,
61        "Create Environment",
62        "Set up a new deployment environment for your project.",
63    );
64
65    // Step 1: Get environment name
66    let name = match Text::new("Environment name:")
67        .with_placeholder("e.g., production, staging, development")
68        .with_help_message("Choose a descriptive name for this environment")
69        .prompt()
70    {
71        Ok(name) => {
72            if name.trim().is_empty() {
73                println!("\n{}", "Environment name cannot be empty.".red());
74                return EnvironmentCreationResult::Cancelled;
75            }
76            name.trim().to_string()
77        }
78        Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
79            println!("\n{}", "Wizard cancelled.".dimmed());
80            return EnvironmentCreationResult::Cancelled;
81        }
82        Err(e) => {
83            return EnvironmentCreationResult::Error(format!("Input error: {}", e));
84        }
85    };
86
87    // Step 2: Select target type
88    display_step_header(
89        2,
90        "Select Target Type",
91        "Choose how this environment will deploy services.",
92    );
93
94    let target_options = vec![
95        format!(
96            "{}  {}",
97            "Cloud Runner".cyan(),
98            "Fully managed, auto-scaling containers".dimmed()
99        ),
100        format!(
101            "{}  {}",
102            "Kubernetes".cyan(),
103            "Deploy to your own K8s cluster".dimmed()
104        ),
105    ];
106
107    let target_selection = Select::new("Select target type:", target_options)
108        .with_render_config(wizard_render_config())
109        .with_help_message("Cloud Runner: serverless, Kubernetes: full control")
110        .prompt();
111
112    let env_type = match target_selection {
113        Ok(answer) => {
114            if answer.contains("Cloud Runner") {
115                EnvironmentType::Cloud
116            } else {
117                EnvironmentType::Cluster
118            }
119        }
120        Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
121            println!("\n{}", "Wizard cancelled.".dimmed());
122            return EnvironmentCreationResult::Cancelled;
123        }
124        Err(e) => {
125            return EnvironmentCreationResult::Error(format!("Selection error: {}", e));
126        }
127    };
128
129    println!(
130        "\n{} Target: {}",
131        "✓".green(),
132        env_type.display_name().bold()
133    );
134
135    // Step 3: If Kubernetes (cluster), select cluster
136    let cluster_id = if env_type == EnvironmentType::Cluster {
137        match select_cluster_for_env(client, project_id).await {
138            ClusterSelectionResult::Selected(id) => Some(id),
139            ClusterSelectionResult::NoClusters => {
140                println!(
141                    "\n{}",
142                    "No Kubernetes clusters available. Please provision a cluster first.".red()
143                );
144                return EnvironmentCreationResult::Cancelled;
145            }
146            ClusterSelectionResult::Cancelled => {
147                return EnvironmentCreationResult::Cancelled;
148            }
149            ClusterSelectionResult::Error(e) => {
150                return EnvironmentCreationResult::Error(e);
151            }
152        }
153    } else {
154        None
155    };
156
157    // Create the environment via API
158    println!("\n{}", "Creating environment...".dimmed());
159
160    match client
161        .create_environment(
162            project_id,
163            &name,
164            env_type.as_str(),
165            cluster_id.as_deref(),
166        )
167        .await
168    {
169        Ok(env) => {
170            println!(
171                "\n{} Environment {} created successfully!",
172                "✓".green().bold(),
173                env.name.bold()
174            );
175            println!("  ID: {}", env.id.dimmed());
176            println!("  Type: {}", env.environment_type);
177            if let Some(cid) = &env.cluster_id {
178                println!("  Cluster: {}", cid);
179            }
180            EnvironmentCreationResult::Created(env)
181        }
182        Err(e) => EnvironmentCreationResult::Error(format!("Failed to create environment: {}", e)),
183    }
184}
185
186/// Result of cluster selection
187enum ClusterSelectionResult {
188    Selected(String),
189    NoClusters,
190    Cancelled,
191    Error(String),
192}
193
194/// Select a Kubernetes cluster from available clusters
195async fn select_cluster_for_env(
196    client: &PlatformApiClient,
197    project_id: &str,
198) -> ClusterSelectionResult {
199    display_step_header(
200        3,
201        "Select Cluster",
202        "Choose a Kubernetes cluster for this environment.",
203    );
204
205    // Get available clusters
206    let clusters: Vec<ClusterSummary> =
207        match get_available_clusters_for_project(client, project_id).await {
208            Ok(c) => c,
209            Err(e) => return ClusterSelectionResult::Error(e),
210        };
211
212    if clusters.is_empty() {
213        return ClusterSelectionResult::NoClusters;
214    }
215
216    // Build options
217    let options: Vec<String> = clusters
218        .iter()
219        .map(|c| {
220            let health = if c.is_healthy {
221                "healthy".green()
222            } else {
223                "unhealthy".red()
224            };
225            format!("{} ({}) - {}", c.name.bold(), c.region.dimmed(), health)
226        })
227        .collect();
228
229    let selection = Select::new("Select cluster:", options.clone())
230        .with_render_config(wizard_render_config())
231        .with_help_message("Choose the cluster to deploy to")
232        .prompt();
233
234    match selection {
235        Ok(answer) => {
236            // Find the selected cluster by matching the name at the start
237            let selected_name = answer.split(" (").next().unwrap_or("");
238            if let Some(cluster) = clusters.iter().find(|c| c.name == selected_name) {
239                println!("\n{} Selected: {}", "✓".green(), cluster.name.bold());
240                ClusterSelectionResult::Selected(cluster.id.clone())
241            } else {
242                ClusterSelectionResult::Error("Failed to match selected cluster".to_string())
243            }
244        }
245        Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
246            println!("\n{}", "Wizard cancelled.".dimmed());
247            ClusterSelectionResult::Cancelled
248        }
249        Err(e) => ClusterSelectionResult::Error(format!("Selection error: {}", e)),
250    }
251}
252
253/// Get available clusters from all connected providers for a project
254async fn get_available_clusters_for_project(
255    client: &PlatformApiClient,
256    project_id: &str,
257) -> Result<Vec<ClusterSummary>, String> {
258    // Get provider deployment statuses which include cluster info
259    let statuses = get_provider_deployment_statuses(client, project_id)
260        .await
261        .map_err(|e| format!("Failed to get provider statuses: {}", e))?;
262
263    // Collect all clusters from connected providers
264    let mut all_clusters = Vec::new();
265    for status in statuses {
266        if status.is_connected {
267            all_clusters.extend(status.clusters);
268        }
269    }
270
271    Ok(all_clusters)
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_environment_creation_result_variants() {
280        let created = EnvironmentCreationResult::Created(Environment {
281            id: "env-1".to_string(),
282            name: "test".to_string(),
283            project_id: "proj-1".to_string(),
284            environment_type: "cloud".to_string(),
285            cluster_id: None,
286            namespace: None,
287            description: None,
288            is_active: true,
289            created_at: None,
290            updated_at: None,
291        });
292        assert!(matches!(created, EnvironmentCreationResult::Created(_)));
293
294        let cancelled = EnvironmentCreationResult::Cancelled;
295        assert!(matches!(cancelled, EnvironmentCreationResult::Cancelled));
296
297        let error = EnvironmentCreationResult::Error("test error".to_string());
298        assert!(matches!(error, EnvironmentCreationResult::Error(_)));
299    }
300
301    #[test]
302    fn test_environment_type_as_str() {
303        assert_eq!(EnvironmentType::Cluster.as_str(), "cluster");
304        assert_eq!(EnvironmentType::Cloud.as_str(), "cloud");
305    }
306
307    #[test]
308    fn test_environment_type_display_name() {
309        assert_eq!(EnvironmentType::Cluster.display_name(), "Kubernetes");
310        assert_eq!(EnvironmentType::Cloud.display_name(), "Cloud Runner");
311    }
312}