edge-connect-client/internal/apply/manager_test.go

593 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/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) 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{
Organization: "testorg",
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",
},
Spec: config.Spec{
K8sApp: &config.K8sApp{
AppVersion: "1.0.0",
ManifestFile: manifestFile,
},
InfraTemplate: []config.InfraTemplate{
{
Organization: "testorg",
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",
},
},
},
},
}
}
func TestApplyDeploymentSuccess(t *testing.T) {
mockClient := &MockResourceClient{}
logger := &TestLogger{}
manager := NewResourceManager(mockClient, WithLogger(logger))
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)
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))
plan := createTestDeploymentPlan()
config := createTestManagerConfig(t)
// Mock app creation failure
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)
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(), "failed to create application")
mockClient.AssertExpectations(t)
}
func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) {
mockClient := &MockResourceClient{}
logger := &TestLogger{}
manager := NewResourceManager(mockClient, WithLogger(logger), WithRollbackOnFail(true))
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)
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(), "instance actions failed")
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)
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))
// 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{
Organization: "testorg",
Region: "US",
CloudletOrg: "cloudletorg1",
CloudletName: "cloudlet1",
FlavorName: "small",
},
Desired: &InstanceState{Name: "instance1"},
InstanceName: "instance1",
},
{
Type: ActionCreate,
Target: config.InfraTemplate{
Organization: "testorg",
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)
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))
// 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 TestReadManifestFile(t *testing.T) {
manager := &EdgeConnectResourceManager{}
tempDir := t.TempDir()
// Create test file
testFile := filepath.Join(tempDir, "test.yaml")
expectedContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
err := os.WriteFile(testFile, []byte(expectedContent), 0644)
require.NoError(t, err)
content, err := manager.readManifestFile(testFile)
require.NoError(t, err)
assert.Equal(t, expectedContent, content)
// Test empty path
content, err = manager.readManifestFile("")
require.NoError(t, err)
assert.Empty(t, content)
// Test non-existent file
_, err = manager.readManifestFile("/non/existent/file")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to open manifest file")
}
func TestGetDeploymentType(t *testing.T) {
manager := &EdgeConnectResourceManager{}
// Test k8s app
k8sConfig := &config.EdgeConnectConfig{
Spec: config.Spec{
K8sApp: &config.K8sApp{},
},
}
assert.Equal(t, "kubernetes", manager.getDeploymentType(k8sConfig))
// Test docker app
dockerConfig := &config.EdgeConnectConfig{
Spec: config.Spec{
DockerApp: &config.DockerApp{},
},
}
assert.Equal(t, "docker", manager.getDeploymentType(dockerConfig))
}
func TestGetImagePath(t *testing.T) {
manager := &EdgeConnectResourceManager{}
// Test docker app with image
dockerConfig := &config.EdgeConnectConfig{
Spec: config.Spec{
DockerApp: &config.DockerApp{
Image: "my-custom-image:latest",
},
},
}
assert.Equal(t, "my-custom-image:latest", manager.getImagePath(dockerConfig))
// Test k8s app (should use default)
k8sConfig := &config.EdgeConnectConfig{
Spec: config.Spec{
K8sApp: &config.K8sApp{},
},
}
assert.Equal(t, "https://registry-1.docker.io/library/nginx:latest", manager.getImagePath(k8sConfig))
}
func TestConvertNetworkRules(t *testing.T) {
manager := &EdgeConnectResourceManager{}
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 := manager.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)
}
func TestCreateApplicationInput(t *testing.T) {
mockClient := &MockResourceClient{}
manager := NewResourceManager(mockClient)
config := createTestManagerConfig(t)
action := AppAction{
Type: ActionCreate,
Desired: &AppState{
Name: "test-app",
Version: "1.0.0",
Organization: "testorg",
Region: "US",
},
}
// Capture the input passed to CreateApp
var capturedInput *edgeconnect.NewAppInput
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")).
Run(func(args mock.Arguments) {
capturedInput = args.Get(1).(*edgeconnect.NewAppInput)
}).
Return(nil)
ctx := context.Background()
success, err := manager.(*EdgeConnectResourceManager).createApplication(ctx, action, config)
require.NoError(t, err)
assert.True(t, success)
require.NotNil(t, capturedInput)
// Verify the input was constructed correctly
assert.Equal(t, "US", capturedInput.Region)
assert.Equal(t, "testorg", capturedInput.App.Key.Organization)
assert.Equal(t, "test-app", capturedInput.App.Key.Name)
assert.Equal(t, "1.0.0", capturedInput.App.Key.Version)
assert.Equal(t, "kubernetes", capturedInput.App.Deployment)
assert.Equal(t, "ImageTypeDocker", capturedInput.App.ImageType)
assert.True(t, capturedInput.App.AllowServerless)
assert.NotEmpty(t, capturedInput.App.DeploymentManifest)
assert.Len(t, capturedInput.App.RequiredOutboundConnections, 1)
mockClient.AssertExpectations(t)
}