Skip to content

Commit dbdb812

Browse files
committed
feat: implement ecs canary clean stage
Signed-off-by: Hoang Ngo <adlehoang118@gmail.com>
1 parent 3a9bff4 commit dbdb812

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

pkg/app/pipedv1/plugin/ecs/deployment/canary.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package deployment
1717
import (
1818
"context"
1919
"encoding/json"
20+
"errors"
2021
"fmt"
2122

2223
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
@@ -147,3 +148,56 @@ func canaryRollout(
147148
lp.Successf("Successfully rolled out CANARY task set %s for service %s", *taskSet.TaskSetArn, *service.ServiceName)
148149
return taskSet, nil
149150
}
151+
152+
func (p *ECSPlugin) executeECSCanaryCleanStage(
153+
ctx context.Context,
154+
input *sdk.ExecuteStageInput[ecsconfig.ECSApplicationSpec],
155+
deployTarget *sdk.DeployTarget[ecsconfig.ECSDeployTargetConfig],
156+
) sdk.StageStatus {
157+
lp := input.Client.LogPersister()
158+
159+
taskSetData, found, err := input.Client.GetDeploymentPluginMetadata(ctx, canaryTaskSetMetadataKey)
160+
if err != nil {
161+
lp.Errorf("Failed to retrieve canary task set from metadata store: %v", err)
162+
return sdk.StageStatusFailure
163+
}
164+
if !found {
165+
lp.Info("No canary task set found in metadata store, nothing to clean up")
166+
return sdk.StageStatusSuccess
167+
}
168+
169+
var taskSet types.TaskSet
170+
if err := json.Unmarshal([]byte(taskSetData), &taskSet); err != nil {
171+
lp.Errorf("Failed to unmarshal canary task set from metadata store: %v", err)
172+
return sdk.StageStatusFailure
173+
}
174+
175+
client, err := provider.DefaultRegistry().Client(deployTarget.Name, deployTarget.Config)
176+
if err != nil {
177+
lp.Errorf("Failed to get ECS client for deploy target %s: %v", deployTarget.Name, err)
178+
return sdk.StageStatusFailure
179+
}
180+
181+
if err := canaryClean(ctx, lp, client, taskSet); err != nil {
182+
lp.Errorf("Failed to clean up ECS canary task set: %v", err)
183+
return sdk.StageStatusFailure
184+
}
185+
186+
return sdk.StageStatusSuccess
187+
}
188+
189+
// canaryClean deletes the canary task set
190+
func canaryClean(ctx context.Context, lp sdk.StageLogPersister, client provider.Client, taskSet types.TaskSet) error {
191+
lp.Infof("Deleting canary task set %s", *taskSet.TaskSetArn)
192+
if err := client.DeleteTaskSet(ctx, taskSet); err != nil {
193+
// If the task set is already gone, treat as success
194+
var notFound *types.TaskSetNotFoundException
195+
if errors.As(err, &notFound) {
196+
lp.Infof("Canary task set %s already deleted, skipping", *taskSet.TaskSetArn)
197+
return nil
198+
}
199+
return fmt.Errorf("failed to delete canary task set %s: %w", *taskSet.TaskSetArn, err)
200+
}
201+
lp.Successf("Successfully deleted canary task set %s", *taskSet.TaskSetArn)
202+
return nil
203+
}

pkg/app/pipedv1/plugin/ecs/deployment/canary_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,71 @@ import (
2525
"github.com/stretchr/testify/require"
2626
)
2727

28+
func TestCanaryClean(t *testing.T) {
29+
t.Parallel()
30+
31+
var (
32+
tsArn = "arn:aws:ecs:us-east-1:123456789012:task-set/my-cluster/my-service/ecs-svc:1"
33+
taskSet = types.TaskSet{
34+
TaskSetArn: aws.String(tsArn),
35+
}
36+
)
37+
38+
testcases := []struct {
39+
name string
40+
taskSet types.TaskSet
41+
client *mockECSClient
42+
wantErr bool
43+
wantErrMsg string
44+
}{
45+
{
46+
name: "success: canary task set is deleted",
47+
taskSet: taskSet,
48+
client: &mockECSClient{
49+
DeleteTaskSetFunc: func(_ context.Context, ts types.TaskSet) error {
50+
assert.Equal(t, tsArn, aws.ToString(ts.TaskSetArn))
51+
return nil
52+
},
53+
},
54+
},
55+
{
56+
name: "success: task set already deleted (idempotent retry)",
57+
taskSet: taskSet,
58+
client: &mockECSClient{
59+
DeleteTaskSetFunc: func(_ context.Context, _ types.TaskSet) error {
60+
return &types.TaskSetNotFoundException{}
61+
},
62+
},
63+
},
64+
{
65+
name: "fail: DeleteTaskSet error",
66+
taskSet: taskSet,
67+
client: &mockECSClient{
68+
DeleteTaskSetFunc: func(_ context.Context, _ types.TaskSet) error {
69+
return errors.New("delete error")
70+
},
71+
},
72+
wantErr: true,
73+
wantErrMsg: "failed to delete canary task set",
74+
},
75+
}
76+
77+
for _, tc := range testcases {
78+
t.Run(tc.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
err := canaryClean(context.Background(), &fakeLogPersister{}, tc.client, tc.taskSet)
82+
83+
if tc.wantErr {
84+
require.Error(t, err)
85+
assert.Contains(t, err.Error(), tc.wantErrMsg)
86+
return
87+
}
88+
require.NoError(t, err)
89+
})
90+
}
91+
}
92+
2893
func TestCanaryRollout(t *testing.T) {
2994
t.Parallel()
3095

pkg/app/pipedv1/plugin/ecs/deployment/plugin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ func (p *ECSPlugin) ExecuteStage(
8181
return &sdk.ExecuteStageResponse{
8282
Status: p.executeECSCanaryRolloutStage(ctx, input, deployTargets[0]),
8383
}, nil
84+
case StageECSCanaryClean:
85+
return &sdk.ExecuteStageResponse{
86+
Status: p.executeECSCanaryCleanStage(ctx, input, deployTargets[0]),
87+
}, nil
8488
case StageECSTrafficRouting:
8589
return &sdk.ExecuteStageResponse{
8690
Status: p.executeECSTrafficRouting(ctx, input, deployTargets[0]),

0 commit comments

Comments
 (0)