// 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/v2/internal/config" "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/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 }