2025-09-29 17:24:59 +02:00
|
|
|
// ABOUTME: Deployment planner for EdgeConnect apply command with intelligent state comparison
|
|
|
|
|
// ABOUTME: Analyzes desired vs current state to generate optimal deployment plans with minimal API calls
|
2025-10-20 13:57:57 +02:00
|
|
|
package v2
|
2025-09-29 17:24:59 +02:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-10-21 11:40:35 +02:00
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
|
|
|
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
2025-09-29 17:24:59 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
|
|
|
|
type EdgeConnectClientInterface interface {
|
2025-10-20 13:34:22 +02:00
|
|
|
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
|
|
|
|
|
CreateApp(ctx context.Context, input *v2.NewAppInput) error
|
|
|
|
|
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
|
|
|
|
|
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
|
|
|
|
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error)
|
|
|
|
|
CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
|
|
|
|
|
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
|
|
|
|
|
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
|
2025-09-29 17:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Planner defines the interface for deployment planning
|
|
|
|
|
type Planner interface {
|
|
|
|
|
// Plan analyzes the configuration and current state to generate a deployment 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
|
|
|
|
|
|
|
|
|
|
// Force indicates to proceed even with warnings
|
|
|
|
|
Force bool
|
|
|
|
|
|
|
|
|
|
// SkipStateCheck bypasses current state queries (useful for testing)
|
|
|
|
|
SkipStateCheck bool
|
|
|
|
|
|
|
|
|
|
// ParallelQueries enables parallel state fetching
|
|
|
|
|
ParallelQueries bool
|
|
|
|
|
|
|
|
|
|
// Timeout for API operations
|
|
|
|
|
Timeout time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DefaultPlanOptions returns sensible default planning options
|
|
|
|
|
func DefaultPlanOptions() PlanOptions {
|
|
|
|
|
return PlanOptions{
|
|
|
|
|
DryRun: false,
|
|
|
|
|
Force: false,
|
|
|
|
|
SkipStateCheck: false,
|
|
|
|
|
ParallelQueries: true,
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EdgeConnectPlanner implements the Planner interface for EdgeConnect
|
|
|
|
|
type EdgeConnectPlanner struct {
|
|
|
|
|
client EdgeConnectClientInterface
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlanner creates a new EdgeConnect deployment planner
|
|
|
|
|
func NewPlanner(client EdgeConnectClientInterface) Planner {
|
|
|
|
|
return &EdgeConnectPlanner{
|
|
|
|
|
client: client,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Plan analyzes the configuration and generates a deployment plan
|
|
|
|
|
func (p *EdgeConnectPlanner) Plan(ctx context.Context, config *config.EdgeConnectConfig) (*PlanResult, error) {
|
|
|
|
|
return p.PlanWithOptions(ctx, config, DefaultPlanOptions())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PlanWithOptions generates a deployment 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 deployment plan structure
|
|
|
|
|
plan := &DeploymentPlan{
|
|
|
|
|
ConfigName: config.Metadata.Name,
|
|
|
|
|
CreatedAt: startTime,
|
|
|
|
|
DryRun: opts.DryRun,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 1: Plan application state
|
|
|
|
|
appAction, appWarnings, err := p.planAppAction(ctx, config, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return &PlanResult{Error: err}, err
|
|
|
|
|
}
|
|
|
|
|
plan.AppAction = *appAction
|
|
|
|
|
warnings = append(warnings, appWarnings...)
|
|
|
|
|
|
|
|
|
|
// Step 2: Plan instance actions
|
|
|
|
|
instanceActions, instanceWarnings, err := p.planInstanceActions(ctx, config, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return &PlanResult{Error: err}, err
|
|
|
|
|
}
|
|
|
|
|
plan.InstanceActions = instanceActions
|
|
|
|
|
warnings = append(warnings, instanceWarnings...)
|
|
|
|
|
|
|
|
|
|
// Step 3: Calculate plan metadata
|
|
|
|
|
p.calculatePlanMetadata(plan)
|
|
|
|
|
|
|
|
|
|
// Step 4: Generate summary
|
|
|
|
|
plan.Summary = plan.GenerateSummary()
|
|
|
|
|
|
|
|
|
|
// Step 5: Validate the plan
|
|
|
|
|
if err := plan.Validate(); err != nil {
|
|
|
|
|
return &PlanResult{Error: fmt.Errorf("invalid deployment plan: %w", err)}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &PlanResult{
|
|
|
|
|
Plan: plan,
|
|
|
|
|
Warnings: warnings,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// planAppAction determines what action needs to be taken for the application
|
|
|
|
|
func (p *EdgeConnectPlanner) planAppAction(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) (*AppAction, []string, error) {
|
|
|
|
|
var warnings []string
|
|
|
|
|
|
|
|
|
|
// Build desired app state
|
|
|
|
|
desired := &AppState{
|
2025-09-30 11:33:52 +02:00
|
|
|
Name: config.Metadata.Name,
|
2025-10-07 16:01:38 +02:00
|
|
|
Version: config.Metadata.AppVersion,
|
2025-10-21 11:40:35 +02:00
|
|
|
Organization: config.Metadata.Organization, // Use first infra template for org
|
|
|
|
|
Region: config.Spec.InfraTemplate[0].Region, // Use first infra template for region
|
|
|
|
|
Exists: false, // Will be set based on current state
|
2025-09-29 17:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config.Spec.IsK8sApp() {
|
|
|
|
|
desired.AppType = AppTypeK8s
|
|
|
|
|
} else {
|
|
|
|
|
desired.AppType = AppTypeDocker
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
// Extract outbound connections from config
|
|
|
|
|
if config.Spec.Network != nil {
|
|
|
|
|
desired.OutboundConnections = make([]SecurityRule, len(config.Spec.Network.OutboundConnections))
|
|
|
|
|
for i, conn := range config.Spec.Network.OutboundConnections {
|
|
|
|
|
desired.OutboundConnections[i] = SecurityRule{
|
|
|
|
|
Protocol: conn.Protocol,
|
|
|
|
|
PortRangeMin: conn.PortRangeMin,
|
|
|
|
|
PortRangeMax: conn.PortRangeMax,
|
|
|
|
|
RemoteCIDR: conn.RemoteCIDR,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 17:24:59 +02:00
|
|
|
// Calculate manifest hash
|
|
|
|
|
manifestHash, err := p.calculateManifestHash(config.Spec.GetManifestFile())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, warnings, fmt.Errorf("failed to calculate manifest hash: %w", err)
|
|
|
|
|
}
|
|
|
|
|
desired.ManifestHash = manifestHash
|
|
|
|
|
|
|
|
|
|
action := &AppAction{
|
|
|
|
|
Type: ActionNone,
|
|
|
|
|
Desired: desired,
|
|
|
|
|
ManifestHash: manifestHash,
|
|
|
|
|
Reason: "No action needed",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip state check if requested (useful for testing)
|
|
|
|
|
if opts.SkipStateCheck {
|
|
|
|
|
action.Type = ActionCreate
|
|
|
|
|
action.Reason = "Creating app (state check skipped)"
|
|
|
|
|
action.Changes = []string{"Create new application"}
|
|
|
|
|
return action, warnings, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query current app state
|
|
|
|
|
current, err := p.getCurrentAppState(ctx, desired, opts.Timeout)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// If app doesn't exist, we need to create it
|
|
|
|
|
if isResourceNotFoundError(err) {
|
|
|
|
|
action.Type = ActionCreate
|
|
|
|
|
action.Reason = "Application does not exist"
|
|
|
|
|
action.Changes = []string{"Create new application"}
|
|
|
|
|
return action, warnings, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, warnings, fmt.Errorf("failed to query current app state: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action.Current = current
|
|
|
|
|
|
|
|
|
|
// Compare current vs desired state
|
|
|
|
|
changes, manifestChanged := p.compareAppStates(current, desired)
|
|
|
|
|
action.ManifestChanged = manifestChanged
|
|
|
|
|
|
|
|
|
|
if len(changes) > 0 {
|
|
|
|
|
action.Type = ActionUpdate
|
|
|
|
|
action.Changes = changes
|
|
|
|
|
action.Reason = "Application configuration has changed"
|
2025-10-07 16:01:38 +02:00
|
|
|
fmt.Printf("Changes: %v\n", changes)
|
2025-09-29 17:24:59 +02:00
|
|
|
|
|
|
|
|
if manifestChanged {
|
|
|
|
|
warnings = append(warnings, "Manifest file has changed - instances may need to be recreated")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return action, warnings, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// planInstanceActions determines what actions need to be taken for instances
|
|
|
|
|
func (p *EdgeConnectPlanner) planInstanceActions(ctx context.Context, config *config.EdgeConnectConfig, opts PlanOptions) ([]InstanceAction, []string, error) {
|
|
|
|
|
var actions []InstanceAction
|
|
|
|
|
var warnings []string
|
|
|
|
|
|
|
|
|
|
for _, infra := range config.Spec.InfraTemplate {
|
2025-10-07 16:01:38 +02:00
|
|
|
instanceName := getInstanceName(config.Metadata.Name, config.Metadata.AppVersion)
|
2025-09-29 17:24:59 +02:00
|
|
|
|
|
|
|
|
desired := &InstanceState{
|
|
|
|
|
Name: instanceName,
|
2025-10-07 16:01:38 +02:00
|
|
|
AppVersion: config.Metadata.AppVersion,
|
2025-10-07 16:30:57 +02:00
|
|
|
Organization: config.Metadata.Organization,
|
2025-09-29 17:24:59 +02:00
|
|
|
Region: infra.Region,
|
|
|
|
|
CloudletOrg: infra.CloudletOrg,
|
|
|
|
|
CloudletName: infra.CloudletName,
|
|
|
|
|
FlavorName: infra.FlavorName,
|
|
|
|
|
Exists: false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action := &InstanceAction{
|
|
|
|
|
Type: ActionNone,
|
|
|
|
|
Target: infra,
|
|
|
|
|
Desired: desired,
|
|
|
|
|
InstanceName: instanceName,
|
|
|
|
|
Reason: "No action needed",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip state check if requested
|
|
|
|
|
if opts.SkipStateCheck {
|
|
|
|
|
action.Type = ActionCreate
|
|
|
|
|
action.Reason = "Creating instance (state check skipped)"
|
|
|
|
|
action.Changes = []string{"Create new instance"}
|
|
|
|
|
actions = append(actions, *action)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query current instance state
|
|
|
|
|
current, err := p.getCurrentInstanceState(ctx, desired, opts.Timeout)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// If instance doesn't exist, we need to create it
|
|
|
|
|
if isResourceNotFoundError(err) {
|
|
|
|
|
action.Type = ActionCreate
|
|
|
|
|
action.Reason = "Instance does not exist"
|
|
|
|
|
action.Changes = []string{"Create new instance"}
|
|
|
|
|
actions = append(actions, *action)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return nil, warnings, fmt.Errorf("failed to query current instance state: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
action.Current = current
|
|
|
|
|
|
|
|
|
|
// Compare current vs desired state
|
|
|
|
|
changes := p.compareInstanceStates(current, desired)
|
|
|
|
|
if len(changes) > 0 {
|
|
|
|
|
action.Type = ActionUpdate
|
|
|
|
|
action.Changes = changes
|
|
|
|
|
action.Reason = "Instance configuration has changed"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actions = append(actions, *action)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return actions, warnings, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getCurrentAppState queries the current state of an application
|
|
|
|
|
func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *AppState, timeout time.Duration) (*AppState, error) {
|
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
appKey := v2.AppKey{
|
2025-09-29 17:24:59 +02:00
|
|
|
Organization: desired.Organization,
|
|
|
|
|
Name: desired.Name,
|
|
|
|
|
Version: desired.Version,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app, err := p.client.ShowApp(timeoutCtx, appKey, desired.Region)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current := &AppState{
|
|
|
|
|
Name: app.Key.Name,
|
|
|
|
|
Version: app.Key.Version,
|
|
|
|
|
Organization: app.Key.Organization,
|
|
|
|
|
Region: desired.Region,
|
|
|
|
|
Exists: true,
|
|
|
|
|
LastUpdated: time.Now(), // EdgeConnect doesn't provide this, so use current time
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 17:08:33 +02:00
|
|
|
// Calculate current manifest hash
|
|
|
|
|
hasher := sha256.New()
|
|
|
|
|
hasher.Write([]byte(app.DeploymentManifest))
|
|
|
|
|
current.ManifestHash = fmt.Sprintf("%x", hasher.Sum(nil))
|
|
|
|
|
|
2025-09-29 17:24:59 +02:00
|
|
|
// Note: EdgeConnect API doesn't currently support annotations for manifest hash tracking
|
|
|
|
|
// This would be implemented when the API supports it
|
|
|
|
|
|
|
|
|
|
// Determine app type based on deployment type
|
|
|
|
|
if app.Deployment == "kubernetes" {
|
|
|
|
|
current.AppType = AppTypeK8s
|
|
|
|
|
} else {
|
|
|
|
|
current.AppType = AppTypeDocker
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
// Extract outbound connections from the app
|
|
|
|
|
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
|
|
|
|
for i, conn := range app.RequiredOutboundConnections {
|
|
|
|
|
current.OutboundConnections[i] = SecurityRule{
|
|
|
|
|
Protocol: conn.Protocol,
|
|
|
|
|
PortRangeMin: conn.PortRangeMin,
|
|
|
|
|
PortRangeMax: conn.PortRangeMax,
|
|
|
|
|
RemoteCIDR: conn.RemoteCIDR,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 17:24:59 +02:00
|
|
|
return current, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getCurrentInstanceState queries the current state of an application instance
|
|
|
|
|
func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desired *InstanceState, timeout time.Duration) (*InstanceState, error) {
|
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
instanceKey := v2.AppInstanceKey{
|
2025-09-29 17:24:59 +02:00
|
|
|
Organization: desired.Organization,
|
|
|
|
|
Name: desired.Name,
|
2025-10-20 13:34:22 +02:00
|
|
|
CloudletKey: v2.CloudletKey{
|
2025-09-29 17:24:59 +02:00
|
|
|
Organization: desired.CloudletOrg,
|
|
|
|
|
Name: desired.CloudletName,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current := &InstanceState{
|
|
|
|
|
Name: instance.Key.Name,
|
|
|
|
|
AppName: instance.AppKey.Name,
|
|
|
|
|
AppVersion: instance.AppKey.Version,
|
|
|
|
|
Organization: instance.Key.Organization,
|
|
|
|
|
Region: desired.Region,
|
|
|
|
|
CloudletOrg: instance.Key.CloudletKey.Organization,
|
|
|
|
|
CloudletName: instance.Key.CloudletKey.Name,
|
|
|
|
|
FlavorName: instance.Flavor.Name,
|
|
|
|
|
State: instance.State,
|
|
|
|
|
PowerState: instance.PowerState,
|
|
|
|
|
Exists: true,
|
|
|
|
|
LastUpdated: time.Now(), // EdgeConnect doesn't provide this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return current, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// compareAppStates compares current and desired app states and returns changes
|
|
|
|
|
func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]string, bool) {
|
|
|
|
|
var changes []string
|
|
|
|
|
manifestChanged := false
|
|
|
|
|
|
|
|
|
|
// Compare manifest hash - only if both states have hash values
|
|
|
|
|
// Since EdgeConnect API doesn't support annotations yet, skip manifest hash comparison for now
|
|
|
|
|
// This would be implemented when the API supports manifest hash tracking
|
|
|
|
|
if current.ManifestHash != "" && desired.ManifestHash != "" && current.ManifestHash != desired.ManifestHash {
|
|
|
|
|
changes = append(changes, fmt.Sprintf("Manifest hash changed: %s -> %s", current.ManifestHash, desired.ManifestHash))
|
|
|
|
|
manifestChanged = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare app type
|
|
|
|
|
if current.AppType != desired.AppType {
|
|
|
|
|
changes = append(changes, fmt.Sprintf("App type changed: %s -> %s", current.AppType, desired.AppType))
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
// Compare outbound connections
|
2025-10-06 16:45:53 +02:00
|
|
|
outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
|
|
|
|
|
if len(outboundChanges) > 0 {
|
2025-10-21 11:40:35 +02:00
|
|
|
sb := strings.Builder{}
|
2025-10-07 15:40:27 +02:00
|
|
|
sb.WriteString("Outbound connections changed:\n")
|
|
|
|
|
for _, change := range outboundChanges {
|
|
|
|
|
sb.WriteString(change)
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
}
|
|
|
|
|
changes = append(changes, sb.String())
|
2025-10-01 10:49:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 17:24:59 +02:00
|
|
|
return changes, manifestChanged
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
// compareOutboundConnections compares two sets of outbound connections for equality
|
2025-10-06 16:45:53 +02:00
|
|
|
func (p *EdgeConnectPlanner) compareOutboundConnections(current, desired []SecurityRule) []string {
|
|
|
|
|
var changes []string
|
|
|
|
|
makeMap := func(rules []SecurityRule) map[string]SecurityRule {
|
|
|
|
|
m := make(map[string]SecurityRule, len(rules))
|
2025-10-01 10:49:15 +02:00
|
|
|
for _, r := range rules {
|
|
|
|
|
key := fmt.Sprintf("%s:%d-%d:%s",
|
|
|
|
|
strings.ToLower(r.Protocol),
|
|
|
|
|
r.PortRangeMin,
|
|
|
|
|
r.PortRangeMax,
|
|
|
|
|
r.RemoteCIDR,
|
|
|
|
|
)
|
2025-10-06 16:45:53 +02:00
|
|
|
m[key] = r
|
2025-10-01 10:49:15 +02:00
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentMap := makeMap(current)
|
|
|
|
|
desiredMap := makeMap(desired)
|
|
|
|
|
|
2025-10-06 16:45:53 +02:00
|
|
|
// Find added and modified rules
|
|
|
|
|
for key, rule := range desiredMap {
|
|
|
|
|
if _, exists := currentMap[key]; !exists {
|
|
|
|
|
changes = append(changes, fmt.Sprintf(" - Added outbound connection: %s %d-%d to %s", rule.Protocol, rule.PortRangeMin, rule.PortRangeMax, rule.RemoteCIDR))
|
|
|
|
|
}
|
2025-10-01 10:49:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 16:45:53 +02:00
|
|
|
// Find removed rules
|
|
|
|
|
for key, rule := range currentMap {
|
|
|
|
|
if _, exists := desiredMap[key]; !exists {
|
|
|
|
|
changes = append(changes, fmt.Sprintf(" - Removed outbound connection: %s %d-%d to %s", rule.Protocol, rule.PortRangeMin, rule.PortRangeMax, rule.RemoteCIDR))
|
2025-10-01 10:49:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 16:45:53 +02:00
|
|
|
return changes
|
2025-10-01 10:49:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 17:24:59 +02:00
|
|
|
// compareInstanceStates compares current and desired instance states and returns changes
|
|
|
|
|
func (p *EdgeConnectPlanner) compareInstanceStates(current, desired *InstanceState) []string {
|
|
|
|
|
var changes []string
|
|
|
|
|
|
|
|
|
|
if current.FlavorName != desired.FlavorName {
|
|
|
|
|
changes = append(changes, fmt.Sprintf("Flavor changed: %s -> %s", current.FlavorName, desired.FlavorName))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if current.CloudletName != desired.CloudletName {
|
|
|
|
|
changes = append(changes, fmt.Sprintf("Cloudlet changed: %s -> %s", current.CloudletName, desired.CloudletName))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if current.CloudletOrg != desired.CloudletOrg {
|
|
|
|
|
changes = append(changes, fmt.Sprintf("Cloudlet org changed: %s -> %s", current.CloudletOrg, desired.CloudletOrg))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return changes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculateManifestHash computes the SHA256 hash of a manifest file
|
|
|
|
|
func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string, error) {
|
|
|
|
|
if manifestPath == "" {
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(manifestPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
hasher := sha256.New()
|
|
|
|
|
if _, err := io.Copy(hasher, file); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to hash manifest file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculatePlanMetadata computes metadata for the deployment plan
|
|
|
|
|
func (p *EdgeConnectPlanner) calculatePlanMetadata(plan *DeploymentPlan) {
|
|
|
|
|
totalActions := 0
|
|
|
|
|
|
|
|
|
|
if plan.AppAction.Type != ActionNone {
|
|
|
|
|
totalActions++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, action := range plan.InstanceActions {
|
|
|
|
|
if action.Type != ActionNone {
|
|
|
|
|
totalActions++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plan.TotalActions = totalActions
|
|
|
|
|
|
|
|
|
|
// Estimate duration based on action types and counts
|
|
|
|
|
plan.EstimatedDuration = p.estimateDeploymentDuration(plan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// estimateDeploymentDuration provides a rough estimate of deployment time
|
|
|
|
|
func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) time.Duration {
|
|
|
|
|
var duration time.Duration
|
|
|
|
|
|
|
|
|
|
// App operations
|
|
|
|
|
if plan.AppAction.Type == ActionCreate {
|
|
|
|
|
duration += 30 * time.Second
|
|
|
|
|
} else if plan.AppAction.Type == ActionUpdate {
|
|
|
|
|
duration += 15 * time.Second
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Instance operations (can be done in parallel)
|
|
|
|
|
instanceDuration := time.Duration(0)
|
|
|
|
|
for _, action := range plan.InstanceActions {
|
|
|
|
|
if action.Type == ActionCreate {
|
|
|
|
|
instanceDuration = max(instanceDuration, 2*time.Minute)
|
|
|
|
|
} else if action.Type == ActionUpdate {
|
|
|
|
|
instanceDuration = max(instanceDuration, 1*time.Minute)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
duration += instanceDuration
|
|
|
|
|
|
|
|
|
|
// Add buffer time
|
|
|
|
|
duration += 30 * time.Second
|
|
|
|
|
|
|
|
|
|
return duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isResourceNotFoundError checks if an error indicates a resource was not found
|
|
|
|
|
func isResourceNotFoundError(err error) bool {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errStr := strings.ToLower(err.Error())
|
|
|
|
|
return strings.Contains(errStr, "not found") ||
|
2025-09-29 17:35:34 +02:00
|
|
|
strings.Contains(errStr, "does not exist") ||
|
|
|
|
|
strings.Contains(errStr, "404")
|
2025-09-29 17:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// max returns the larger of two durations
|
|
|
|
|
func max(a, b time.Duration) time.Duration {
|
|
|
|
|
if a > b {
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getInstanceName generates the instance name following the pattern: appName-appVersion-instance
|
|
|
|
|
func getInstanceName(appName, appVersion string) string {
|
|
|
|
|
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
|
2025-09-29 17:35:34 +02:00
|
|
|
}
|