edge-connect-client/cmd/apply.go
Richard Robert Reitz 59ba5ffb02 fix(apply): add validation to reject v1 API version
The apply command requires v2 API features and cannot work with v1.
Add early validation to provide a clear error message when users try
to use apply with --api-version v1, instead of failing with a cryptic
403 Forbidden error.

Error message explains that apply only supports v2 and guides users
to use --api-version v2 or remove the api_version setting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 13:49:09 +02:00

183 lines
5.5 KiB
Go
Raw 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"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/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: Validate API version (apply only supports v2)
apiVersion := getAPIVersion()
if apiVersion == "v1" {
return fmt.Errorf("apply command only supports API v2. The v1 API does not support the advanced deployment features required by this command. Please use --api-version v2 or remove the api_version setting")
}
// Step 4: Create EdgeConnect client (v2 only)
client := newSDKClientV2()
// Step 5: Create deployment planner
planner := apply.NewPlanner(client)
// Step 6: Generate deployment plan
fmt.Println("🔍 Analyzing current state and generating deployment plan...")
planOptions := apply.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)
}
// Step 6: 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)
}
}
// Step 7: If dry-run, stop here
if isDryRun {
fmt.Println("\n🔍 Dry-run complete. No changes were made.")
return nil
}
// Step 8: Confirm deployment (in non-dry-run mode)
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
}
// Step 9: Execute deployment
fmt.Println("\n🚀 Starting deployment...")
manager := apply.NewResourceManager(client, apply.WithLogger(log.Default()))
deployResult, err := manager.ApplyDeployment(context.Background(), result.Plan, cfg, manifestContent)
if err != nil {
return fmt.Errorf("deployment failed: %w", err)
}
// Step 10: Display results
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")
applyCmd.MarkFlagRequired("file")
}