edge-connect-client/internal/delete/v1/planner.go
Patrick Sy ece2955a2a
All checks were successful
test / test (push) Successful in 56s
ci / goreleaser (push) Successful in 54s
feat(api): Added AppKey to ShowAppInstances
2025-11-13 16:59:38 +01:00

229 lines
6.9 KiB
Go

// 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, appKey edgeconnect.AppKey, 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,
},
}
appKey := edgeconnect.AppKey{Name: config.Metadata.Name}
instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, 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
}