feat(apply): add v1 API support to apply command
Refactor apply command to support both v1 and v2 APIs: - Split internal/apply into v1 and v2 subdirectories - v1: Uses sdk/edgeconnect (from revision/v1 branch) - v2: Uses sdk/edgeconnect/v2 - Update cmd/apply.go to route to appropriate version based on api_version config - Both versions now fully functional with their respective API endpoints Changes: - Created internal/apply/v1/ with v1 SDK implementation - Created internal/apply/v2/ with v2 SDK implementation - Updated cmd/apply.go with runApplyV1() and runApplyV2() functions - Removed validation error that rejected v1 - Apply command now respects --api-version flag and config setting Testing: - V1 with edge.platform: ✅ Generates deployment plan correctly - V2 with orca.platform: ✅ Works as before 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
59ba5ffb02
commit
98a8c4db4a
15 changed files with 3265 additions and 22 deletions
497
internal/apply/v2/manager_test.go
Normal file
497
internal/apply/v2/manager_test.go
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
// 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"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/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 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue