Moves the `domain` and `ports` packages from `internal/core` to `internal`. This refactoring simplifies the directory structure by elevating the core architectural concepts of domain and ports to the top level of the `internal` directory. The `core` directory is now removed as its only purpose was to house these two packages. All import paths across the project have been updated to reflect this change.
289 lines
No EOL
9.5 KiB
Go
289 lines
No EOL
9.5 KiB
Go
// ABOUTME: Resource management for EdgeConnect apply command with deployment execution and rollback
|
|
// ABOUTME: Handles actual deployment operations, manifest processing, and error recovery with parallel execution
|
|
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/infrastructure/config"
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/domain"
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/ports/driven"
|
|
)
|
|
|
|
// ResourceManagerInterface defines the interface for resource management
|
|
type ResourceManagerInterface interface {
|
|
// ApplyDeployment executes a deployment plan
|
|
ApplyDeployment(ctx context.Context, plan *DeploymentPlan, config *config.EdgeConnectConfig, manifestContent string) (*ExecutionResult, error)
|
|
|
|
// RollbackDeployment attempts to rollback a failed deployment
|
|
RollbackDeployment(ctx context.Context, result *ExecutionResult) error
|
|
|
|
// ValidatePrerequisites checks if deployment prerequisites are met
|
|
ValidatePrerequisites(ctx context.Context, plan *DeploymentPlan) error
|
|
}
|
|
|
|
// EdgeConnectResourceManager implements resource management for EdgeConnect
|
|
type EdgeConnectResourceManager struct {
|
|
appRepo driven.AppRepository
|
|
appInstRepo driven.AppInstanceRepository
|
|
parallelLimit int
|
|
rollbackOnFail bool
|
|
logger Logger
|
|
strategyConfig StrategyConfig
|
|
}
|
|
|
|
// Logger interface for deployment logging
|
|
type Logger interface {
|
|
Printf(format string, v ...interface{})
|
|
}
|
|
|
|
// ResourceManagerOptions configures the resource manager behavior
|
|
type ResourceManagerOptions struct {
|
|
// ParallelLimit controls how many operations run concurrently
|
|
ParallelLimit int
|
|
|
|
// RollbackOnFail automatically rolls back on deployment failure
|
|
RollbackOnFail bool
|
|
|
|
// Logger for deployment operations
|
|
Logger Logger
|
|
|
|
// Timeout for individual operations
|
|
OperationTimeout time.Duration
|
|
|
|
// StrategyConfig for deployment strategies
|
|
StrategyConfig StrategyConfig
|
|
}
|
|
|
|
// DefaultResourceManagerOptions returns sensible defaults
|
|
func DefaultResourceManagerOptions() ResourceManagerOptions {
|
|
return ResourceManagerOptions{
|
|
ParallelLimit: 5, // Conservative parallel limit
|
|
RollbackOnFail: true,
|
|
OperationTimeout: 2 * time.Minute,
|
|
StrategyConfig: DefaultStrategyConfig(),
|
|
}
|
|
}
|
|
|
|
// NewResourceManager creates a new EdgeConnect resource manager
|
|
func NewResourceManager(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository, opts ...func(*ResourceManagerOptions)) ResourceManagerInterface {
|
|
options := DefaultResourceManagerOptions()
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &EdgeConnectResourceManager{
|
|
appRepo: appRepo,
|
|
appInstRepo: appInstRepo,
|
|
parallelLimit: options.ParallelLimit,
|
|
rollbackOnFail: options.RollbackOnFail,
|
|
logger: options.Logger,
|
|
strategyConfig: options.StrategyConfig,
|
|
}
|
|
}
|
|
|
|
// WithParallelLimit sets the parallel execution limit
|
|
func WithParallelLimit(limit int) func(*ResourceManagerOptions) {
|
|
return func(opts *ResourceManagerOptions) {
|
|
opts.ParallelLimit = limit
|
|
}
|
|
}
|
|
|
|
// WithRollbackOnFail enables/disables automatic rollback
|
|
func WithRollbackOnFail(rollback bool) func(*ResourceManagerOptions) {
|
|
return func(opts *ResourceManagerOptions) {
|
|
opts.RollbackOnFail = rollback
|
|
}
|
|
}
|
|
|
|
// WithLogger sets a logger for deployment operations
|
|
func WithLogger(logger Logger) func(*ResourceManagerOptions) {
|
|
return func(opts *ResourceManagerOptions) {
|
|
opts.Logger = logger
|
|
}
|
|
}
|
|
|
|
// WithStrategyConfig sets the strategy configuration
|
|
func WithStrategyConfig(config StrategyConfig) func(*ResourceManagerOptions) {
|
|
return func(opts *ResourceManagerOptions) {
|
|
opts.StrategyConfig = config
|
|
}
|
|
}
|
|
|
|
// ApplyDeployment executes a deployment plan using deployment strategies
|
|
func (rm *EdgeConnectResourceManager) ApplyDeployment(ctx context.Context, plan *DeploymentPlan, config *config.EdgeConnectConfig, manifestContent string) (*ExecutionResult, error) {
|
|
rm.logf("Starting deployment: %s", plan.ConfigName)
|
|
|
|
// Step 1: Validate prerequisites
|
|
if err := rm.ValidatePrerequisites(ctx, plan); err != nil {
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{},
|
|
FailedActions: []ActionResult{},
|
|
Error: fmt.Errorf("prerequisites validation failed: %w", err),
|
|
Duration: 0,
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// Step 2: Determine deployment strategy
|
|
strategyName := DeploymentStrategy(config.Spec.GetDeploymentStrategy())
|
|
rm.logf("Using deployment strategy: %s", strategyName)
|
|
|
|
// Step 3: Create strategy executor
|
|
strategyConfig := rm.strategyConfig
|
|
strategyConfig.ParallelOperations = rm.parallelLimit > 1
|
|
|
|
factory := NewStrategyFactory(rm.appRepo, rm.appInstRepo, strategyConfig, rm.logger)
|
|
strategy, err := factory.CreateStrategy(strategyName)
|
|
if err != nil {
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{},
|
|
FailedActions: []ActionResult{},
|
|
Error: fmt.Errorf("failed to create deployment strategy: %w", err),
|
|
Duration: 0,
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// Step 4: Validate strategy can handle this deployment
|
|
if err := strategy.Validate(plan); err != nil {
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{},
|
|
FailedActions: []ActionResult{},
|
|
Error: fmt.Errorf("strategy validation failed: %w", err),
|
|
Duration: 0,
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// Step 5: Execute the deployment strategy
|
|
rm.logf("Estimated deployment duration: %v", strategy.EstimateDuration(plan))
|
|
result, err := strategy.Execute(ctx, plan, config, manifestContent)
|
|
|
|
// Step 6: Handle rollback if needed
|
|
if err != nil && rm.rollbackOnFail && result != nil {
|
|
rm.logf("Deployment failed, attempting rollback...")
|
|
if rollbackErr := rm.RollbackDeployment(ctx, result); rollbackErr != nil {
|
|
rm.logf("Rollback failed: %v", rollbackErr)
|
|
} else {
|
|
result.RollbackPerformed = true
|
|
result.RollbackSuccess = true
|
|
}
|
|
}
|
|
|
|
if result != nil && result.Success {
|
|
rm.logf("Deployment completed successfully in %v", result.Duration)
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
// ValidatePrerequisites checks if deployment prerequisites are met
|
|
func (rm *EdgeConnectResourceManager) ValidatePrerequisites(ctx context.Context, plan *DeploymentPlan) error {
|
|
rm.logf("Validating deployment prerequisites for: %s", plan.ConfigName)
|
|
|
|
// Check if we have any actions to perform
|
|
if plan.IsEmpty() {
|
|
return fmt.Errorf("deployment plan is empty - no actions to perform")
|
|
}
|
|
|
|
// Validate that we have required client capabilities
|
|
if rm.appRepo == nil || rm.appInstRepo == nil {
|
|
return fmt.Errorf("repositories are not configured")
|
|
}
|
|
|
|
rm.logf("Prerequisites validation passed")
|
|
return nil
|
|
}
|
|
|
|
// RollbackDeployment attempts to rollback a failed deployment
|
|
func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, result *ExecutionResult) error {
|
|
rm.logf("Starting rollback for deployment: %s", result.Plan.ConfigName)
|
|
|
|
rollbackErrors := []error{}
|
|
|
|
// Rollback completed instances (in reverse order)
|
|
for i := len(result.CompletedActions) - 1; i >= 0; i-- {
|
|
action := result.CompletedActions[i]
|
|
|
|
switch action.Type {
|
|
case ActionCreate:
|
|
if err := rm.rollbackCreateAction(ctx, action, result.Plan); err != nil {
|
|
rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to rollback %s: %w", action.Target, err))
|
|
} else {
|
|
rm.logf("Successfully rolled back: %s", action.Target)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(rollbackErrors) > 0 {
|
|
return fmt.Errorf("rollback encountered %d errors: %v", len(rollbackErrors), rollbackErrors)
|
|
}
|
|
|
|
rm.logf("Rollback completed successfully")
|
|
return nil
|
|
}
|
|
|
|
// rollbackCreateAction rolls back a CREATE action by deleting the resource
|
|
func (rm *EdgeConnectResourceManager) rollbackCreateAction(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
|
if action.Type != ActionCreate {
|
|
return nil
|
|
}
|
|
|
|
// Determine if this is an app or instance rollback based on the target name
|
|
isInstance := false
|
|
for _, instanceAction := range plan.InstanceActions {
|
|
if instanceAction.InstanceName == action.Target {
|
|
isInstance = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isInstance {
|
|
return rm.rollbackInstance(ctx, action, plan)
|
|
} else {
|
|
return rm.rollbackApp(ctx, action, plan)
|
|
}
|
|
}
|
|
|
|
// rollbackApp deletes an application that was created
|
|
func (rm *EdgeConnectResourceManager) rollbackApp(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
|
appKey := domain.AppKey{
|
|
Organization: plan.AppAction.Desired.Organization,
|
|
Name: plan.AppAction.Desired.Name,
|
|
Version: plan.AppAction.Desired.Version,
|
|
}
|
|
|
|
return rm.appRepo.DeleteApp(ctx, plan.AppAction.Desired.Region, appKey)
|
|
}
|
|
|
|
// rollbackInstance deletes an instance that was created
|
|
func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
|
// Find the instance action to get the details
|
|
for _, instanceAction := range plan.InstanceActions {
|
|
if instanceAction.InstanceName == action.Target {
|
|
instanceKey := domain.AppInstanceKey{
|
|
Organization: plan.AppAction.Desired.Organization,
|
|
Name: instanceAction.InstanceName,
|
|
CloudletKey: domain.CloudletKey{
|
|
Organization: instanceAction.Target.CloudletOrg,
|
|
Name: instanceAction.Target.CloudletName,
|
|
},
|
|
}
|
|
return rm.appInstRepo.DeleteAppInstance(ctx, instanceAction.Target.Region, instanceKey)
|
|
}
|
|
}
|
|
return fmt.Errorf("instance action not found for rollback: %s", action.Target)
|
|
}
|
|
|
|
// logf logs a message if a logger is configured
|
|
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
|
if rm.logger != nil {
|
|
rm.logger.Printf("[ResourceManager] "+format, v...)
|
|
}
|
|
} |