2025-10-20 15:15:23 +02:00
|
|
|
|
// ABOUTME: CLI command for deleting EdgeConnect applications from YAML configuration
|
|
|
|
|
|
// ABOUTME: Removes applications and their instances based on configuration file specification
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2025-10-21 11:40:35 +02:00
|
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
|
|
|
|
|
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v1"
|
|
|
|
|
|
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v2"
|
2025-10-20 15:15:23 +02:00
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
deleteConfigFile string
|
|
|
|
|
|
deleteDryRun bool
|
|
|
|
|
|
deleteAutoApprove bool
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var deleteCmd = &cobra.Command{
|
|
|
|
|
|
Use: "delete",
|
|
|
|
|
|
Short: "Delete EdgeConnect applications from configuration files",
|
|
|
|
|
|
Long: `Delete EdgeConnect applications and their instances based on YAML configuration files.
|
|
|
|
|
|
This command reads a configuration file, finds matching resources, and deletes them.
|
|
|
|
|
|
Instances are always deleted before the application.`,
|
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
|
if deleteConfigFile == "" {
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
2025-10-22 12:47:15 +02:00
|
|
|
|
_ = cmd.Usage()
|
2025-10-20 15:15:23 +02:00
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := runDelete(deleteConfigFile, deleteDryRun, deleteAutoApprove); err != nil {
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runDelete(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, _, 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: Execute deletion based on API version
|
|
|
|
|
|
if apiVersion == "v1" {
|
|
|
|
|
|
return runDeleteV1(cfg, isDryRun, autoApprove)
|
|
|
|
|
|
}
|
|
|
|
|
|
return runDeleteV2(cfg, isDryRun, autoApprove)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runDeleteV1(cfg *config.EdgeConnectConfig, isDryRun bool, autoApprove bool) error {
|
|
|
|
|
|
// Create v1 client
|
|
|
|
|
|
client := newSDKClientV1()
|
|
|
|
|
|
|
|
|
|
|
|
// Create deletion planner
|
|
|
|
|
|
planner := deletev1.NewPlanner(client)
|
|
|
|
|
|
|
|
|
|
|
|
// Generate deletion plan
|
|
|
|
|
|
fmt.Println("🔍 Analyzing current state and generating deletion plan...")
|
|
|
|
|
|
|
|
|
|
|
|
planOptions := deletev1.DefaultPlanOptions()
|
|
|
|
|
|
planOptions.DryRun = isDryRun
|
|
|
|
|
|
|
|
|
|
|
|
result, err := planner.PlanWithOptions(context.Background(), cfg, planOptions)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to generate deletion plan: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display plan summary
|
|
|
|
|
|
fmt.Println("\n📋 Deletion 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if there's anything to delete
|
|
|
|
|
|
if result.Plan.TotalActions == 0 {
|
|
|
|
|
|
fmt.Println("\n✅ No resources found to delete.")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Printf("\nThis will delete %d resource(s). Estimated time: %v\n",
|
|
|
|
|
|
result.Plan.TotalActions, result.Plan.EstimatedDuration)
|
|
|
|
|
|
|
|
|
|
|
|
if !autoApprove && !confirmDeletion() {
|
|
|
|
|
|
fmt.Println("Deletion cancelled.")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Execute deletion
|
|
|
|
|
|
fmt.Println("\n🗑️ Starting deletion...")
|
|
|
|
|
|
|
|
|
|
|
|
manager := deletev1.NewResourceManager(client, deletev1.WithLogger(log.Default()))
|
|
|
|
|
|
deleteResult, err := manager.ExecuteDeletion(context.Background(), result.Plan)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("deletion failed: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display results
|
|
|
|
|
|
return displayDeletionResults(deleteResult)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runDeleteV2(cfg *config.EdgeConnectConfig, isDryRun bool, autoApprove bool) error {
|
|
|
|
|
|
// Create v2 client
|
|
|
|
|
|
client := newSDKClientV2()
|
|
|
|
|
|
|
|
|
|
|
|
// Create deletion planner
|
|
|
|
|
|
planner := deletev2.NewPlanner(client)
|
|
|
|
|
|
|
|
|
|
|
|
// Generate deletion plan
|
|
|
|
|
|
fmt.Println("🔍 Analyzing current state and generating deletion plan...")
|
|
|
|
|
|
|
|
|
|
|
|
planOptions := deletev2.DefaultPlanOptions()
|
|
|
|
|
|
planOptions.DryRun = isDryRun
|
|
|
|
|
|
|
|
|
|
|
|
result, err := planner.PlanWithOptions(context.Background(), cfg, planOptions)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to generate deletion plan: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display plan summary
|
|
|
|
|
|
fmt.Println("\n📋 Deletion 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if there's anything to delete
|
|
|
|
|
|
if result.Plan.TotalActions == 0 {
|
|
|
|
|
|
fmt.Println("\n✅ No resources found to delete.")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Printf("\nThis will delete %d resource(s). Estimated time: %v\n",
|
|
|
|
|
|
result.Plan.TotalActions, result.Plan.EstimatedDuration)
|
|
|
|
|
|
|
|
|
|
|
|
if !autoApprove && !confirmDeletion() {
|
|
|
|
|
|
fmt.Println("Deletion cancelled.")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Execute deletion
|
|
|
|
|
|
fmt.Println("\n🗑️ Starting deletion...")
|
|
|
|
|
|
|
|
|
|
|
|
manager := deletev2.NewResourceManager(client, deletev2.WithLogger(log.Default()))
|
|
|
|
|
|
deleteResult, err := manager.ExecuteDeletion(context.Background(), result.Plan)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("deletion failed: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display results
|
|
|
|
|
|
return displayDeletionResults(deleteResult)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func displayDeletionResults(result interface{}) error {
|
|
|
|
|
|
// Use type assertion to handle both v1 and v2 result types
|
|
|
|
|
|
switch r := result.(type) {
|
|
|
|
|
|
case *deletev1.DeletionResult:
|
|
|
|
|
|
return displayDeletionResultsV1(r)
|
|
|
|
|
|
case *deletev2.DeletionResult:
|
|
|
|
|
|
return displayDeletionResultsV2(r)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("unknown deletion result type")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func displayDeletionResultsV1(deleteResult *deletev1.DeletionResult) error {
|
|
|
|
|
|
if deleteResult.Success {
|
|
|
|
|
|
fmt.Printf("\n✅ Deletion completed successfully in %v\n", deleteResult.Duration)
|
|
|
|
|
|
if len(deleteResult.CompletedActions) > 0 {
|
|
|
|
|
|
fmt.Println("\nDeleted resources:")
|
|
|
|
|
|
for _, action := range deleteResult.CompletedActions {
|
|
|
|
|
|
fmt.Printf(" ✅ %s %s\n", action.Type, action.Target)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fmt.Printf("\n❌ Deletion failed after %v\n", deleteResult.Duration)
|
|
|
|
|
|
if deleteResult.Error != nil {
|
|
|
|
|
|
fmt.Printf("Error: %v\n", deleteResult.Error)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(deleteResult.FailedActions) > 0 {
|
|
|
|
|
|
fmt.Println("\nFailed actions:")
|
|
|
|
|
|
for _, action := range deleteResult.FailedActions {
|
|
|
|
|
|
fmt.Printf(" ❌ %s %s: %v\n", action.Type, action.Target, action.Error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("deletion failed with %d failed actions", len(deleteResult.FailedActions))
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func displayDeletionResultsV2(deleteResult *deletev2.DeletionResult) error {
|
|
|
|
|
|
if deleteResult.Success {
|
|
|
|
|
|
fmt.Printf("\n✅ Deletion completed successfully in %v\n", deleteResult.Duration)
|
|
|
|
|
|
if len(deleteResult.CompletedActions) > 0 {
|
|
|
|
|
|
fmt.Println("\nDeleted resources:")
|
|
|
|
|
|
for _, action := range deleteResult.CompletedActions {
|
|
|
|
|
|
fmt.Printf(" ✅ %s %s\n", action.Type, action.Target)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fmt.Printf("\n❌ Deletion failed after %v\n", deleteResult.Duration)
|
|
|
|
|
|
if deleteResult.Error != nil {
|
|
|
|
|
|
fmt.Printf("Error: %v\n", deleteResult.Error)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(deleteResult.FailedActions) > 0 {
|
|
|
|
|
|
fmt.Println("\nFailed actions:")
|
|
|
|
|
|
for _, action := range deleteResult.FailedActions {
|
|
|
|
|
|
fmt.Printf(" ❌ %s %s: %v\n", action.Type, action.Target, action.Error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("deletion failed with %d failed actions", len(deleteResult.FailedActions))
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func confirmDeletion() bool {
|
|
|
|
|
|
fmt.Print("Do you want to proceed with deletion? (yes/no): ")
|
|
|
|
|
|
var response string
|
2025-10-22 12:47:15 +02:00
|
|
|
|
_, _ = fmt.Scanln(&response)
|
2025-10-20 15:15:23 +02:00
|
|
|
|
|
|
|
|
|
|
switch response {
|
|
|
|
|
|
case "yes", "y", "YES", "Y":
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
rootCmd.AddCommand(deleteCmd)
|
|
|
|
|
|
|
|
|
|
|
|
deleteCmd.Flags().StringVarP(&deleteConfigFile, "file", "f", "", "configuration file path (required)")
|
|
|
|
|
|
deleteCmd.Flags().BoolVar(&deleteDryRun, "dry-run", false, "preview deletion without actually deleting resources")
|
|
|
|
|
|
deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan")
|
|
|
|
|
|
|
2025-10-22 12:47:15 +02:00
|
|
|
|
if err := deleteCmd.MarkFlagRequired("file"); err != nil {
|
|
|
|
|
|
panic(err)
|
|
|
|
|
|
}
|
2025-10-20 15:15:23 +02:00
|
|
|
|
}
|