All checks were successful
test / test (push) Successful in 48s
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>
228 lines
6.7 KiB
Go
228 lines
6.7 KiB
Go
// 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
|
|
}
|