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.
538 lines
16 KiB
Go
538 lines
16 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 apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/infrastructure/config"
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/domain"
|
|
"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 {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockResourceClient) CreateApp(ctx context.Context, region string, app *domain.App) error {
|
|
args := m.Called(ctx, region, app)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error) {
|
|
args := m.Called(ctx, region, appKey)
|
|
return args.Get(0).(*domain.App), args.Error(1)
|
|
}
|
|
|
|
func (m *MockResourceClient) ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error) {
|
|
args := m.Called(ctx, region, appKey)
|
|
return args.Get(0).([]domain.App), args.Error(1)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error {
|
|
args := m.Called(ctx, region, appKey)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateApp(ctx context.Context, region string, app *domain.App) error {
|
|
args := m.Called(ctx, region, app)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
|
args := m.Called(ctx, region, appInst)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error) {
|
|
args := m.Called(ctx, region, appInstKey)
|
|
return args.Get(0).(*domain.AppInstance), args.Error(1)
|
|
}
|
|
|
|
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error) {
|
|
args := m.Called(ctx, region, appInstKey)
|
|
return args.Get(0).([]domain.AppInstance), args.Error(1)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
|
args := m.Called(ctx, region, appInstKey)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
|
args := m.Called(ctx, region, appInst)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
|
args := m.Called(ctx, region, appInstKey)
|
|
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) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo)
|
|
|
|
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) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo,
|
|
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) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock successful operations
|
|
mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
|
|
Return(nil)
|
|
mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
|
|
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)
|
|
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentAppFailure(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock app creation failure - deployment should stop here
|
|
mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
|
|
Return(fmt.Errorf("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")
|
|
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo, WithLogger(logger), WithRollbackOnFail(true), WithStrategyConfig(createTestStrategyConfig()))
|
|
|
|
plan := createTestDeploymentPlan()
|
|
config := createTestManagerConfig(t)
|
|
|
|
// Mock successful app creation but failed instance creation
|
|
mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
|
|
Return(nil)
|
|
mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
|
|
Return(fmt.Errorf("Instance creation failed"))
|
|
|
|
// Mock rollback operations
|
|
mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
|
|
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")
|
|
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
}
|
|
|
|
func TestApplyDeploymentNoActions(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo)
|
|
|
|
// 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")
|
|
|
|
mockAppRepo.AssertNotCalled(t, "CreateApp")
|
|
mockAppInstRepo.AssertNotCalled(t, "CreateAppInstance")
|
|
}
|
|
|
|
func TestApplyDeploymentMultipleInstances(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo, 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
|
|
mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
|
|
Return(nil)
|
|
mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
|
|
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)
|
|
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
}
|
|
|
|
func TestValidatePrerequisites(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo)
|
|
|
|
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) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
logger := &TestLogger{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo, 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
|
|
mockAppInstRepo.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppInstanceKey")).
|
|
Return(nil)
|
|
mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
|
|
Return(nil)
|
|
|
|
ctx := context.Background()
|
|
err := manager.RollbackDeployment(ctx, result)
|
|
|
|
require.NoError(t, err)
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
|
|
// Check rollback was logged
|
|
assert.Greater(t, len(logger.messages), 0)
|
|
}
|
|
|
|
func TestRollbackDeploymentFailure(t *testing.T) {
|
|
mockAppRepo := &MockResourceClient{}
|
|
mockAppInstRepo := &MockResourceClient{}
|
|
manager := NewResourceManager(mockAppRepo, mockAppInstRepo)
|
|
|
|
plan := createTestDeploymentPlan()
|
|
result := &ExecutionResult{
|
|
Plan: plan,
|
|
CompletedActions: []ActionResult{
|
|
{
|
|
Type: ActionCreate,
|
|
Target: "test-app",
|
|
Success: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Mock rollback failure
|
|
mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
|
|
Return(fmt.Errorf("Delete failed"))
|
|
|
|
ctx := context.Background()
|
|
err := manager.RollbackDeployment(ctx, result)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "rollback encountered")
|
|
mockAppRepo.AssertExpectations(t)
|
|
mockAppInstRepo.AssertExpectations(t)
|
|
}
|
|
|
|
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)
|
|
}
|