edge-connect-client/cmd/apply.go
Richard Robert Reitz 9772a072e8
All checks were successful
test / test (push) Successful in 46s
chore(linting): Fixed all linter errors
2025-10-22 12:47:15 +02:00

297 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}
}