497 lines
14 KiB
Go
497 lines
14 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/config"
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
|
"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 *edgeconnect.NewAppInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error {
|
|
args := m.Called(ctx, appKey, region)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error {
|
|
args := m.Called(ctx, input)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.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("*edgeconnect.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.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("*edgeconnect.NewAppInput")).
|
|
Return(&edgeconnect.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("*edgeconnect.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")).
|
|
Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})
|
|
|
|
// Mock rollback operations
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.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("*edgeconnect.NewAppInput")).
|
|
Return(nil)
|
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.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("edgeconnect.AppInstanceKey"), "US").
|
|
Return(nil)
|
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.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("edgeconnect.AppKey"), "US").
|
|
Return(&edgeconnect.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 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)
|
|
}
|