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>
This commit is contained in:
parent
f921169351
commit
df697c0ff6
14 changed files with 1921 additions and 5 deletions
294
cmd/delete.go
Normal file
294
cmd/delete.go
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
// 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")
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ func TestParseExampleConfig(t *testing.T) {
|
|||
parser := NewParser()
|
||||
|
||||
// Parse the actual example file (now that we've created the manifest file)
|
||||
examplePath := filepath.Join("../../sdk/examples/comprehensive/EdgeConnectConfig.yaml")
|
||||
examplePath := filepath.Join("../../sdk/examples/comprehensive/EdgeConnectConfig_v1.yaml")
|
||||
config, parsedManifest, err := parser.ParseFile(examplePath)
|
||||
|
||||
// This should now succeed with full validation
|
||||
|
|
|
|||
166
internal/delete/v1/manager.go
Normal file
166
internal/delete/v1/manager.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// ABOUTME: Resource management for EdgeConnect delete command with deletion execution
|
||||
// ABOUTME: Handles actual deletion operations with proper ordering (instances first, then app)
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
type ResourceManagerInterface interface {
|
||||
// ExecuteDeletion executes a deletion plan
|
||||
ExecuteDeletion(ctx context.Context, plan *DeletionPlan) (*DeletionResult, error)
|
||||
}
|
||||
|
||||
// EdgeConnectResourceManager implements resource management for EdgeConnect
|
||||
type EdgeConnectResourceManager struct {
|
||||
client EdgeConnectClientInterface
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// Logger interface for deletion logging
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// ResourceManagerOptions configures the resource manager behavior
|
||||
type ResourceManagerOptions struct {
|
||||
// Logger for deletion operations
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// DefaultResourceManagerOptions returns sensible defaults
|
||||
func DefaultResourceManagerOptions() ResourceManagerOptions {
|
||||
return ResourceManagerOptions{
|
||||
Logger: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewResourceManager creates a new EdgeConnect resource manager
|
||||
func NewResourceManager(client EdgeConnectClientInterface, opts ...func(*ResourceManagerOptions)) ResourceManagerInterface {
|
||||
options := DefaultResourceManagerOptions()
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return &EdgeConnectResourceManager{
|
||||
client: client,
|
||||
logger: options.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets a logger for deletion operations
|
||||
func WithLogger(logger Logger) func(*ResourceManagerOptions) {
|
||||
return func(opts *ResourceManagerOptions) {
|
||||
opts.Logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteDeletion executes a deletion plan
|
||||
// Important: Instances must be deleted before the app
|
||||
func (rm *EdgeConnectResourceManager) ExecuteDeletion(ctx context.Context, plan *DeletionPlan) (*DeletionResult, error) {
|
||||
startTime := time.Now()
|
||||
rm.logf("Starting deletion: %s", plan.ConfigName)
|
||||
|
||||
result := &DeletionResult{
|
||||
Plan: plan,
|
||||
Success: true,
|
||||
CompletedActions: []DeletionActionResult{},
|
||||
FailedActions: []DeletionActionResult{},
|
||||
}
|
||||
|
||||
// If plan is empty, return success immediately
|
||||
if plan.IsEmpty() {
|
||||
rm.logf("No resources to delete")
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Step 1: Delete all instances first
|
||||
for _, instance := range plan.InstancesToDelete {
|
||||
actionStart := time.Now()
|
||||
rm.logf("Deleting instance: %s", instance.Name)
|
||||
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
Organization: instance.Organization,
|
||||
Name: instance.Name,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
Organization: instance.CloudletOrg,
|
||||
Name: instance.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
err := rm.client.DeleteAppInstance(ctx, instanceKey, instance.Region)
|
||||
actionResult := DeletionActionResult{
|
||||
Type: "instance",
|
||||
Target: instance.Name,
|
||||
Duration: time.Since(actionStart),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rm.logf("Failed to delete instance %s: %v", instance.Name, err)
|
||||
actionResult.Success = false
|
||||
actionResult.Error = err
|
||||
result.FailedActions = append(result.FailedActions, actionResult)
|
||||
result.Success = false
|
||||
result.Error = fmt.Errorf("failed to delete instance %s: %w", instance.Name, err)
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
rm.logf("Successfully deleted instance: %s", instance.Name)
|
||||
actionResult.Success = true
|
||||
result.CompletedActions = append(result.CompletedActions, actionResult)
|
||||
}
|
||||
|
||||
// Step 2: Delete the app (only after all instances are deleted)
|
||||
if plan.AppToDelete != nil {
|
||||
actionStart := time.Now()
|
||||
app := plan.AppToDelete
|
||||
rm.logf("Deleting app: %s version %s", app.Name, app.Version)
|
||||
|
||||
appKey := edgeconnect.AppKey{
|
||||
Organization: app.Organization,
|
||||
Name: app.Name,
|
||||
Version: app.Version,
|
||||
}
|
||||
|
||||
err := rm.client.DeleteApp(ctx, appKey, app.Region)
|
||||
actionResult := DeletionActionResult{
|
||||
Type: "app",
|
||||
Target: fmt.Sprintf("%s:%s", app.Name, app.Version),
|
||||
Duration: time.Since(actionStart),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rm.logf("Failed to delete app %s: %v", app.Name, err)
|
||||
actionResult.Success = false
|
||||
actionResult.Error = err
|
||||
result.FailedActions = append(result.FailedActions, actionResult)
|
||||
result.Success = false
|
||||
result.Error = fmt.Errorf("failed to delete app %s: %w", app.Name, err)
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
rm.logf("Successfully deleted app: %s", app.Name)
|
||||
actionResult.Success = true
|
||||
result.CompletedActions = append(result.CompletedActions, actionResult)
|
||||
}
|
||||
|
||||
result.Duration = time.Since(startTime)
|
||||
rm.logf("Deletion completed successfully in %v", result.Duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// logf logs a message if a logger is configured
|
||||
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
228
internal/delete/v1/planner.go
Normal file
228
internal/delete/v1/planner.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// ABOUTME: Deletion planner for EdgeConnect delete command
|
||||
// ABOUTME: Analyzes current state to identify resources for deletion
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||
type EdgeConnectClientInterface interface {
|
||||
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error)
|
||||
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
|
||||
}
|
||||
|
||||
// Planner defines the interface for deletion planning
|
||||
type Planner interface {
|
||||
// Plan analyzes the configuration and current state to generate a deletion plan
|
||||
Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error)
|
||||
|
||||
// PlanWithOptions allows customization of planning behavior
|
||||
PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error)
|
||||
}
|
||||
|
||||
// PlanOptions provides configuration for the planning process
|
||||
type PlanOptions struct {
|
||||
// DryRun indicates this is a planning-only operation
|
||||
DryRun bool
|
||||
|
||||
// Timeout for API operations
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultPlanOptions returns sensible default planning options
|
||||
func DefaultPlanOptions() PlanOptions {
|
||||
return PlanOptions{
|
||||
DryRun: false,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeConnectPlanner implements the Planner interface for EdgeConnect
|
||||
type EdgeConnectPlanner struct {
|
||||
client EdgeConnectClientInterface
|
||||
}
|
||||
|
||||
// NewPlanner creates a new EdgeConnect deletion planner
|
||||
func NewPlanner(client EdgeConnectClientInterface) Planner {
|
||||
return &EdgeConnectPlanner{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Plan analyzes the configuration and generates a deletion plan
|
||||
func (p *EdgeConnectPlanner) Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error) {
|
||||
return p.PlanWithOptions(ctx, config, DefaultPlanOptions())
|
||||
}
|
||||
|
||||
// PlanWithOptions generates a deletion plan with custom options
|
||||
func (p *EdgeConnectPlanner) PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error) {
|
||||
startTime := time.Now()
|
||||
var warnings []string
|
||||
|
||||
// Create the deletion plan structure
|
||||
plan := &DeletionPlan{
|
||||
ConfigName: config.Metadata.Name,
|
||||
CreatedAt: startTime,
|
||||
DryRun: opts.DryRun,
|
||||
}
|
||||
|
||||
// Get the region from the first infra template
|
||||
region := config.Spec.InfraTemplate[0].Region
|
||||
|
||||
// Step 1: Check if instances exist
|
||||
instancesResult := p.findInstancesToDelete(ctx, config, region)
|
||||
plan.InstancesToDelete = instancesResult.instances
|
||||
if instancesResult.err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("Error querying instances: %v", instancesResult.err))
|
||||
}
|
||||
|
||||
// Step 2: Check if app exists
|
||||
appResult := p.findAppToDelete(ctx, config, region)
|
||||
plan.AppToDelete = appResult.app
|
||||
if appResult.err != nil && !isNotFoundError(appResult.err) {
|
||||
warnings = append(warnings, fmt.Sprintf("Error querying app: %v", appResult.err))
|
||||
}
|
||||
|
||||
// Step 3: Calculate plan metadata
|
||||
p.calculatePlanMetadata(plan)
|
||||
|
||||
// Step 4: Generate summary
|
||||
plan.Summary = plan.GenerateSummary()
|
||||
|
||||
return &PlanResult{
|
||||
Plan: plan,
|
||||
Warnings: warnings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type appQueryResult struct {
|
||||
app *AppDeletion
|
||||
err error
|
||||
}
|
||||
|
||||
type instancesQueryResult struct {
|
||||
instances []InstanceDeletion
|
||||
err error
|
||||
}
|
||||
|
||||
// findAppToDelete checks if the app exists and should be deleted
|
||||
func (p *EdgeConnectPlanner) findAppToDelete(ctx context.Context, config *config.EdgeConnectConfig, region string) appQueryResult {
|
||||
appKey := edgeconnect.AppKey{
|
||||
Organization: config.Metadata.Organization,
|
||||
Name: config.Metadata.Name,
|
||||
Version: config.Metadata.AppVersion,
|
||||
}
|
||||
|
||||
app, err := p.client.ShowApp(ctx, appKey, region)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return appQueryResult{app: nil, err: nil}
|
||||
}
|
||||
return appQueryResult{app: nil, err: err}
|
||||
}
|
||||
|
||||
return appQueryResult{
|
||||
app: &AppDeletion{
|
||||
Name: app.Key.Name,
|
||||
Version: app.Key.Version,
|
||||
Organization: app.Key.Organization,
|
||||
Region: region,
|
||||
},
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// findInstancesToDelete finds all instances that match the config
|
||||
func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *config.EdgeConnectConfig, region string) instancesQueryResult {
|
||||
var allInstances []InstanceDeletion
|
||||
|
||||
// Query instances for each infra template
|
||||
for _, infra := range config.Spec.InfraTemplate {
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
Organization: config.Metadata.Organization,
|
||||
Name: generateInstanceName(config.Metadata.Name, config.Metadata.AppVersion),
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
Organization: infra.CloudletOrg,
|
||||
Name: infra.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
|
||||
if err != nil {
|
||||
// If it's a not found error, just continue
|
||||
if isNotFoundError(err) {
|
||||
continue
|
||||
}
|
||||
return instancesQueryResult{instances: nil, err: err}
|
||||
}
|
||||
|
||||
// Add found instances to the list
|
||||
for _, inst := range instances {
|
||||
allInstances = append(allInstances, InstanceDeletion{
|
||||
Name: inst.Key.Name,
|
||||
Organization: inst.Key.Organization,
|
||||
Region: infra.Region,
|
||||
CloudletOrg: inst.Key.CloudletKey.Organization,
|
||||
CloudletName: inst.Key.CloudletKey.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return instancesQueryResult{
|
||||
instances: allInstances,
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// calculatePlanMetadata calculates the total actions and estimated duration
|
||||
func (p *EdgeConnectPlanner) calculatePlanMetadata(plan *DeletionPlan) {
|
||||
totalActions := 0
|
||||
|
||||
if plan.AppToDelete != nil {
|
||||
totalActions++
|
||||
}
|
||||
|
||||
totalActions += len(plan.InstancesToDelete)
|
||||
|
||||
plan.TotalActions = totalActions
|
||||
|
||||
// Estimate duration: ~5 seconds per instance, ~3 seconds for app
|
||||
estimatedSeconds := len(plan.InstancesToDelete) * 5
|
||||
if plan.AppToDelete != nil {
|
||||
estimatedSeconds += 3
|
||||
}
|
||||
plan.EstimatedDuration = time.Duration(estimatedSeconds) * time.Second
|
||||
}
|
||||
|
||||
// generateInstanceName creates an instance name from app name and version
|
||||
func generateInstanceName(appName, appVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
|
||||
}
|
||||
|
||||
// isNotFoundError checks if an error is a 404 not found error
|
||||
func isNotFoundError(err error) bool {
|
||||
if apiErr, ok := err.(*edgeconnect.APIError); ok {
|
||||
return apiErr.StatusCode == 404
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PlanResult represents the result of a deletion planning operation
|
||||
type PlanResult struct {
|
||||
// Plan is the generated deletion plan
|
||||
Plan *DeletionPlan
|
||||
|
||||
// Error if planning failed
|
||||
Error error
|
||||
|
||||
// Warnings encountered during planning
|
||||
Warnings []string
|
||||
}
|
||||
157
internal/delete/v1/types.go
Normal file
157
internal/delete/v1/types.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// ABOUTME: Deletion planning types for EdgeConnect delete command
|
||||
// ABOUTME: Defines structures for deletion plans and deletion results
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeletionPlan represents the complete deletion plan for a configuration
|
||||
type DeletionPlan struct {
|
||||
// ConfigName is the name from metadata
|
||||
ConfigName string
|
||||
|
||||
// AppToDelete defines the app that will be deleted (nil if app doesn't exist)
|
||||
AppToDelete *AppDeletion
|
||||
|
||||
// InstancesToDelete defines the instances that will be deleted
|
||||
InstancesToDelete []InstanceDeletion
|
||||
|
||||
// Summary provides a human-readable summary of the plan
|
||||
Summary string
|
||||
|
||||
// TotalActions is the count of all actions that will be performed
|
||||
TotalActions int
|
||||
|
||||
// EstimatedDuration is the estimated time to complete the deletion
|
||||
EstimatedDuration time.Duration
|
||||
|
||||
// CreatedAt timestamp when the plan was created
|
||||
CreatedAt time.Time
|
||||
|
||||
// DryRun indicates if this is a dry-run plan
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// AppDeletion represents an application to be deleted
|
||||
type AppDeletion struct {
|
||||
// Name of the application
|
||||
Name string
|
||||
|
||||
// Version of the application
|
||||
Version string
|
||||
|
||||
// Organization that owns the app
|
||||
Organization string
|
||||
|
||||
// Region where the app is deployed
|
||||
Region string
|
||||
}
|
||||
|
||||
// InstanceDeletion represents an application instance to be deleted
|
||||
type InstanceDeletion struct {
|
||||
// Name of the instance
|
||||
Name string
|
||||
|
||||
// Organization that owns the instance
|
||||
Organization string
|
||||
|
||||
// Region where the instance is deployed
|
||||
Region string
|
||||
|
||||
// CloudletOrg that hosts the cloudlet
|
||||
CloudletOrg string
|
||||
|
||||
// CloudletName where the instance is running
|
||||
CloudletName string
|
||||
}
|
||||
|
||||
// DeletionResult represents the result of a deletion operation
|
||||
type DeletionResult struct {
|
||||
// Plan that was executed
|
||||
Plan *DeletionPlan
|
||||
|
||||
// Success indicates if the deletion was successful
|
||||
Success bool
|
||||
|
||||
// CompletedActions lists actions that were successfully completed
|
||||
CompletedActions []DeletionActionResult
|
||||
|
||||
// FailedActions lists actions that failed
|
||||
FailedActions []DeletionActionResult
|
||||
|
||||
// Error that caused the deletion to fail (if any)
|
||||
Error error
|
||||
|
||||
// Duration taken to execute the plan
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// DeletionActionResult represents the result of executing a single deletion action
|
||||
type DeletionActionResult struct {
|
||||
// Type of resource that was deleted ("app" or "instance")
|
||||
Type string
|
||||
|
||||
// Target describes what was being deleted
|
||||
Target string
|
||||
|
||||
// Success indicates if the action succeeded
|
||||
Success bool
|
||||
|
||||
// Error if the action failed
|
||||
Error error
|
||||
|
||||
// Duration taken to complete the action
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the deletion plan has no actions to perform
|
||||
func (dp *DeletionPlan) IsEmpty() bool {
|
||||
return dp.AppToDelete == nil && len(dp.InstancesToDelete) == 0
|
||||
}
|
||||
|
||||
// GenerateSummary creates a human-readable summary of the deletion plan
|
||||
func (dp *DeletionPlan) GenerateSummary() string {
|
||||
if dp.IsEmpty() {
|
||||
return "No resources found to delete"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Deletion plan for '%s':\n", dp.ConfigName))
|
||||
|
||||
// Instance actions
|
||||
if len(dp.InstancesToDelete) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- DELETE %d instance(s)\n", len(dp.InstancesToDelete)))
|
||||
cloudletSet := make(map[string]bool)
|
||||
for _, inst := range dp.InstancesToDelete {
|
||||
key := fmt.Sprintf("%s:%s", inst.CloudletOrg, inst.CloudletName)
|
||||
cloudletSet[key] = true
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" Across %d cloudlet(s)\n", len(cloudletSet)))
|
||||
}
|
||||
|
||||
// App action
|
||||
if dp.AppToDelete != nil {
|
||||
sb.WriteString(fmt.Sprintf("- DELETE application '%s' version %s\n",
|
||||
dp.AppToDelete.Name, dp.AppToDelete.Version))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("Estimated duration: %s", dp.EstimatedDuration.String()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Validate checks if the deletion plan is valid
|
||||
func (dp *DeletionPlan) Validate() error {
|
||||
if dp.ConfigName == "" {
|
||||
return fmt.Errorf("deletion plan must have a config name")
|
||||
}
|
||||
|
||||
if dp.IsEmpty() {
|
||||
return fmt.Errorf("deletion plan has no resources to delete")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
166
internal/delete/v2/manager.go
Normal file
166
internal/delete/v2/manager.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// ABOUTME: Resource management for EdgeConnect delete command with deletion execution
|
||||
// ABOUTME: Handles actual deletion operations with proper ordering (instances first, then app)
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
type ResourceManagerInterface interface {
|
||||
// ExecuteDeletion executes a deletion plan
|
||||
ExecuteDeletion(ctx context.Context, plan *DeletionPlan) (*DeletionResult, error)
|
||||
}
|
||||
|
||||
// EdgeConnectResourceManager implements resource management for EdgeConnect
|
||||
type EdgeConnectResourceManager struct {
|
||||
client EdgeConnectClientInterface
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// Logger interface for deletion logging
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// ResourceManagerOptions configures the resource manager behavior
|
||||
type ResourceManagerOptions struct {
|
||||
// Logger for deletion operations
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// DefaultResourceManagerOptions returns sensible defaults
|
||||
func DefaultResourceManagerOptions() ResourceManagerOptions {
|
||||
return ResourceManagerOptions{
|
||||
Logger: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewResourceManager creates a new EdgeConnect resource manager
|
||||
func NewResourceManager(client EdgeConnectClientInterface, opts ...func(*ResourceManagerOptions)) ResourceManagerInterface {
|
||||
options := DefaultResourceManagerOptions()
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return &EdgeConnectResourceManager{
|
||||
client: client,
|
||||
logger: options.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets a logger for deletion operations
|
||||
func WithLogger(logger Logger) func(*ResourceManagerOptions) {
|
||||
return func(opts *ResourceManagerOptions) {
|
||||
opts.Logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteDeletion executes a deletion plan
|
||||
// Important: Instances must be deleted before the app
|
||||
func (rm *EdgeConnectResourceManager) ExecuteDeletion(ctx context.Context, plan *DeletionPlan) (*DeletionResult, error) {
|
||||
startTime := time.Now()
|
||||
rm.logf("Starting deletion: %s", plan.ConfigName)
|
||||
|
||||
result := &DeletionResult{
|
||||
Plan: plan,
|
||||
Success: true,
|
||||
CompletedActions: []DeletionActionResult{},
|
||||
FailedActions: []DeletionActionResult{},
|
||||
}
|
||||
|
||||
// If plan is empty, return success immediately
|
||||
if plan.IsEmpty() {
|
||||
rm.logf("No resources to delete")
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Step 1: Delete all instances first
|
||||
for _, instance := range plan.InstancesToDelete {
|
||||
actionStart := time.Now()
|
||||
rm.logf("Deleting instance: %s", instance.Name)
|
||||
|
||||
instanceKey := v2.AppInstanceKey{
|
||||
Organization: instance.Organization,
|
||||
Name: instance.Name,
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: instance.CloudletOrg,
|
||||
Name: instance.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
err := rm.client.DeleteAppInstance(ctx, instanceKey, instance.Region)
|
||||
actionResult := DeletionActionResult{
|
||||
Type: "instance",
|
||||
Target: instance.Name,
|
||||
Duration: time.Since(actionStart),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rm.logf("Failed to delete instance %s: %v", instance.Name, err)
|
||||
actionResult.Success = false
|
||||
actionResult.Error = err
|
||||
result.FailedActions = append(result.FailedActions, actionResult)
|
||||
result.Success = false
|
||||
result.Error = fmt.Errorf("failed to delete instance %s: %w", instance.Name, err)
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
rm.logf("Successfully deleted instance: %s", instance.Name)
|
||||
actionResult.Success = true
|
||||
result.CompletedActions = append(result.CompletedActions, actionResult)
|
||||
}
|
||||
|
||||
// Step 2: Delete the app (only after all instances are deleted)
|
||||
if plan.AppToDelete != nil {
|
||||
actionStart := time.Now()
|
||||
app := plan.AppToDelete
|
||||
rm.logf("Deleting app: %s version %s", app.Name, app.Version)
|
||||
|
||||
appKey := v2.AppKey{
|
||||
Organization: app.Organization,
|
||||
Name: app.Name,
|
||||
Version: app.Version,
|
||||
}
|
||||
|
||||
err := rm.client.DeleteApp(ctx, appKey, app.Region)
|
||||
actionResult := DeletionActionResult{
|
||||
Type: "app",
|
||||
Target: fmt.Sprintf("%s:%s", app.Name, app.Version),
|
||||
Duration: time.Since(actionStart),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rm.logf("Failed to delete app %s: %v", app.Name, err)
|
||||
actionResult.Success = false
|
||||
actionResult.Error = err
|
||||
result.FailedActions = append(result.FailedActions, actionResult)
|
||||
result.Success = false
|
||||
result.Error = fmt.Errorf("failed to delete app %s: %w", app.Name, err)
|
||||
result.Duration = time.Since(startTime)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
rm.logf("Successfully deleted app: %s", app.Name)
|
||||
actionResult.Success = true
|
||||
result.CompletedActions = append(result.CompletedActions, actionResult)
|
||||
}
|
||||
|
||||
result.Duration = time.Since(startTime)
|
||||
rm.logf("Deletion completed successfully in %v", result.Duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// logf logs a message if a logger is configured
|
||||
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
200
internal/delete/v2/manager_test.go
Normal file
200
internal/delete/v2/manager_test.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// ABOUTME: Tests for EdgeConnect deletion manager with mock scenarios
|
||||
// ABOUTME: Tests deletion execution and error handling with mock clients
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockResourceClient for testing deletion manager
|
||||
type MockResourceClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockResourceClient) ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error) {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return v2.App{}, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(v2.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]v2.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// TestLogger implements Logger interface for testing
|
||||
type TestLogger struct {
|
||||
messages []string
|
||||
}
|
||||
|
||||
func (l *TestLogger) Printf(format string, v ...interface{}) {
|
||||
l.messages = append(l.messages, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func TestNewResourceManager(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
manager := NewResourceManager(mockClient)
|
||||
|
||||
assert.NotNil(t, manager)
|
||||
}
|
||||
|
||||
func TestWithLogger(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
logger := &TestLogger{}
|
||||
|
||||
manager := NewResourceManager(mockClient, WithLogger(logger))
|
||||
|
||||
// Cast to implementation to check logger was set
|
||||
impl := manager.(*EdgeConnectResourceManager)
|
||||
assert.Equal(t, logger, impl.logger)
|
||||
}
|
||||
|
||||
func createTestDeletionPlan() *DeletionPlan {
|
||||
return &DeletionPlan{
|
||||
ConfigName: "test-deletion",
|
||||
AppToDelete: &AppDeletion{
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
Organization: "testorg",
|
||||
Region: "US",
|
||||
},
|
||||
InstancesToDelete: []InstanceDeletion{
|
||||
{
|
||||
Name: "test-app-1.0.0-instance",
|
||||
Organization: "testorg",
|
||||
Region: "US",
|
||||
CloudletOrg: "cloudletorg",
|
||||
CloudletName: "cloudlet1",
|
||||
},
|
||||
},
|
||||
TotalActions: 2,
|
||||
EstimatedDuration: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteDeletion_Success(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
logger := &TestLogger{}
|
||||
manager := NewResourceManager(mockClient, WithLogger(logger))
|
||||
|
||||
plan := createTestDeletionPlan()
|
||||
|
||||
// Mock successful deletion operations
|
||||
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return(nil)
|
||||
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||
Return(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := manager.ExecuteDeletion(ctx, plan)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.True(t, result.Success)
|
||||
assert.Len(t, result.CompletedActions, 2) // 1 instance + 1 app
|
||||
assert.Len(t, result.FailedActions, 0)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestExecuteDeletion_InstanceDeleteFails(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
logger := &TestLogger{}
|
||||
manager := NewResourceManager(mockClient, WithLogger(logger))
|
||||
|
||||
plan := createTestDeletionPlan()
|
||||
|
||||
// Mock instance deletion failure
|
||||
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return(fmt.Errorf("instance deletion failed"))
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := manager.ExecuteDeletion(ctx, plan)
|
||||
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.False(t, result.Success)
|
||||
assert.Len(t, result.FailedActions, 1)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestExecuteDeletion_OnlyInstances(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
logger := &TestLogger{}
|
||||
manager := NewResourceManager(mockClient, WithLogger(logger))
|
||||
|
||||
plan := &DeletionPlan{
|
||||
ConfigName: "test-deletion",
|
||||
AppToDelete: nil, // No app to delete
|
||||
InstancesToDelete: []InstanceDeletion{
|
||||
{
|
||||
Name: "test-app-1.0.0-instance",
|
||||
Organization: "testorg",
|
||||
Region: "US",
|
||||
CloudletOrg: "cloudletorg",
|
||||
CloudletName: "cloudlet1",
|
||||
},
|
||||
},
|
||||
TotalActions: 1,
|
||||
EstimatedDuration: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Mock successful instance deletion
|
||||
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := manager.ExecuteDeletion(ctx, plan)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.True(t, result.Success)
|
||||
assert.Len(t, result.CompletedActions, 1)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestExecuteDeletion_EmptyPlan(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
manager := NewResourceManager(mockClient)
|
||||
|
||||
plan := &DeletionPlan{
|
||||
ConfigName: "test-deletion",
|
||||
AppToDelete: nil,
|
||||
InstancesToDelete: []InstanceDeletion{},
|
||||
TotalActions: 0,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := manager.ExecuteDeletion(ctx, plan)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.True(t, result.Success)
|
||||
assert.Len(t, result.CompletedActions, 0)
|
||||
assert.Len(t, result.FailedActions, 0)
|
||||
}
|
||||
228
internal/delete/v2/planner.go
Normal file
228
internal/delete/v2/planner.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// ABOUTME: Deletion planner for EdgeConnect delete command
|
||||
// ABOUTME: Analyzes current state to identify resources for deletion
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||
type EdgeConnectClientInterface interface {
|
||||
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error)
|
||||
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
|
||||
}
|
||||
|
||||
// Planner defines the interface for deletion planning
|
||||
type Planner interface {
|
||||
// Plan analyzes the configuration and current state to generate a deletion plan
|
||||
Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error)
|
||||
|
||||
// PlanWithOptions allows customization of planning behavior
|
||||
PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error)
|
||||
}
|
||||
|
||||
// PlanOptions provides configuration for the planning process
|
||||
type PlanOptions struct {
|
||||
// DryRun indicates this is a planning-only operation
|
||||
DryRun bool
|
||||
|
||||
// Timeout for API operations
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultPlanOptions returns sensible default planning options
|
||||
func DefaultPlanOptions() PlanOptions {
|
||||
return PlanOptions{
|
||||
DryRun: false,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeConnectPlanner implements the Planner interface for EdgeConnect
|
||||
type EdgeConnectPlanner struct {
|
||||
client EdgeConnectClientInterface
|
||||
}
|
||||
|
||||
// NewPlanner creates a new EdgeConnect deletion planner
|
||||
func NewPlanner(client EdgeConnectClientInterface) Planner {
|
||||
return &EdgeConnectPlanner{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Plan analyzes the configuration and generates a deletion plan
|
||||
func (p *EdgeConnectPlanner) Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error) {
|
||||
return p.PlanWithOptions(ctx, config, DefaultPlanOptions())
|
||||
}
|
||||
|
||||
// PlanWithOptions generates a deletion plan with custom options
|
||||
func (p *EdgeConnectPlanner) PlanWithOptions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*PlanResult, error) {
|
||||
startTime := time.Now()
|
||||
var warnings []string
|
||||
|
||||
// Create the deletion plan structure
|
||||
plan := &DeletionPlan{
|
||||
ConfigName: config.Metadata.Name,
|
||||
CreatedAt: startTime,
|
||||
DryRun: opts.DryRun,
|
||||
}
|
||||
|
||||
// Get the region from the first infra template
|
||||
region := config.Spec.InfraTemplate[0].Region
|
||||
|
||||
// Step 1: Check if instances exist
|
||||
instancesResult := p.findInstancesToDelete(ctx, config, region)
|
||||
plan.InstancesToDelete = instancesResult.instances
|
||||
if instancesResult.err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("Error querying instances: %v", instancesResult.err))
|
||||
}
|
||||
|
||||
// Step 2: Check if app exists
|
||||
appResult := p.findAppToDelete(ctx, config, region)
|
||||
plan.AppToDelete = appResult.app
|
||||
if appResult.err != nil && !isNotFoundError(appResult.err) {
|
||||
warnings = append(warnings, fmt.Sprintf("Error querying app: %v", appResult.err))
|
||||
}
|
||||
|
||||
// Step 3: Calculate plan metadata
|
||||
p.calculatePlanMetadata(plan)
|
||||
|
||||
// Step 4: Generate summary
|
||||
plan.Summary = plan.GenerateSummary()
|
||||
|
||||
return &PlanResult{
|
||||
Plan: plan,
|
||||
Warnings: warnings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type appQueryResult struct {
|
||||
app *AppDeletion
|
||||
err error
|
||||
}
|
||||
|
||||
type instancesQueryResult struct {
|
||||
instances []InstanceDeletion
|
||||
err error
|
||||
}
|
||||
|
||||
// findAppToDelete checks if the app exists and should be deleted
|
||||
func (p *EdgeConnectPlanner) findAppToDelete(ctx context.Context, config *config.EdgeConnectConfig, region string) appQueryResult {
|
||||
appKey := v2.AppKey{
|
||||
Organization: config.Metadata.Organization,
|
||||
Name: config.Metadata.Name,
|
||||
Version: config.Metadata.AppVersion,
|
||||
}
|
||||
|
||||
app, err := p.client.ShowApp(ctx, appKey, region)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return appQueryResult{app: nil, err: nil}
|
||||
}
|
||||
return appQueryResult{app: nil, err: err}
|
||||
}
|
||||
|
||||
return appQueryResult{
|
||||
app: &AppDeletion{
|
||||
Name: app.Key.Name,
|
||||
Version: app.Key.Version,
|
||||
Organization: app.Key.Organization,
|
||||
Region: region,
|
||||
},
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// findInstancesToDelete finds all instances that match the config
|
||||
func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *config.EdgeConnectConfig, region string) instancesQueryResult {
|
||||
var allInstances []InstanceDeletion
|
||||
|
||||
// Query instances for each infra template
|
||||
for _, infra := range config.Spec.InfraTemplate {
|
||||
instanceKey := v2.AppInstanceKey{
|
||||
Organization: config.Metadata.Organization,
|
||||
Name: generateInstanceName(config.Metadata.Name, config.Metadata.AppVersion),
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: infra.CloudletOrg,
|
||||
Name: infra.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
|
||||
if err != nil {
|
||||
// If it's a not found error, just continue
|
||||
if isNotFoundError(err) {
|
||||
continue
|
||||
}
|
||||
return instancesQueryResult{instances: nil, err: err}
|
||||
}
|
||||
|
||||
// Add found instances to the list
|
||||
for _, inst := range instances {
|
||||
allInstances = append(allInstances, InstanceDeletion{
|
||||
Name: inst.Key.Name,
|
||||
Organization: inst.Key.Organization,
|
||||
Region: infra.Region,
|
||||
CloudletOrg: inst.Key.CloudletKey.Organization,
|
||||
CloudletName: inst.Key.CloudletKey.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return instancesQueryResult{
|
||||
instances: allInstances,
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// calculatePlanMetadata calculates the total actions and estimated duration
|
||||
func (p *EdgeConnectPlanner) calculatePlanMetadata(plan *DeletionPlan) {
|
||||
totalActions := 0
|
||||
|
||||
if plan.AppToDelete != nil {
|
||||
totalActions++
|
||||
}
|
||||
|
||||
totalActions += len(plan.InstancesToDelete)
|
||||
|
||||
plan.TotalActions = totalActions
|
||||
|
||||
// Estimate duration: ~5 seconds per instance, ~3 seconds for app
|
||||
estimatedSeconds := len(plan.InstancesToDelete) * 5
|
||||
if plan.AppToDelete != nil {
|
||||
estimatedSeconds += 3
|
||||
}
|
||||
plan.EstimatedDuration = time.Duration(estimatedSeconds) * time.Second
|
||||
}
|
||||
|
||||
// generateInstanceName creates an instance name from app name and version
|
||||
func generateInstanceName(appName, appVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
|
||||
}
|
||||
|
||||
// isNotFoundError checks if an error is a 404 not found error
|
||||
func isNotFoundError(err error) bool {
|
||||
if apiErr, ok := err.(*v2.APIError); ok {
|
||||
return apiErr.StatusCode == 404
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PlanResult represents the result of a deletion planning operation
|
||||
type PlanResult struct {
|
||||
// Plan is the generated deletion plan
|
||||
Plan *DeletionPlan
|
||||
|
||||
// Error if planning failed
|
||||
Error error
|
||||
|
||||
// Warnings encountered during planning
|
||||
Warnings []string
|
||||
}
|
||||
219
internal/delete/v2/planner_test.go
Normal file
219
internal/delete/v2/planner_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// ABOUTME: Tests for EdgeConnect deletion planner with mock scenarios
|
||||
// ABOUTME: Tests deletion planning logic and resource discovery
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockEdgeConnectClient is a mock implementation of the EdgeConnect client
|
||||
type MockEdgeConnectClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error) {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return v2.App{}, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(v2.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]v2.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func createTestConfig(t *testing.T) *config.EdgeConnectConfig {
|
||||
// Create temporary manifest file
|
||||
tempDir := t.TempDir()
|
||||
manifestFile := filepath.Join(tempDir, "test-manifest.yaml")
|
||||
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
||||
err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &config.EdgeConnectConfig{
|
||||
Kind: "edgeconnect-deployment",
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "testorg",
|
||||
},
|
||||
Spec: config.Spec{
|
||||
K8sApp: &config.K8sApp{
|
||||
ManifestFile: manifestFile,
|
||||
},
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "US",
|
||||
CloudletOrg: "TestCloudletOrg",
|
||||
CloudletName: "TestCloudlet",
|
||||
FlavorName: "small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPlanner(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
|
||||
assert.NotNil(t, planner)
|
||||
}
|
||||
|
||||
func TestPlanDeletion_WithExistingResources(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Mock existing app
|
||||
existingApp := v2.App{
|
||||
Key: v2.AppKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
}
|
||||
|
||||
// Mock existing instances
|
||||
existingInstances := []v2.AppInstance{
|
||||
{
|
||||
Key: v2.AppInstanceKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: "TestCloudletOrg",
|
||||
Name: "TestCloudlet",
|
||||
},
|
||||
},
|
||||
AppKey: v2.AppKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||
Return(existingApp, nil)
|
||||
|
||||
mockClient.On("ShowAppInstances", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return(existingInstances, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Equal(t, "test-app", plan.ConfigName)
|
||||
assert.NotNil(t, plan.AppToDelete)
|
||||
assert.Equal(t, "test-app", plan.AppToDelete.Name)
|
||||
assert.Equal(t, "1.0.0", plan.AppToDelete.Version)
|
||||
assert.Equal(t, "testorg", plan.AppToDelete.Organization)
|
||||
|
||||
require.Len(t, plan.InstancesToDelete, 1)
|
||||
assert.Equal(t, "test-app-1.0.0-instance", plan.InstancesToDelete[0].Name)
|
||||
assert.Equal(t, "testorg", plan.InstancesToDelete[0].Organization)
|
||||
|
||||
assert.Equal(t, 2, plan.TotalActions) // 1 app + 1 instance
|
||||
assert.False(t, plan.IsEmpty())
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanDeletion_NoResourcesExist(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Mock API calls to return "not found" errors
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||
Return(v2.App{}, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||
|
||||
mockClient.On("ShowAppInstances", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return([]v2.AppInstance{}, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Equal(t, "test-app", plan.ConfigName)
|
||||
assert.Nil(t, plan.AppToDelete)
|
||||
assert.Len(t, plan.InstancesToDelete, 0)
|
||||
assert.Equal(t, 0, plan.TotalActions)
|
||||
assert.True(t, plan.IsEmpty())
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanDeletion_OnlyInstancesExist(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Mock existing instances but no app
|
||||
existingInstances := []v2.AppInstance{
|
||||
{
|
||||
Key: v2.AppInstanceKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: "TestCloudletOrg",
|
||||
Name: "TestCloudlet",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||
Return(v2.App{}, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||
|
||||
mockClient.On("ShowAppInstances", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||
Return(existingInstances, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Nil(t, plan.AppToDelete)
|
||||
assert.Len(t, plan.InstancesToDelete, 1)
|
||||
assert.Equal(t, 1, plan.TotalActions)
|
||||
assert.False(t, plan.IsEmpty())
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
157
internal/delete/v2/types.go
Normal file
157
internal/delete/v2/types.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// ABOUTME: Deletion planning types for EdgeConnect delete command
|
||||
// ABOUTME: Defines structures for deletion plans and deletion results
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeletionPlan represents the complete deletion plan for a configuration
|
||||
type DeletionPlan struct {
|
||||
// ConfigName is the name from metadata
|
||||
ConfigName string
|
||||
|
||||
// AppToDelete defines the app that will be deleted (nil if app doesn't exist)
|
||||
AppToDelete *AppDeletion
|
||||
|
||||
// InstancesToDelete defines the instances that will be deleted
|
||||
InstancesToDelete []InstanceDeletion
|
||||
|
||||
// Summary provides a human-readable summary of the plan
|
||||
Summary string
|
||||
|
||||
// TotalActions is the count of all actions that will be performed
|
||||
TotalActions int
|
||||
|
||||
// EstimatedDuration is the estimated time to complete the deletion
|
||||
EstimatedDuration time.Duration
|
||||
|
||||
// CreatedAt timestamp when the plan was created
|
||||
CreatedAt time.Time
|
||||
|
||||
// DryRun indicates if this is a dry-run plan
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// AppDeletion represents an application to be deleted
|
||||
type AppDeletion struct {
|
||||
// Name of the application
|
||||
Name string
|
||||
|
||||
// Version of the application
|
||||
Version string
|
||||
|
||||
// Organization that owns the app
|
||||
Organization string
|
||||
|
||||
// Region where the app is deployed
|
||||
Region string
|
||||
}
|
||||
|
||||
// InstanceDeletion represents an application instance to be deleted
|
||||
type InstanceDeletion struct {
|
||||
// Name of the instance
|
||||
Name string
|
||||
|
||||
// Organization that owns the instance
|
||||
Organization string
|
||||
|
||||
// Region where the instance is deployed
|
||||
Region string
|
||||
|
||||
// CloudletOrg that hosts the cloudlet
|
||||
CloudletOrg string
|
||||
|
||||
// CloudletName where the instance is running
|
||||
CloudletName string
|
||||
}
|
||||
|
||||
// DeletionResult represents the result of a deletion operation
|
||||
type DeletionResult struct {
|
||||
// Plan that was executed
|
||||
Plan *DeletionPlan
|
||||
|
||||
// Success indicates if the deletion was successful
|
||||
Success bool
|
||||
|
||||
// CompletedActions lists actions that were successfully completed
|
||||
CompletedActions []DeletionActionResult
|
||||
|
||||
// FailedActions lists actions that failed
|
||||
FailedActions []DeletionActionResult
|
||||
|
||||
// Error that caused the deletion to fail (if any)
|
||||
Error error
|
||||
|
||||
// Duration taken to execute the plan
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// DeletionActionResult represents the result of executing a single deletion action
|
||||
type DeletionActionResult struct {
|
||||
// Type of resource that was deleted ("app" or "instance")
|
||||
Type string
|
||||
|
||||
// Target describes what was being deleted
|
||||
Target string
|
||||
|
||||
// Success indicates if the action succeeded
|
||||
Success bool
|
||||
|
||||
// Error if the action failed
|
||||
Error error
|
||||
|
||||
// Duration taken to complete the action
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the deletion plan has no actions to perform
|
||||
func (dp *DeletionPlan) IsEmpty() bool {
|
||||
return dp.AppToDelete == nil && len(dp.InstancesToDelete) == 0
|
||||
}
|
||||
|
||||
// GenerateSummary creates a human-readable summary of the deletion plan
|
||||
func (dp *DeletionPlan) GenerateSummary() string {
|
||||
if dp.IsEmpty() {
|
||||
return "No resources found to delete"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Deletion plan for '%s':\n", dp.ConfigName))
|
||||
|
||||
// Instance actions
|
||||
if len(dp.InstancesToDelete) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- DELETE %d instance(s)\n", len(dp.InstancesToDelete)))
|
||||
cloudletSet := make(map[string]bool)
|
||||
for _, inst := range dp.InstancesToDelete {
|
||||
key := fmt.Sprintf("%s:%s", inst.CloudletOrg, inst.CloudletName)
|
||||
cloudletSet[key] = true
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" Across %d cloudlet(s)\n", len(cloudletSet)))
|
||||
}
|
||||
|
||||
// App action
|
||||
if dp.AppToDelete != nil {
|
||||
sb.WriteString(fmt.Sprintf("- DELETE application '%s' version %s\n",
|
||||
dp.AppToDelete.Name, dp.AppToDelete.Version))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("Estimated duration: %s", dp.EstimatedDuration.String()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Validate checks if the deletion plan is valid
|
||||
func (dp *DeletionPlan) Validate() error {
|
||||
if dp.ConfigName == "" {
|
||||
return fmt.Errorf("deletion plan must have a config name")
|
||||
}
|
||||
|
||||
if dp.IsEmpty() {
|
||||
return fmt.Errorf("deletion plan has no resources to delete")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
95
internal/delete/v2/types_test.go
Normal file
95
internal/delete/v2/types_test.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeletionPlan_IsEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plan *DeletionPlan
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty plan with no resources",
|
||||
plan: &DeletionPlan{
|
||||
ConfigName: "test-config",
|
||||
AppToDelete: nil,
|
||||
InstancesToDelete: []InstanceDeletion{},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "plan with app deletion",
|
||||
plan: &DeletionPlan{
|
||||
ConfigName: "test-config",
|
||||
AppToDelete: &AppDeletion{
|
||||
Name: "test-app",
|
||||
Organization: "test-org",
|
||||
Version: "1.0",
|
||||
Region: "US",
|
||||
},
|
||||
InstancesToDelete: []InstanceDeletion{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "plan with instance deletion",
|
||||
plan: &DeletionPlan{
|
||||
ConfigName: "test-config",
|
||||
AppToDelete: nil,
|
||||
InstancesToDelete: []InstanceDeletion{
|
||||
{
|
||||
Name: "test-instance",
|
||||
Organization: "test-org",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.plan.IsEmpty()
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletionPlan_GenerateSummary(t *testing.T) {
|
||||
plan := &DeletionPlan{
|
||||
ConfigName: "test-config",
|
||||
AppToDelete: &AppDeletion{
|
||||
Name: "test-app",
|
||||
Organization: "test-org",
|
||||
Version: "1.0",
|
||||
Region: "US",
|
||||
},
|
||||
InstancesToDelete: []InstanceDeletion{
|
||||
{
|
||||
Name: "test-instance-1",
|
||||
Organization: "test-org",
|
||||
CloudletName: "cloudlet-1",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
},
|
||||
{
|
||||
Name: "test-instance-2",
|
||||
Organization: "test-org",
|
||||
CloudletName: "cloudlet-2",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
},
|
||||
},
|
||||
TotalActions: 3,
|
||||
EstimatedDuration: 30 * time.Second,
|
||||
}
|
||||
|
||||
summary := plan.GenerateSummary()
|
||||
|
||||
assert.Contains(t, summary, "test-config")
|
||||
assert.Contains(t, summary, "DELETE application 'test-app'")
|
||||
assert.Contains(t, summary, "DELETE 2 instance(s)")
|
||||
}
|
||||
|
|
@ -173,8 +173,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
|||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst"
|
||||
|
||||
input := DeleteAppInstanceInput{
|
||||
Key: appInstKey,
|
||||
Region: region,
|
||||
}
|
||||
input.AppInst.Key = appInstKey
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -144,9 +144,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
|||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
|
||||
|
||||
input := DeleteAppInput{
|
||||
Key: appKey,
|
||||
Region: region,
|
||||
}
|
||||
input.App.Key = appKey
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -273,13 +273,18 @@ type UpdateAppInstanceInput struct {
|
|||
|
||||
// DeleteAppInput represents input for deleting an application
|
||||
type DeleteAppInput struct {
|
||||
Key AppKey `json:"key"`
|
||||
Region string `json:"region"`
|
||||
App struct {
|
||||
Key AppKey `json:"key"`
|
||||
} `json:"app"`
|
||||
}
|
||||
|
||||
// DeleteAppInstanceInput represents input for deleting an app instance
|
||||
type DeleteAppInstanceInput struct {
|
||||
Key AppInstanceKey `json:"key"`
|
||||
Region string `json:"region"`
|
||||
AppInst struct {
|
||||
Key AppInstanceKey `json:"key"`
|
||||
} `json:"appinst"`
|
||||
}
|
||||
|
||||
// Response wrapper types
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue