Skip to content

Commit ebdc746

Browse files
authored
Scriptrun Rollback Implementation (#6094)
* rollback impl Signed-off-by: hiep-tk <[email protected]> * refactor logic for scriptrun rollback Signed-off-by: hiep-tk <[email protected]> * lint Signed-off-by: hiep-tk <[email protected]> * update test case Signed-off-by: hiep-tk <[email protected]> * update test case. fix type Signed-off-by: hiep-tk <[email protected]> * update metadata method signature to match with sdk Signed-off-by: hiep-tk <[email protected]> --------- Signed-off-by: hiep-tk <[email protected]>
1 parent 560f8c5 commit ebdc746

File tree

4 files changed

+150
-27
lines changed

4 files changed

+150
-27
lines changed

pkg/app/pipedv1/plugin/scriptrun/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ go 1.24.1
44

55
require (
66
github.com/creasty/defaults v1.6.0
7-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250728033142-7a6a214b39f7
7+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250807000858-21595f74c628
88
github.com/stretchr/testify v1.10.0
9-
go.uber.org/zap v1.19.1
109
)
1110

1211
require (
@@ -32,7 +31,7 @@ require (
3231
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3332
github.com/kr/text v0.2.0 // indirect
3433
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
35-
github.com/pipe-cd/pipecd v0.52.1-0.20250722035702-5722fabb80ce // indirect
34+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 // indirect
3635
github.com/pmezard/go-difflib v1.0.0 // indirect
3736
github.com/prometheus/client_golang v1.12.1 // indirect
3837
github.com/prometheus/client_model v0.5.0 // indirect
@@ -47,6 +46,7 @@ require (
4746
go.opentelemetry.io/otel/trace v1.28.0 // indirect
4847
go.uber.org/atomic v1.11.0 // indirect
4948
go.uber.org/multierr v1.6.0 // indirect
49+
go.uber.org/zap v1.19.1 // indirect
5050
go.yaml.in/yaml/v2 v2.4.2 // indirect
5151
golang.org/x/crypto v0.36.0 // indirect
5252
golang.org/x/net v0.38.0 // indirect

pkg/app/pipedv1/plugin/scriptrun/go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
209209
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
210210
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
211211
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
212-
github.com/pipe-cd/pipecd v0.52.1-0.20250722035702-5722fabb80ce h1:zyYJ+lIC3oLya2ZoNL90TdDI6IkQVL7aptkT1f9I69U=
213-
github.com/pipe-cd/pipecd v0.52.1-0.20250722035702-5722fabb80ce/go.mod h1:5H0ydj0eUpGnJOesA2GPU3mTVlZEZDb8cNP7/lvNPTU=
214-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250728024352-18775906c499 h1:mLZ6EDH4h6tQai3/1QHcT/7oSgol73ar1bC84cN7x6E=
215-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250728024352-18775906c499/go.mod h1:v+kzPB2Tom8uEIObY5Le0aBpVLtedwNhMsq2U+dovpg=
216-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250728033142-7a6a214b39f7 h1:AGxK4Ffb4MKW8Fda150hm/H6P/ulFeOrslrzUox5iBs=
217-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250728033142-7a6a214b39f7/go.mod h1:v+kzPB2Tom8uEIObY5Le0aBpVLtedwNhMsq2U+dovpg=
212+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 h1:1VM6ZkE2YfXqROq3lU8xrOV21MdJ257p19VX71E/nsU=
213+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5/go.mod h1:5H0ydj0eUpGnJOesA2GPU3mTVlZEZDb8cNP7/lvNPTU=
214+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250807000858-21595f74c628 h1:qBKzGbprq7dlpZJ0zLazEbamCeoaT8G0afkZsC0ipzM=
215+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250807000858-21595f74c628/go.mod h1:JjOYv2tMx72fvLpe88KG8cvrlHiI5XKYeZBvdDO3g80=
218216
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
219217
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
220218
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

pkg/app/pipedv1/plugin/scriptrun/plugin.go

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
const (
3030
stageScriptRun = "SCRIPT_RUN"
3131
stageScriptRunRollback = "SCRIPT_RUN_ROLLBACK"
32+
metadataKeyPrefix = "started-"
33+
nonEmptyValue = "_"
3234
)
3335

3436
type ContextInfo struct {
@@ -81,27 +83,67 @@ func (p *plugin) ExecuteStage(ctx context.Context, _ sdk.ConfigNone, _ sdk.Deplo
8183
switch input.Request.StageName {
8284
case stageScriptRun:
8385
return &sdk.ExecuteStageResponse{
84-
Status: executeScriptRun(ctx, input.Request, input.Client.LogPersister()),
86+
Status: executeScriptRun(ctx, input.Request, input.Client.LogPersister(), input.Client),
8587
}, nil
8688
case stageScriptRunRollback:
87-
panic("unimplemented")
89+
return &sdk.ExecuteStageResponse{
90+
Status: executeRollback(ctx, input.Request, input.Client.LogPersister(), input.Client),
91+
}, nil
8892
}
8993
return nil, fmt.Errorf("unsupported stage %s", input.Request.StageName)
9094
}
9195

92-
func executeScriptRun(ctx context.Context, request sdk.ExecuteStageRequest[struct{}], lp sdk.StageLogPersister) sdk.StageStatus {
96+
func executeScriptRun(ctx context.Context, request sdk.ExecuteStageRequest[struct{}], lp sdk.StageLogPersister, metadataStore deploymentMetadataStore) sdk.StageStatus {
9397
lp.Infof("Start executing the script run stage")
9498
opts, err := decode(request.StageConfig)
9599
if err != nil {
96100
lp.Errorf("failed to decode the stage config: %v", err)
97101
return sdk.StageStatusFailure
98102
}
103+
// need to store the index of which stage that's already run so only their respective rollback stages are triggered
104+
if err = metadataStore.PutDeploymentPluginMetadata(ctx, metadataKeyPrefix+strconv.Itoa(request.StageIndex), nonEmptyValue); err != nil {
105+
lp.Errorf("failed to put metadata to mark the stage as started: %v", err)
106+
return sdk.StageStatusFailure
107+
}
99108
if opts.Run == "" {
100109
return sdk.StageStatusSuccess
101110
}
102111
c := make(chan sdk.StageStatus, 1)
103112
go func() {
104-
c <- executeCommand(opts, request, lp)
113+
c <- executeCommand(opts.Run, opts.Env, request, lp)
114+
}()
115+
select {
116+
case result := <-c:
117+
return result
118+
case <-ctx.Done():
119+
lp.Info("ScriptRun cancelled")
120+
// We can return any status here because the piped handles this case as cancelled by a user,
121+
// ignoring the result from a plugin.
122+
return sdk.StageStatusFailure
123+
}
124+
}
125+
func executeRollback(ctx context.Context, request sdk.ExecuteStageRequest[struct{}], lp sdk.StageLogPersister, metadataStore deploymentMetadataStore) sdk.StageStatus {
126+
lp.Infof("Start executing the script run rollback stage")
127+
opts, err := decode(request.StageConfig)
128+
if err != nil {
129+
lp.Errorf("failed to decode the stage config: %v", err)
130+
return sdk.StageStatusFailure
131+
}
132+
_, found, err := metadataStore.GetDeploymentPluginMetadata(ctx, metadataKeyPrefix+strconv.Itoa(request.StageIndex))
133+
if err != nil {
134+
lp.Errorf("failed to retrieve stage run status metadata: %v", err)
135+
return sdk.StageStatusFailure
136+
}
137+
if !found {
138+
lp.Infof("skip this rollback stage because the SCRIPT_RUN stage of index %d has not run", request.StageIndex)
139+
return sdk.StageStatusSuccess
140+
}
141+
if opts.OnRollback == "" {
142+
return sdk.StageStatusSuccess
143+
}
144+
c := make(chan sdk.StageStatus, 1)
145+
go func() {
146+
c <- executeCommand(opts.OnRollback, opts.Env, request, lp)
105147
}()
106148
select {
107149
case result := <-c:
@@ -116,10 +158,9 @@ func executeScriptRun(ctx context.Context, request sdk.ExecuteStageRequest[struc
116158
func (p *plugin) FetchDefinedStages() []string {
117159
return []string{stageScriptRun, stageScriptRunRollback}
118160
}
119-
120-
func executeCommand(opts scriptRunStageOptions, request sdk.ExecuteStageRequest[struct{}], lp sdk.StageLogPersister) sdk.StageStatus {
161+
func executeCommand(commands string, customEnv map[string]string, request sdk.ExecuteStageRequest[struct{}], lp sdk.StageLogPersister) sdk.StageStatus {
121162
lp.Infof("Running commands...")
122-
for _, v := range strings.Split(opts.Run, "\n") {
163+
for _, v := range strings.Split(commands, "\n") {
123164
if v != "" {
124165
lp.Infof(" %s", v)
125166
}
@@ -141,16 +182,16 @@ func executeCommand(opts scriptRunStageOptions, request sdk.ExecuteStageRequest[
141182
lp.Errorf("failed to encode the stage config: %v", err)
142183
return sdk.StageStatusFailure
143184
}
144-
envs := make([]string, 0, len(ciEnv)+len(opts.Env))
185+
envs := make([]string, 0, len(ciEnv)+len(customEnv))
145186
for key, value := range ciEnv {
146187
envs = append(envs, key+"="+value)
147188
}
148189

149-
for key, value := range opts.Env {
190+
for key, value := range customEnv {
150191
envs = append(envs, key+"="+value)
151192
}
152193

153-
cmd := exec.Command("/bin/sh", "-l", "-c", opts.Run)
194+
cmd := exec.Command("/bin/sh", "-l", "-c", commands)
154195
cmd.Env = append(os.Environ(), envs...)
155196
cmd.Dir = request.TargetDeploymentSource.ApplicationDirectory
156197
cmd.Stdout = lp
@@ -186,3 +227,8 @@ func (ci *ContextInfo) buildEnv() (map[string]string, error) {
186227
}
187228
return envs, nil
188229
}
230+
231+
type deploymentMetadataStore interface {
232+
GetDeploymentPluginMetadata(ctx context.Context, key string) (string, bool, error)
233+
PutDeploymentPluginMetadata(ctx context.Context, key string, value string) error
234+
}

pkg/app/pipedv1/plugin/scriptrun/plugin_test.go

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package main
1616

1717
import (
18+
"context"
1819
"encoding/json"
1920
"testing"
2021

@@ -23,6 +24,22 @@ import (
2324
"github.com/stretchr/testify/assert"
2425
)
2526

27+
type mockDeploymentMetadataStore struct {
28+
metadata map[string]string
29+
}
30+
31+
func (m *mockDeploymentMetadataStore) GetDeploymentPluginMetadata(_ context.Context, key string) (string, bool, error) {
32+
metadata, ok := m.metadata[key]
33+
if !ok {
34+
return "", false, nil
35+
}
36+
return metadata, true, nil
37+
}
38+
39+
func (m *mockDeploymentMetadataStore) PutDeploymentPluginMetadata(_ context.Context, key string, value string) error {
40+
m.metadata[key] = value
41+
return nil
42+
}
2643
func TestBuildPipelineSyncStages(t *testing.T) {
2744
t.Parallel()
2845
p := &plugin{}
@@ -215,10 +232,11 @@ func Test_ContextInfo_BuildEnv(t *testing.T) {
215232
func TestPlugin_ExecuteScriptRun(t *testing.T) {
216233
t.Parallel()
217234
testcases := []struct {
218-
name string
219-
req sdk.ExecuteStageRequest[struct{}]
220-
lp sdk.StageLogPersister
221-
want sdk.StageStatus
235+
name string
236+
req sdk.ExecuteStageRequest[struct{}]
237+
lp sdk.StageLogPersister
238+
metadataStore mockDeploymentMetadataStore
239+
want sdk.StageStatus
222240
}{
223241
{
224242
name: "success",
@@ -230,7 +248,10 @@ func TestPlugin_ExecuteScriptRun(t *testing.T) {
230248
ApplicationID: "app-1",
231249
},
232250
},
233-
lp: logpersistertest.NewTestLogPersister(t),
251+
lp: logpersistertest.NewTestLogPersister(t),
252+
metadataStore: mockDeploymentMetadataStore{
253+
metadata: map[string]string{},
254+
},
234255
want: sdk.StageStatusSuccess,
235256
},
236257
{
@@ -243,7 +264,10 @@ func TestPlugin_ExecuteScriptRun(t *testing.T) {
243264
ApplicationID: "app-2",
244265
},
245266
},
246-
lp: logpersistertest.NewTestLogPersister(t),
267+
lp: logpersistertest.NewTestLogPersister(t),
268+
metadataStore: mockDeploymentMetadataStore{
269+
metadata: map[string]string{},
270+
},
247271
want: sdk.StageStatusFailure,
248272
},
249273
{
@@ -256,14 +280,69 @@ func TestPlugin_ExecuteScriptRun(t *testing.T) {
256280
ApplicationID: "app-3",
257281
},
258282
},
259-
lp: logpersistertest.NewTestLogPersister(t),
283+
lp: logpersistertest.NewTestLogPersister(t),
284+
metadataStore: mockDeploymentMetadataStore{
285+
metadata: map[string]string{},
286+
},
260287
want: sdk.StageStatusFailure,
261288
},
262289
}
263290
for _, tc := range testcases {
264291
t.Run(tc.name, func(t *testing.T) {
265292
t.Parallel()
266-
resp := executeScriptRun(t.Context(), tc.req, tc.lp)
293+
resp := executeScriptRun(t.Context(), tc.req, tc.lp, &tc.metadataStore)
294+
assert.Equal(t, tc.want, resp)
295+
})
296+
}
297+
}
298+
func TestPlugin_ExecuteRollback(t *testing.T) {
299+
t.Parallel()
300+
testcases := []struct {
301+
name string
302+
req sdk.ExecuteStageRequest[struct{}]
303+
lp sdk.StageLogPersister
304+
metadataStore mockDeploymentMetadataStore
305+
want sdk.StageStatus
306+
}{
307+
{
308+
name: "rollback run if its scriptrun record exists",
309+
req: sdk.ExecuteStageRequest[struct{}]{
310+
StageName: stageScriptRunRollback,
311+
StageIndex: 1,
312+
StageConfig: []byte(`{"run": "echo 4", "onRollback": "echo 'rollback'"}`),
313+
Deployment: sdk.Deployment{
314+
ID: "deployment-4",
315+
ApplicationID: "app-4",
316+
},
317+
},
318+
lp: logpersistertest.NewTestLogPersister(t),
319+
metadataStore: mockDeploymentMetadataStore{
320+
metadata: map[string]string{metadataKeyPrefix + "1": nonEmptyValue},
321+
},
322+
want: sdk.StageStatusSuccess,
323+
},
324+
{
325+
name: "rollback should not run if its scriptrun record does not exist",
326+
req: sdk.ExecuteStageRequest[struct{}]{
327+
StageName: stageScriptRunRollback,
328+
StageIndex: 1,
329+
StageConfig: []byte(`{"run": "echo 5","onRollback": "exit 1"}`),
330+
Deployment: sdk.Deployment{
331+
ID: "deployment-5",
332+
ApplicationID: "app-5",
333+
},
334+
},
335+
lp: logpersistertest.NewTestLogPersister(t),
336+
metadataStore: mockDeploymentMetadataStore{
337+
metadata: map[string]string{},
338+
},
339+
want: sdk.StageStatusSuccess,
340+
},
341+
}
342+
for _, tc := range testcases {
343+
t.Run(tc.name, func(t *testing.T) {
344+
t.Parallel()
345+
resp := executeScriptRun(t.Context(), tc.req, tc.lp, &tc.metadataStore)
267346
assert.Equal(t, tc.want, resp)
268347
})
269348
}

0 commit comments

Comments
 (0)