Skip to content

Commit ea9a453

Browse files
authored
Implement rollback for script run as pre defined stage (#4743)
* Add ROLLBACK_SCRIPT_RUN stage and enable to plan itn Signed-off-by: Yoshiki Fujikane <[email protected]> * Enable to execute multiple rollback stages Signed-off-by: Yoshiki Fujikane <[email protected]> * Add script run rollback logic to k8s app Signed-off-by: Yoshiki Fujikane <[email protected]> * Fix rfc Signed-off-by: Yoshiki Fujikane <[email protected]> * Use log.Info Signed-off-by: Yoshiki Fujikane <[email protected]> --------- Signed-off-by: Yoshiki Fujikane <[email protected]>
1 parent 82540c7 commit ea9a453

File tree

8 files changed

+200
-50
lines changed

8 files changed

+200
-50
lines changed

docs/rfcs/0011-script-run-stage.md

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -90,34 +90,23 @@ spec:
9090
- "curl -X POST -H 'Content-type: application/json' --data '{"text":"failed to deploy: rollback"}' $SLACK_WEBHOOK_URL"
9191
```
9292

93-
**SCRIPT_SYNC stage also rollbacks** when the deployment status is `DeploymentStatus_DEPLOYMENT_CANCELLED` or `DeploymentStatus_DEPLOYMENT_FAILURE` even though other rollback stage is also executed.
93+
**SCRIPT_RUN stage also rollbacks**. Execute the command to rollback SCRIPT_RUN to the point where the deployment was canceled or failed.
94+
When there are multiple SCRIPT_RUN stages to be rolled back, they are executed in the same order as SCRIPT_RUN on the pipeline.
9495

95-
For example, here is a deploy pipeline combined with other k8s stages.
96-
The result status of the pipeline is FAIL or CANCELED, piped rollbacks the stages `K8S_CANARY_ROLLOUT`, `K8S_PRIMARY_ROLLOUT`, and `SCRIPT_RUN`.
96+
For example, consider when deployment proceeds in the following order from 1 to 7.
97+
98+
1. K8S_CANARY_ROLLOUT
99+
2. WAIT
100+
3. SCRIPT_RUN
101+
4. K8S_PRIMARY_ROLLOUT
102+
5. SCRIPT_RUN
103+
6. K8S_CANARY_CLEAN
104+
7. SCRIPT_RUN
105+
106+
Then
107+
- If 4 is canceled or fails while running, only SCRIPT_RUN of 3 will be rolled back.
108+
- If 6 is canceled or fails while running, only SCRIPT_RUNs 3 and 5 will be rolled back.
97109

98-
```yaml
99-
apiVersion: pipecd.dev/v1beta1
100-
kind: KubernetesApp
101-
spec:
102-
pipeline:
103-
stages:
104-
- name: K8S_CANARY_ROLLOUT
105-
with:
106-
replicas: 10%
107-
- name: WAIT_APPROVAL
108-
with:
109-
timeout: 30m
110-
- name: K8S_PRIMARY_ROLLOUT
111-
- name: K8S_CANARY_CLEAN
112-
- name: SCRIPT_RUN
113-
with:
114-
env:
115-
SLACK_WEBHOOK_URL: ""
116-
runs:
117-
- "curl -X POST -H 'Content-type: application/json' --data '{"text":"successfully deployed!!"}' $SLACK_WEBHOOK_URL"
118-
onRollback:
119-
- "curl -X POST -H 'Content-type: application/json' --data '{"text":"failed to deploy: rollback"}' $SLACK_WEBHOOK_URL"
120-
```
121110

122111
## prepare environment for execution
123112

pkg/app/piped/controller/scheduler.go

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -369,34 +369,37 @@ func (s *scheduler) Run(ctx context.Context) error {
369369
// we start rollback stage if the auto-rollback option is true.
370370
if deploymentStatus == model.DeploymentStatus_DEPLOYMENT_CANCELLED ||
371371
deploymentStatus == model.DeploymentStatus_DEPLOYMENT_FAILURE {
372-
if stage, ok := s.deployment.FindRollbackStage(); ok {
372+
373+
if rollbackStages, ok := s.deployment.FindRollbackStages(); ok {
373374
// Update to change deployment status to ROLLING_BACK.
374375
if err := s.reportDeploymentStatusChanged(ctx, model.DeploymentStatus_DEPLOYMENT_ROLLING_BACK, statusReason); err != nil {
375376
return err
376377
}
377378

378-
// Start running rollback stage.
379-
var (
380-
sig, handler = executor.NewStopSignal()
381-
doneCh = make(chan struct{})
382-
)
383-
go func() {
384-
rbs := *stage
385-
rbs.Requires = []string{lastStage.Id}
386-
s.executeStage(sig, rbs, func(in executor.Input) (executor.Executor, bool) {
387-
return s.executorRegistry.RollbackExecutor(s.deployment.Kind, in)
388-
})
389-
close(doneCh)
390-
}()
391-
392-
select {
393-
case <-ctx.Done():
394-
handler.Terminate()
395-
<-doneCh
396-
return nil
397-
398-
case <-doneCh:
399-
break
379+
for _, stage := range rollbackStages {
380+
// Start running rollback stage.
381+
var (
382+
sig, handler = executor.NewStopSignal()
383+
doneCh = make(chan struct{})
384+
)
385+
go func() {
386+
rbs := *stage
387+
rbs.Requires = []string{lastStage.Id}
388+
s.executeStage(sig, rbs, func(in executor.Input) (executor.Executor, bool) {
389+
return s.executorRegistry.RollbackExecutor(s.deployment.Kind, in)
390+
})
391+
close(doneCh)
392+
}()
393+
394+
select {
395+
case <-ctx.Done():
396+
handler.Terminate()
397+
<-doneCh
398+
return nil
399+
400+
case <-doneCh:
401+
break
402+
}
400403
}
401404
}
402405
}
@@ -433,6 +436,24 @@ func (s *scheduler) executeStage(sig executor.StopSignal, ps model.PipelineStage
433436
lp.Complete(time.Minute)
434437
}()
435438

439+
// Check whether to execute the script rollback stage or not.
440+
// If the base stage is executed, the script rollback stage will be executed.
441+
if ps.Name == model.StageScriptRunRollback.String() {
442+
baseStageID := ps.Metadata["baseStageID"]
443+
if baseStageID == "" {
444+
return
445+
}
446+
447+
baseStageStatus, ok := s.stageStatuses[baseStageID]
448+
if !ok {
449+
return
450+
}
451+
452+
if baseStageStatus == model.StageStatus_STAGE_NOT_STARTED_YET || baseStageStatus == model.StageStatus_STAGE_SKIPPED {
453+
return
454+
}
455+
}
456+
436457
// Update stage status to RUNNING if needed.
437458
if model.CanUpdateStageStatus(ps.Status, model.StageStatus_STAGE_RUNNING) {
438459
if err := s.reportStageStatus(ctx, ps.Id, model.StageStatus_STAGE_RUNNING, ps.Requires); err != nil {

pkg/app/piped/executor/kubernetes/rollback.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ package kubernetes
1616

1717
import (
1818
"context"
19+
"encoding/json"
20+
"os"
21+
"os/exec"
1922
"strings"
2023

2124
"go.uber.org/zap"
@@ -27,6 +30,8 @@ import (
2730

2831
type rollbackExecutor struct {
2932
executor.Input
33+
34+
appDir string
3035
}
3136

3237
func (e *rollbackExecutor) Execute(sig executor.StopSignal) model.StageStatus {
@@ -39,7 +44,8 @@ func (e *rollbackExecutor) Execute(sig executor.StopSignal) model.StageStatus {
3944
switch model.Stage(e.Stage.Name) {
4045
case model.StageRollback:
4146
status = e.ensureRollback(ctx)
42-
47+
case model.StageScriptRunRollback:
48+
status = e.ensureScriptRunRollback(ctx)
4349
default:
4450
e.LogPersister.Errorf("Unsupported stage %s for kubernetes application", e.Stage.Name)
4551
return model.StageStatus_STAGE_FAILURE
@@ -74,6 +80,8 @@ func (e *rollbackExecutor) ensureRollback(ctx context.Context) model.StageStatus
7480
}
7581
}
7682

83+
e.appDir = ds.AppDir
84+
7785
loader := provider.NewLoader(e.Deployment.ApplicationName, ds.AppDir, ds.RepoDir, e.Deployment.GitPath.ConfigFilename, appCfg.Input, e.GitClient, e.Logger)
7886
e.Logger.Info("start executing kubernetes stage",
7987
zap.String("stage-name", e.Stage.Name),
@@ -171,3 +179,45 @@ func (e *rollbackExecutor) ensureRollback(ctx context.Context) model.StageStatus
171179
}
172180
return model.StageStatus_STAGE_SUCCESS
173181
}
182+
183+
func (e *rollbackExecutor) ensureScriptRunRollback(ctx context.Context) model.StageStatus {
184+
e.LogPersister.Info("Runnnig commands for rollback...")
185+
186+
onRollback, ok := e.Stage.Metadata["onRollback"]
187+
if !ok {
188+
e.LogPersister.Error("onRollback metadata is missing")
189+
return model.StageStatus_STAGE_FAILURE
190+
}
191+
192+
if onRollback == "" {
193+
e.LogPersister.Info("No commands to run")
194+
return model.StageStatus_STAGE_SUCCESS
195+
}
196+
197+
envStr, ok := e.Stage.Metadata["env"]
198+
env := make(map[string]string, 0)
199+
if ok {
200+
_ = json.Unmarshal([]byte(envStr), &env)
201+
}
202+
203+
for _, v := range strings.Split(onRollback, "\n") {
204+
if v != "" {
205+
e.LogPersister.Infof(" %s", v)
206+
}
207+
}
208+
209+
envs := make([]string, 0, len(env))
210+
for key, value := range env {
211+
envs = append(envs, key+"="+value)
212+
}
213+
214+
cmd := exec.Command("/bin/sh", "-l", "-c", onRollback)
215+
cmd.Dir = e.appDir
216+
cmd.Env = append(os.Environ(), envs...)
217+
cmd.Stdout = e.LogPersister
218+
cmd.Stderr = e.LogPersister
219+
if err := cmd.Run(); err != nil {
220+
return model.StageStatus_STAGE_FAILURE
221+
}
222+
return model.StageStatus_STAGE_SUCCESS
223+
}

pkg/app/piped/planner/kubernetes/pipeline.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package kubernetes
1616

1717
import (
18+
"encoding/json"
1819
"fmt"
1920
"time"
2021

@@ -114,6 +115,31 @@ func buildProgressivePipeline(pp *config.DeploymentPipeline, autoRollback bool,
114115
CreatedAt: now.Unix(),
115116
UpdatedAt: now.Unix(),
116117
})
118+
119+
// Add a stage for rolling back script run stages.
120+
for i, s := range pp.Stages {
121+
if s.Name == model.StageScriptRun {
122+
// Use metadata as a way to pass parameters to the stage.
123+
envStr, _ := json.Marshal(s.ScriptRunStageOptions.Env)
124+
metadata := map[string]string{
125+
"baseStageID": out[i].Id,
126+
"onRollback": s.ScriptRunStageOptions.OnRollback,
127+
"env": string(envStr),
128+
}
129+
ss, _ := planner.GetPredefinedStage(planner.PredefinedStageScriptRunRollback)
130+
out = append(out, &model.PipelineStage{
131+
Id: ss.ID,
132+
Name: ss.Name.String(),
133+
Desc: ss.Desc,
134+
Predefined: true,
135+
Visible: false,
136+
Status: model.StageStatus_STAGE_NOT_STARTED_YET,
137+
Metadata: metadata,
138+
CreatedAt: now.Unix(),
139+
UpdatedAt: now.Unix(),
140+
})
141+
}
142+
}
117143
}
118144

119145
return out

pkg/app/piped/planner/predefined_stages.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
PredefinedStageECSSync = "ECSSync"
2828
PredefinedStageRollback = "Rollback"
2929
PredefinedStageCustomSyncRollback = "CustomSyncRollback"
30+
PredefinedStageScriptRunRollback = "ScriptRunRollback"
3031
)
3132

3233
var predefinedStages = map[string]config.PipelineStage{
@@ -65,6 +66,11 @@ var predefinedStages = map[string]config.PipelineStage{
6566
Name: model.StageCustomSyncRollback,
6667
Desc: "Rollback the custom stages",
6768
},
69+
PredefinedStageScriptRunRollback: {
70+
ID: PredefinedStageScriptRunRollback,
71+
Name: model.StageScriptRunRollback,
72+
Desc: "Rollback the script run stage",
73+
},
6874
}
6975

7076
// GetPredefinedStage finds and returns the predefined stage for the given id.

pkg/model/deployment.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ func (d *Deployment) FindRollbackStage() (*PipelineStage, bool) {
154154
return nil, false
155155
}
156156

157+
func (d *Deployment) FindRollbackStages() ([]*PipelineStage, bool) {
158+
rollbackStages := make([]*PipelineStage, 0, len(d.Stages))
159+
for i, stage := range d.Stages {
160+
if d.Stages[i].Name == StageRollback.String() || d.Stages[i].Name == StageScriptRunRollback.String() {
161+
rollbackStages = append(rollbackStages, stage)
162+
}
163+
}
164+
return rollbackStages, len(rollbackStages) > 0
165+
}
166+
157167
// DeploymentStatusesFromStrings converts a list of strings to list of DeploymentStatus.
158168
func DeploymentStatusesFromStrings(statuses []string) ([]DeploymentStatus, error) {
159169
out := make([]DeploymentStatus, 0, len(statuses))

pkg/model/deployment_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ func TestFindRollbackStage(t *testing.T) {
587587
}
588588

589589
for _, tt := range tests {
590+
tt := tt
590591
t.Run(tt.name, func(t *testing.T) {
591592
d := &Deployment{
592593
Stages: tt.stages,
@@ -597,3 +598,46 @@ func TestFindRollbackStage(t *testing.T) {
597598
})
598599
}
599600
}
601+
602+
func TestFindRollbackStags(t *testing.T) {
603+
tests := []struct {
604+
name string
605+
stages []*PipelineStage
606+
wantStages []*PipelineStage
607+
wantStageFound bool
608+
}{
609+
{
610+
name: "found",
611+
stages: []*PipelineStage{
612+
{Name: StageK8sSync.String()},
613+
{Name: StageRollback.String()},
614+
{Name: StageScriptRunRollback.String()},
615+
},
616+
wantStages: []*PipelineStage{
617+
{Name: StageRollback.String()},
618+
{Name: StageScriptRunRollback.String()},
619+
},
620+
wantStageFound: true,
621+
},
622+
{
623+
name: "not found",
624+
stages: []*PipelineStage{
625+
{Name: StageK8sSync.String()},
626+
},
627+
wantStages: []*PipelineStage{},
628+
wantStageFound: false,
629+
},
630+
}
631+
632+
for _, tt := range tests {
633+
tt := tt
634+
t.Run(tt.name, func(t *testing.T) {
635+
d := &Deployment{
636+
Stages: tt.stages,
637+
}
638+
stages, found := d.FindRollbackStages()
639+
assert.Equal(t, tt.wantStages, stages)
640+
assert.Equal(t, tt.wantStageFound, found)
641+
})
642+
}
643+
}

pkg/model/stage.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ const (
108108
// all changes made by the CUSTOM_SYNC stage will be reverted to
109109
// bring back the pre-deploy stage.
110110
StageCustomSyncRollback Stage = "CUSTOM_SYNC_ROLLBACK"
111+
// StageScriptRunRollback represents a state where
112+
// all changes made by the SCRIPT_RUN_ROLLBACK stage will be reverted to
113+
// bring back the pre-deploy stage.
114+
StageScriptRunRollback Stage = "SCRIPT_RUN_ROLLBACK"
111115
)
112116

113117
func (s Stage) String() string {

0 commit comments

Comments
 (0)