// ABOUTME: CLI command for declarative deployment of EdgeConnect applications from YAML configuration // ABOUTME: Integrates config parser, deployment planner, and resource manager for complete deployment workflow package cmd import ( "context" "fmt" "log" "os" "path/filepath" "strings" applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v1" applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v2" "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" "github.com/spf13/cobra" ) var ( configFile string dryRun bool autoApprove bool ) var applyCmd = &cobra.Command{ Use: "apply", Short: "Deploy EdgeConnect applications from configuration files", Long: `Deploy EdgeConnect applications and their instances from YAML configuration files. This command reads a configuration file, analyzes the current state, and applies the necessary changes to deploy your applications across multiple cloudlets.`, Run: func(cmd *cobra.Command, args []string) { if configFile == "" { fmt.Fprintf(os.Stderr, "Error: configuration file is required\n") _ = cmd.Usage() os.Exit(1) } if err := runApply(configFile, dryRun, autoApprove); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } }, } func runApply(configPath string, isDryRun bool, autoApprove bool) error { // Step 1: Validate and resolve config file path absPath, err := filepath.Abs(configPath) if err != nil { return fmt.Errorf("failed to resolve config file path: %w", err) } if _, err := os.Stat(absPath); os.IsNotExist(err) { return fmt.Errorf("configuration file not found: %s", absPath) } fmt.Printf("šŸ“„ Loading configuration from: %s\n", absPath) // Step 2: Parse and validate configuration parser := config.NewParser() cfg, manifestContent, err := parser.ParseFile(absPath) if err != nil { return fmt.Errorf("failed to parse configuration: %w", err) } if err := parser.Validate(cfg); err != nil { return fmt.Errorf("configuration validation failed: %w", err) } fmt.Printf("āœ… Configuration loaded successfully: %s\n", cfg.Metadata.Name) // Step 3: Determine API version and create appropriate client apiVersion := getAPIVersion() // Step 4-6: Execute deployment based on API version if apiVersion == "v1" { return runApplyV1(cfg, manifestContent, isDryRun, autoApprove) } return runApplyV2(cfg, manifestContent, isDryRun, autoApprove) } func runApplyV1(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun bool, autoApprove bool) error { // Create v1 client client := newSDKClientV1() // Create deployment planner planner := applyv1.NewPlanner(client) // Generate deployment plan fmt.Println("šŸ” Analyzing current state and generating deployment plan...") planOptions := applyv1.DefaultPlanOptions() planOptions.DryRun = isDryRun result, err := planner.PlanWithOptions(context.Background(), cfg, planOptions) if err != nil { return fmt.Errorf("failed to generate deployment plan: %w", err) } // Display plan summary fmt.Println("\nšŸ“‹ Deployment Plan:") fmt.Println(strings.Repeat("=", 50)) fmt.Println(result.Plan.Summary) fmt.Println(strings.Repeat("=", 50)) // Display warnings if any if len(result.Warnings) > 0 { fmt.Println("\nāš ļø Warnings:") for _, warning := range result.Warnings { fmt.Printf(" • %s\n", warning) } } // If dry-run, stop here if isDryRun { fmt.Println("\nšŸ” Dry-run complete. No changes were made.") return nil } // Confirm deployment if result.Plan.TotalActions == 0 { fmt.Println("\nāœ… No changes needed. Resources are already in desired state.") return nil } fmt.Printf("\nThis will perform %d actions. Estimated time: %v\n", result.Plan.TotalActions, result.Plan.EstimatedDuration) if !autoApprove && !confirmDeployment() { fmt.Println("Deployment cancelled.") return nil } // Execute deployment fmt.Println("\nšŸš€ Starting deployment...") manager := applyv1.NewResourceManager(client, applyv1.WithLogger(log.Default())) deployResult, err := manager.ApplyDeployment(context.Background(), result.Plan, cfg, manifestContent) if err != nil { return fmt.Errorf("deployment failed: %w", err) } // Display results return displayDeploymentResults(deployResult) } func runApplyV2(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun bool, autoApprove bool) error { // Create v2 client client := newSDKClientV2() // Create deployment planner planner := applyv2.NewPlanner(client) // Generate deployment plan fmt.Println("šŸ” Analyzing current state and generating deployment plan...") planOptions := applyv2.DefaultPlanOptions() planOptions.DryRun = isDryRun result, err := planner.PlanWithOptions(context.Background(), cfg, planOptions) if err != nil { return fmt.Errorf("failed to generate deployment plan: %w", err) } // Display plan summary fmt.Println("\nšŸ“‹ Deployment Plan:") fmt.Println(strings.Repeat("=", 50)) fmt.Println(result.Plan.Summary) fmt.Println(strings.Repeat("=", 50)) // Display warnings if any if len(result.Warnings) > 0 { fmt.Println("\nāš ļø Warnings:") for _, warning := range result.Warnings { fmt.Printf(" • %s\n", warning) } } // If dry-run, stop here if isDryRun { fmt.Println("\nšŸ” Dry-run complete. No changes were made.") return nil } // Confirm deployment if result.Plan.TotalActions == 0 { fmt.Println("\nāœ… No changes needed. Resources are already in desired state.") return nil } fmt.Printf("\nThis will perform %d actions. Estimated time: %v\n", result.Plan.TotalActions, result.Plan.EstimatedDuration) if !autoApprove && !confirmDeployment() { fmt.Println("Deployment cancelled.") return nil } // Execute deployment fmt.Println("\nšŸš€ Starting deployment...") manager := applyv2.NewResourceManager(client, applyv2.WithLogger(log.Default())) deployResult, err := manager.ApplyDeployment(context.Background(), result.Plan, cfg, manifestContent) if err != nil { return fmt.Errorf("deployment failed: %w", err) } // Display results return displayDeploymentResults(deployResult) } func displayDeploymentResults(result interface{}) error { // Use reflection or type assertion to handle both v1 and v2 result types // For now, we'll use a simple approach that works with both switch r := result.(type) { case *applyv1.ExecutionResult: return displayDeploymentResultsV1(r) case *applyv2.ExecutionResult: return displayDeploymentResultsV2(r) default: return fmt.Errorf("unknown deployment result type") } } func displayDeploymentResultsV1(deployResult *applyv1.ExecutionResult) error { if deployResult.Success { fmt.Printf("\nāœ… Deployment completed successfully in %v\n", deployResult.Duration) if len(deployResult.CompletedActions) > 0 { fmt.Println("\nCompleted actions:") for _, action := range deployResult.CompletedActions { fmt.Printf(" āœ… %s %s\n", action.Type, action.Target) } } } else { fmt.Printf("\nāŒ Deployment failed after %v\n", deployResult.Duration) if deployResult.Error != nil { fmt.Printf("Error: %v\n", deployResult.Error) } if len(deployResult.FailedActions) > 0 { fmt.Println("\nFailed actions:") for _, action := range deployResult.FailedActions { fmt.Printf(" āŒ %s %s: %v\n", action.Type, action.Target, action.Error) } } return fmt.Errorf("deployment failed with %d failed actions", len(deployResult.FailedActions)) } return nil } func displayDeploymentResultsV2(deployResult *applyv2.ExecutionResult) error { if deployResult.Success { fmt.Printf("\nāœ… Deployment completed successfully in %v\n", deployResult.Duration) if len(deployResult.CompletedActions) > 0 { fmt.Println("\nCompleted actions:") for _, action := range deployResult.CompletedActions { fmt.Printf(" āœ… %s %s\n", action.Type, action.Target) } } } else { fmt.Printf("\nāŒ Deployment failed after %v\n", deployResult.Duration) if deployResult.Error != nil { fmt.Printf("Error: %v\n", deployResult.Error) } if len(deployResult.FailedActions) > 0 { fmt.Println("\nFailed actions:") for _, action := range deployResult.FailedActions { fmt.Printf(" āŒ %s %s: %v\n", action.Type, action.Target, action.Error) } } return fmt.Errorf("deployment failed with %d failed actions", len(deployResult.FailedActions)) } return nil } func confirmDeployment() bool { fmt.Print("Do you want to proceed? (yes/no): ") var response string _, _ = fmt.Scanln(&response) switch response { case "yes", "y", "YES", "Y": return true default: return false } } func init() { rootCmd.AddCommand(applyCmd) applyCmd.Flags().StringVarP(&configFile, "file", "f", "", "configuration file path (required)") applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without applying them") applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan") if err := applyCmd.MarkFlagRequired("file"); err != nil { panic(err) } }