edge-connect-client/cmd/delete.go
Richard Robert Reitz 9363277532
All checks were successful
test / test (push) Successful in 48s
fix(sdk): correct delete payload structure for v2 API and add delete command
The v2 API requires a different JSON payload structure than what was being sent.
Both DeleteApp and DeleteAppInstance needed to wrap their parameters properly.

SDK Changes:
- Update DeleteAppInput to use {region, app: {key}} structure
- Update DeleteAppInstanceInput to use {region, appinst: {key}} structure
- Fix DeleteApp method to populate new payload structure
- Fix DeleteAppInstance method to populate new payload structure

CLI Changes:
- Add delete command with -f flag for config file specification
- Support --dry-run to preview deletions
- Support --auto-approve to skip confirmation
- Implement v1 and v2 API support following same pattern as apply
- Add deletion planner to discover resources matching config
- Add resource manager to execute deletions (instances first, then app)

Test Changes:
- Update example_test.go to use EdgeConnectConfig_v1.yaml
- All tests passing including comprehensive delete test coverage

Verified working with manual API testing against live endpoint.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 15:15:23 +02:00

294 lines
8.6 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 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"
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v1"
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"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")
cmd.Usage()
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
fmt.Scanln(&response)
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")
deleteCmd.MarkFlagRequired("file")
}