Skip to content

Commit 7c7bab6

Browse files
Merge branch 'master' into feat/k8s-multi-primary-rollout
2 parents d1674b8 + 223a972 commit 7c7bab6

File tree

13 files changed

+888
-17
lines changed

13 files changed

+888
-17
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2026 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package config
16+
17+
// ECSCanaryRolloutStageOptions contains options for the ECS_CANARY_ROLLOUT stage.
18+
type ECSCanaryRolloutStageOptions struct {
19+
// Scale is the percentage of tasks to run as canary (0-100).
20+
Scale float64 `json:"scale,omitempty"`
21+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2026 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package deployment
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
22+
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
23+
sdk "github.com/pipe-cd/piped-plugin-sdk-go"
24+
25+
ecsconfig "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/ecs/config"
26+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/ecs/provider"
27+
)
28+
29+
const canaryTaskSetMetadataKey = "canary-task-set"
30+
31+
func (p *ECSPlugin) executeECSCanaryRolloutStage(
32+
ctx context.Context,
33+
input *sdk.ExecuteStageInput[ecsconfig.ECSApplicationSpec],
34+
deployTarget *sdk.DeployTarget[ecsconfig.ECSDeployTargetConfig],
35+
) sdk.StageStatus {
36+
lp := input.Client.LogPersister()
37+
38+
cfg, err := input.Request.TargetDeploymentSource.AppConfig()
39+
if err != nil {
40+
lp.Errorf("Failed to load app config: %v", err)
41+
return sdk.StageStatusFailure
42+
}
43+
44+
var options ecsconfig.ECSCanaryRolloutStageOptions
45+
if len(input.Request.StageConfig) > 0 {
46+
if err := json.Unmarshal(input.Request.StageConfig, &options); err != nil {
47+
lp.Errorf("Failed to parse canary rollout stage options: %v", err)
48+
return sdk.StageStatusFailure
49+
}
50+
}
51+
client, err := provider.DefaultRegistry().Client(deployTarget.Name, deployTarget.Config)
52+
if err != nil {
53+
lp.Errorf("Failed to get ECS client for deploy target %s: %v", deployTarget.Name, err)
54+
return sdk.StageStatusFailure
55+
}
56+
57+
taskDef, err := provider.LoadTaskDefinition(input.Request.TargetDeploymentSource.ApplicationDirectory, cfg.Spec.Input.TaskDefinitionFile)
58+
if err != nil {
59+
lp.Errorf("Failed to load task definition: %v", err)
60+
return sdk.StageStatusFailure
61+
}
62+
63+
serviceDef, err := provider.LoadServiceDefinition(
64+
input.Request.TargetDeploymentSource.ApplicationDirectory,
65+
cfg.Spec.Input.ServiceDefinitionFile,
66+
input,
67+
)
68+
if err != nil {
69+
lp.Errorf("Failed to load service definition: %v", err)
70+
return sdk.StageStatusFailure
71+
}
72+
73+
var canary *types.LoadBalancer
74+
if cfg.Spec.Input.AccessType == "ELB" {
75+
_, canary, err = provider.LoadTargetGroups(cfg.Spec.Input.TargetGroups)
76+
if err != nil {
77+
lp.Errorf("Failed to load target groups: %v", err)
78+
return sdk.StageStatusFailure
79+
}
80+
if canary == nil {
81+
lp.Error("Canary target group is required for ELB access type in ECS_CANARY_ROLLOUT stage")
82+
return sdk.StageStatusFailure
83+
}
84+
}
85+
86+
taskSet, err := canaryRollout(ctx, lp, client, taskDef, serviceDef, canary, options.Scale)
87+
if err != nil {
88+
lp.Errorf("Failed to roll out ECS canary task set: %v", err)
89+
return sdk.StageStatusFailure
90+
}
91+
92+
// Persist the canary task set so ECS_CANARY_CLEAN can delete it later
93+
taskSetData, err := json.Marshal(taskSet)
94+
if err != nil {
95+
lp.Errorf("Failed to marshal canary task set for metadata store: %v", err)
96+
return sdk.StageStatusFailure
97+
}
98+
if err := input.Client.PutDeploymentPluginMetadata(ctx, canaryTaskSetMetadataKey, string(taskSetData)); err != nil {
99+
lp.Errorf("Failed to store canary task set to metadata store: %v", err)
100+
return sdk.StageStatusFailure
101+
}
102+
103+
return sdk.StageStatusSuccess
104+
}
105+
106+
// canaryRollout performs the canary rollout workflow:
107+
//
108+
// 1. Registers the task definition
109+
//
110+
// 2. Applies the service definition (creates or updates the service)
111+
//
112+
// 3. Creates a new CANARY task set at specified scale
113+
//
114+
// 4. Waits for the service to reach stable state
115+
func canaryRollout(
116+
ctx context.Context,
117+
lp sdk.StageLogPersister,
118+
client provider.Client,
119+
taskDef types.TaskDefinition,
120+
serviceDef types.Service,
121+
canary *types.LoadBalancer,
122+
scale float64,
123+
) (*types.TaskSet, error) {
124+
lp.Info("Start applying the ECS task definition")
125+
td, err := applyTaskDefinition(ctx, client, taskDef)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to apply task definition: %w", err)
128+
}
129+
130+
lp.Info("Start applying the ECS service definition")
131+
service, err := applyServiceDefinition(ctx, lp, client, serviceDef)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to apply service definition: %w", err)
134+
}
135+
136+
lp.Infof("Creating CANARY task set for service %s at scale %.0f%%", *service.ServiceName, scale)
137+
taskSet, err := client.CreateTaskSet(ctx, *service, *td, canary, scale)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to create canary task set for service %s: %w", *service.ServiceName, err)
140+
}
141+
142+
lp.Infof("Waiting for service %s to reach stable state", *service.ServiceName)
143+
if err := client.WaitServiceStable(ctx, *service.ClusterArn, *service.ServiceName); err != nil {
144+
return nil, fmt.Errorf("service %s did not reach stable state: %w", *service.ServiceName, err)
145+
}
146+
147+
lp.Successf("Successfully rolled out CANARY task set %s for service %s", *taskSet.TaskSetArn, *service.ServiceName)
148+
return taskSet, nil
149+
}

0 commit comments

Comments
 (0)