603 lines
17 KiB
Go
603 lines
17 KiB
Go
// ABOUTME: Comprehensive tests for EdgeConnect resource manager with deployment scenarios
|
|
// ABOUTME: Tests deployment execution, rollback functionality, and error handling with mock clients
|
|
package v2
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// MockResourceClient extends MockEdgeConnectClient with resource management methods
|
|
type MockResourceClient struct {
|
|
MockEdgeConnectClient
|
|
}
|
|
|
|
func (m *MockResourceClient) CreateApp(ctx context.Context, input *v2.NewAppInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
|
args := m.Called(ctx, appKey, region)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
|
args := m.Called(ctx, instanceKey, region)
|
|
return args.Error(0)
|
|
}
|
|
|
|
// TestLogger implements Logger interface for testing
|
|
type TestLogger struct {
|
|
messages []string
|
|
}
|
|
|
|
func (l *TestLogger) Printf(format string, v ...interface{}) {
|
|
l.messages = append(l.messages, fmt.Sprintf(format, v...))
|
|
}
|
|
|
|
func TestNewResourceManager(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
manager := NewResourceManager(mockClient)
|
|
|
|
assert.NotNil(t, manager)
|
|
assert.IsType(t, &EdgeConnectResourceManager{}, manager)
|
|
}
|
|
|
|
func TestDefaultResourceManagerOptions(t *testing.T) {
|
|
opts := DefaultResourceManagerOptions()
|
|
|
|
assert.Equal(t, 5, opts.ParallelLimit)
|
|
assert.True(t, opts.RollbackOnFail)
|
|
assert.Equal(t, 2*time.Minute, opts.OperationTimeout)
|
|
}
|
|
|
|
func TestWithOptions(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
|
|
manager := NewResourceManager(mockClient,
|
|
WithParallelLimit(10),
|
|
WithRollbackOnFail(false),
|
|
WithLogger(logger),
|
|
)
|
|
|
|
// Cast to implementation to check options were applied
|
|
impl := manager.(*EdgeConnectResourceManager)
|
|
assert.Equal(t, 10, impl.parallelLimit)
|
|
assert.False(t, impl.rollbackOnFail)
|
|
assert.Equal(t, logger, impl.logger)
|
|
}
|
|
|
|
func createTestDeploymentPlan() *DeploymentPlan {
|
|
return &DeploymentPlan{
|
|
ConfigName: "test-deployment",
|
|
AppAction: AppAction{
|
|
Type: ActionCreate,
|
|
Desired: &AppState{
|
|
Name: "test-app",
|
|
Version: "1.0.0",
|
|
Organization: "testorg",
|
|
Region: "US",
|
|
},
|
|
},
|
|
InstanceActions: []InstanceAction{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: config.InfraTemplate{
|
|
Region: "US",
|
|
CloudletOrg: "cloudletorg",
|
|
CloudletName: "cloudlet1",
|
|
FlavorName: "small",
|
|
},
|
|
Desired: &InstanceState{
|
|
Name: "test-app-1.0.0-instance",
|
|
AppName: "test-app",
|
|
},
|
|
InstanceName: "test-app-1.0.0-instance",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestManagerConfig(t *testing.T) *config.EdgeConnectConfig {
|
|
// Create temporary manifest file
|
|
tempDir := t.TempDir()
|
|
manifestFile := filepath.Join(tempDir, "test-manifest.yaml")
|
|
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
|
err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
return &config.EdgeConnectConfig{
|
|
Kind: "edgeconnect-deployment",
|
|
Metadata: config.Metadata{
|
|
Name: "test-app",
|
|
AppVersion: "1.0.0",
|
|
Organization: "testorg",
|
|
},
|
|
Spec: config.Spec{
|
|
K8sApp: &config.K8sApp{
|
|
ManifestFile: manifestFile,
|
|
},
|
|
InfraTemplate: []config.InfraTemplate{
|
|
{
|
|
Region: "US",
|
|
CloudletOrg: "cloudletorg",
|
|
CloudletName: "cloudlet1",
|
|
FlavorName: "small",
|
|
},
|
|
},
|
|
Network: &config.NetworkConfig{
|
|
OutboundConnections: []config.OutboundConnection{
|
|
{
|
|
Protocol: "tcp",
|
|
PortRangeMin: 80,
|
|
PortRangeMax: 80,
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// createTestStrategyConfig returns a fast configuration for tests
|
|
func createTestStrategyConfig() StrategyConfig {
|
|
return StrategyConfig{
|
|
MaxRetries: 0, // No retries for fast tests
|
|
HealthCheckTimeout: 1 * time.Millisecond,
|
|
ParallelOperations: false, // Sequential for predictable tests
|
|
RetryDelay: 0, // No delay
|
|
}
|
|
}
|
|
|
|
func TestApplyDeploymentSuccess(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock successful operations
|
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
|
Return(nil)
|
|
|
|
ctx := context.Background()
|
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.Success)
|
|
assert.Len(t, result.CompletedActions, 2) // 1 app + 1 instance
|
|
assert.Len(t, result.FailedActions, 0)
|
|
assert.False(t, result.RollbackPerformed)
|
|
assert.Greater(t, result.Duration, time.Duration(0))
|
|
|
|
// Check that operations were logged
|
|
assert.Greater(t, len(logger.messages), 0)
|
|
|
|
mockClient.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentAppFailure(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock app creation failure - deployment should stop here
|
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
|
|
|
ctx := context.Background()
|
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
|
|
|
require.Error(t, err)
|
|
require.NotNil(t, result)
|
|
assert.False(t, result.Success)
|
|
assert.Len(t, result.CompletedActions, 0)
|
|
assert.Len(t, result.FailedActions, 1)
|
|
assert.Contains(t, err.Error(), "Server error")
|
|
|
|
mockClient.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithRollbackOnFail(true), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock successful app creation but failed instance creation
|
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})
|
|
|
|
// Mock rollback operations
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
Return(nil)
|
|
|
|
ctx := context.Background()
|
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
|
|
|
require.Error(t, err)
|
|
require.NotNil(t, result)
|
|
assert.False(t, result.Success)
|
|
assert.Len(t, result.CompletedActions, 1) // App was created
|
|
assert.Len(t, result.FailedActions, 1) // Instance failed
|
|
assert.True(t, result.RollbackPerformed)
|
|
assert.True(t, result.RollbackSuccess)
|
|
assert.Contains(t, err.Error(), "failed to create instance")
|
|
|
|
mockClient.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentNoActions(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
manager := NewResourceManager(mockClient)
|
|
|
|
// Create empty plan
|
|
plan := &DeploymentPlan{
|
|
ConfigName: "empty-plan",
|
|
AppAction: AppAction{Type: ActionNone},
|
|
}
|
|
config := createTestManagerConfig(t)
|
|
|
|
ctx := context.Background()
|
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
|
|
|
require.Error(t, err)
|
|
require.NotNil(t, result)
|
|
assert.Contains(t, err.Error(), "deployment plan is empty")
|
|
|
|
mockClient.AssertNotCalled(t, "CreateApp")
|
|
mockClient.AssertNotCalled(t, "CreateAppInstance")
|
|
}
|
|
|
|
func TestApplyDeploymentMultipleInstances(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithParallelLimit(2), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
// Create plan with multiple instances
|
|
plan := &DeploymentPlan{
|
|
ConfigName: "multi-instance",
|
|
AppAction: AppAction{
|
|
Type: ActionCreate,
|
|
Desired: &AppState{
|
|
Name: "test-app",
|
|
Version: "1.0.0",
|
|
Organization: "testorg",
|
|
Region: "US",
|
|
},
|
|
},
|
|
InstanceActions: []InstanceAction{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: config.InfraTemplate{
|
|
Region: "US",
|
|
CloudletOrg: "cloudletorg1",
|
|
CloudletName: "cloudlet1",
|
|
FlavorName: "small",
|
|
},
|
|
Desired: &InstanceState{Name: "instance1"},
|
|
InstanceName: "instance1",
|
|
},
|
|
{
|
|
Type: ActionCreate,
|
|
Target: config.InfraTemplate{
|
|
Region: "EU",
|
|
CloudletOrg: "cloudletorg2",
|
|
CloudletName: "cloudlet2",
|
|
FlavorName: "medium",
|
|
},
|
|
Desired: &InstanceState{Name: "instance2"},
|
|
InstanceName: "instance2",
|
|
},
|
|
},
|
|
}
|
|
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock successful operations
|
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
|
Return(nil)
|
|
|
|
ctx := context.Background()
|
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.True(t, result.Success)
|
|
assert.Len(t, result.CompletedActions, 3) // 1 app + 2 instances
|
|
assert.Len(t, result.FailedActions, 0)
|
|
|
|
mockClient.AssertExpectations(t)
|
|
}
|
|
|
|
func TestValidatePrerequisites(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
manager := NewResourceManager(mockClient)
|
|
|
|
tests := []struct {
|
|
name string
|
|
plan *DeploymentPlan
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid plan",
|
|
plan: &DeploymentPlan{
|
|
ConfigName: "test",
|
|
AppAction: AppAction{Type: ActionCreate, Desired: &AppState{}},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty plan",
|
|
plan: &DeploymentPlan{
|
|
ConfigName: "test",
|
|
AppAction: AppAction{Type: ActionNone},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "deployment plan is empty",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
err := manager.ValidatePrerequisites(ctx, tt.plan)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errMsg)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRollbackDeployment(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
// Create result with completed actions
|
|
plan := createTestDeploymentPlan()
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app",
|
|
Success: true,
|
|
},
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app-1.0.0-instance",
|
|
Success: true,
|
|
},
|
|
},
|
|
FailedActions: []ActionResult{},
|
|
}
|
|
|
|
// Mock rollback operations
|
|
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
|
Return(nil)
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
Return(nil)
|
|
|
|
ctx := context.Background()
|
|
err := manager.RollbackDeployment(ctx, result)
|
|
|
|
require.NoError(t, err)
|
|
mockClient.AssertExpectations(t)
|
|
|
|
// Check rollback was logged
|
|
assert.Greater(t, len(logger.messages), 0)
|
|
}
|
|
|
|
func TestRollbackDeploymentFailure(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
manager := NewResourceManager(mockClient)
|
|
|
|
plan := createTestDeploymentPlan()
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app",
|
|
Success: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Mock rollback failure
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Delete failed"}})
|
|
|
|
ctx := context.Background()
|
|
err := manager.RollbackDeployment(ctx, result)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "rollback encountered")
|
|
mockClient.AssertExpectations(t)
|
|
}
|
|
|
|
func TestRollbackDeploymentWithRestore(t *testing.T) {
|
|
mockClient := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
|
|
// Simulate a RecreateStrategy scenario:
|
|
// 1. Old app and instance were deleted and backed up
|
|
// 2. New app was created successfully
|
|
// 3. New instance creation failed
|
|
// 4. Rollback should: delete new app, restore old app, restore old instance
|
|
oldApp := v2.App{
|
|
Key: v2.AppKey{
|
|
Organization: "test-org",
|
|
Name: "test-app",
|
|
Version: "1.0.0",
|
|
},
|
|
Deployment: "kubernetes",
|
|
DeploymentManifest: "old-manifest-content",
|
|
}
|
|
|
|
oldInstance := v2.AppInstance{
|
|
Key: v2.AppInstanceKey{
|
|
Organization: "test-org",
|
|
Name: "test-app-1.0.0-instance",
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: "test-cloudlet-org",
|
|
Name: "test-cloudlet",
|
|
},
|
|
},
|
|
AppKey: v2.AppKey{
|
|
Organization: "test-org",
|
|
Name: "test-app",
|
|
Version: "1.0.0",
|
|
},
|
|
Flavor: v2.Flavor{Name: "small"},
|
|
}
|
|
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
// Completed actions: new app was created before failure
|
|
CompletedActions: []ActionResult{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app",
|
|
Success: true,
|
|
},
|
|
},
|
|
// Failed action: new instance creation failed
|
|
FailedActions: []ActionResult{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app-1.0.0-instance",
|
|
Success: false,
|
|
},
|
|
},
|
|
// Backup of deleted resources
|
|
DeletedAppBackup: &AppBackup{
|
|
App: oldApp,
|
|
Region: "US",
|
|
ManifestContent: "old-manifest-content",
|
|
},
|
|
DeletedInstancesBackup: []InstanceBackup{
|
|
{
|
|
Instance: oldInstance,
|
|
Region: "US",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Mock rollback operations in order:
|
|
// 1. Delete newly created app (rollback create)
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
Return(nil).Once()
|
|
|
|
// 2. Restore old app (from backup)
|
|
mockClient.On("CreateApp", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInput) bool {
|
|
return input.App.Key.Name == "test-app" && input.App.DeploymentManifest == "old-manifest-content"
|
|
})).Return(nil).Once()
|
|
|
|
// 3. Restore old instance (from backup)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInstanceInput) bool {
|
|
return input.AppInst.Key.Name == "test-app-1.0.0-instance"
|
|
})).Return(nil).Once()
|
|
|
|
ctx := context.Background()
|
|
err := manager.RollbackDeployment(ctx, result)
|
|
|
|
require.NoError(t, err)
|
|
mockClient.AssertExpectations(t)
|
|
|
|
// Verify rollback was logged
|
|
assert.Greater(t, len(logger.messages), 0)
|
|
// Should have messages about rolling back created resources and restoring deleted resources
|
|
hasRestoreLog := false
|
|
for _, msg := range logger.messages {
|
|
if strings.Contains(msg, "Restoring deleted resources") {
|
|
hasRestoreLog = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasRestoreLog, "Should log restoration of deleted resources")
|
|
}
|
|
|
|
func TestConvertNetworkRules(t *testing.T) {
|
|
network := &config.NetworkConfig{
|
|
OutboundConnections: []config.OutboundConnection{
|
|
{
|
|
Protocol: "tcp",
|
|
PortRangeMin: 80,
|
|
PortRangeMax: 80,
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
},
|
|
{
|
|
Protocol: "tcp",
|
|
PortRangeMin: 443,
|
|
PortRangeMax: 443,
|
|
RemoteCIDR: "10.0.0.0/8",
|
|
},
|
|
},
|
|
}
|
|
|
|
rules := convertNetworkRules(network)
|
|
require.Len(t, rules, 2)
|
|
|
|
assert.Equal(t, "tcp", rules[0].Protocol)
|
|
assert.Equal(t, 80, rules[0].PortRangeMin)
|
|
assert.Equal(t, 80, rules[0].PortRangeMax)
|
|
assert.Equal(t, "0.0.0.0/0", rules[0].RemoteCIDR)
|
|
|
|
assert.Equal(t, "tcp", rules[1].Protocol)
|
|
assert.Equal(t, 443, rules[1].PortRangeMin)
|
|
assert.Equal(t, 443, rules[1].PortRangeMax)
|
|
assert.Equal(t, "10.0.0.0/8", rules[1].RemoteCIDR)
|
|
}
|