Skip to content

Commit 19d7b18

Browse files
committed
Add sequential action
1 parent 53c2be5 commit 19d7b18

File tree

11 files changed

+709
-39
lines changed

11 files changed

+709
-39
lines changed

.mockery.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ packages:
8888
dir: mocks/generated/run/actions/action/restart
8989
interfaces:
9090
Maker: {}
91+
github.com/wstool/wst/run/actions/action/sequential:
92+
config:
93+
dir: mocks/generated/run/actions/action/sequential
94+
interfaces:
95+
Maker: {}
9196
github.com/wstool/wst/run/actions/action/start:
9297
config:
9398
dir: mocks/generated/run/actions/action/start

TODO.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ in the future.
4747

4848
#### Structure - Instances, Actions, Servers, Services
4949

50-
- introduce sequential action for more complex scenarios (e.g. seq task in parallel action)
51-
- might be worth to consider whether top action should be wrapped to reduce code needed
5250
- custom server actions for sequential action
5351
- useful to wrap multiple action - e.g. fpm start + expectations
5452
- introduce new command action and make it work with output

conf/parser/factory/actions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ func (f *NativeActionsFactory) parseAction(
172172
restartAction := &types.RestartAction{Service: meta.serviceName}
173173
err = f.structParser(data, restartAction, path)
174174
action = restartAction
175+
case "sequential":
176+
serviceNameAllowed = false
177+
sequentialAction := &types.SequentialAction{}
178+
err = f.structParser(data, sequentialAction, path)
179+
action = sequentialAction
175180
case "start":
176181
startAction := &types.StartAction{Service: meta.serviceName}
177182
err = f.structParser(data, startAction, path)

conf/parser/factory/actions_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,29 @@ func TestNativeActionsFactory_ParseActions(t *testing.T) {
589589
},
590590
wantErr: false,
591591
},
592+
{
593+
name: "Valid sequential action",
594+
actions: []interface{}{
595+
map[string]interface{}{
596+
"sequential": map[string]interface{}{"actions": []map[string]interface{}{}},
597+
},
598+
},
599+
mockParseCalls: []struct {
600+
data map[string]interface{}
601+
path string
602+
err error
603+
}{
604+
{
605+
data: map[string]interface{}{"actions": []map[string]interface{}{}},
606+
path: staticPath,
607+
err: nil,
608+
},
609+
},
610+
want: []types.Action{
611+
&types.SequentialAction{},
612+
},
613+
wantErr: false,
614+
},
592615
{
593616
name: "Valid start action",
594617
actions: []interface{}{

conf/types/action.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ type ParallelAction struct {
114114
When string `wst:"when,enum=always|on_success|on_fail,default=on_success"`
115115
}
116116

117+
type SequentialAction struct {
118+
Actions []Action `wst:"actions,factory=createActions"`
119+
Timeout int `wst:"timeout"`
120+
When string `wst:"when,enum=always|on_success|on_fail,default=on_success"`
121+
}
122+
117123
type NotAction struct {
118124
Action Action `wst:"action,factory=createAction"`
119125
Timeout int `wst:"timeout"`

mocks/generated/run/actions/action/sequential/mock_Maker.go

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

run/actions/action/parallel/parallel_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,21 +275,21 @@ func TestAction_Execute(t *testing.T) {
275275
runDataMock := runtimeMocks.NewMockData(t)
276276
runMakerMock := runtimeMocks.NewMockMaker(t)
277277
runMakerMock.On("MakeContextWithTimeout", baseCtx, timeout).Return(actCtx, cancel)
278-
actionMocks := []*actionMocks.MockAction{
278+
actMocks := []*actionMocks.MockAction{
279279
actionMocks.NewMockAction(t),
280280
actionMocks.NewMockAction(t),
281281
actionMocks.NewMockAction(t),
282282
}
283-
for _, actionMock := range actionMocks {
284-
actionMock.On("Timeout").Return(timeout).Maybe()
283+
for _, act := range actMocks {
284+
act.On("Timeout").Return(timeout)
285285
}
286286

287287
mockLogger := external.NewMockLogger()
288288
fndMock.On("Logger").Return(mockLogger.SugaredLogger)
289289

290-
tt.setupMocks(t, fndMock, actionMocks, runDataMock, actCtx)
290+
tt.setupMocks(t, fndMock, actMocks, runDataMock, actCtx)
291291

292-
actions := []action.Action{actionMocks[0], actionMocks[1], actionMocks[2]}
292+
actions := []action.Action{actMocks[0], actMocks[1], actMocks[2]}
293293

294294
a := &Action{
295295
fnd: fndMock,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2024 Jakub Zelenka and The WST 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 sequential
16+
17+
import (
18+
"context"
19+
"github.com/pkg/errors"
20+
"github.com/wstool/wst/app"
21+
"github.com/wstool/wst/conf/types"
22+
"github.com/wstool/wst/run/actions/action"
23+
"github.com/wstool/wst/run/instances/runtime"
24+
"github.com/wstool/wst/run/services"
25+
"time"
26+
)
27+
28+
type Maker interface {
29+
Make(
30+
config *types.SequentialAction,
31+
sl services.ServiceLocator,
32+
defaultTimeout int,
33+
actionMaker action.Maker,
34+
) (action.Action, error)
35+
}
36+
37+
type ActionMaker struct {
38+
fnd app.Foundation
39+
runtimeMaker runtime.Maker
40+
}
41+
42+
func CreateActionMaker(fnd app.Foundation, runtimeMaker runtime.Maker) *ActionMaker {
43+
return &ActionMaker{
44+
fnd: fnd,
45+
runtimeMaker: runtimeMaker,
46+
}
47+
}
48+
49+
func (m *ActionMaker) Make(
50+
config *types.SequentialAction,
51+
sl services.ServiceLocator,
52+
defaultTimeout int,
53+
actionMaker action.Maker,
54+
) (action.Action, error) {
55+
if config.Timeout == 0 {
56+
config.Timeout = defaultTimeout
57+
}
58+
59+
var sequentialActions []action.Action
60+
for _, configAction := range config.Actions {
61+
newAction, err := actionMaker.MakeAction(configAction, sl, config.Timeout)
62+
if err != nil {
63+
return nil, err
64+
}
65+
sequentialActions = append(sequentialActions, newAction)
66+
}
67+
return &Action{
68+
fnd: m.fnd,
69+
runtimeMaker: m.runtimeMaker,
70+
actions: sequentialActions,
71+
timeout: time.Duration(config.Timeout * 1e6),
72+
when: action.When(config.When),
73+
}, nil
74+
}
75+
76+
type Action struct {
77+
fnd app.Foundation
78+
runtimeMaker runtime.Maker
79+
actions []action.Action
80+
timeout time.Duration
81+
when action.When
82+
}
83+
84+
func (a *Action) When() action.When {
85+
return a.when
86+
}
87+
88+
func (a *Action) Timeout() time.Duration {
89+
return a.timeout
90+
}
91+
92+
func (a *Action) Execute(ctx context.Context, runData runtime.Data) (bool, error) {
93+
logger := a.fnd.Logger()
94+
logger.Infof("Executing sequential action")
95+
96+
failedActionsCount := 0
97+
var lastErr error = nil
98+
for pos, act := range a.actions {
99+
// Execute action with context
100+
when := act.When()
101+
if when == action.Always ||
102+
(failedActionsCount == 0 && when == action.OnSuccess) ||
103+
(failedActionsCount > 0 && when == action.OnFailure) {
104+
actTimeout := act.Timeout()
105+
logger.Debugf("Executing sequential action %d with timeout %s", pos, actTimeout)
106+
// Create context for action
107+
actCtx, cancel := a.runtimeMaker.MakeContextWithTimeout(ctx, actTimeout)
108+
success, err := act.Execute(actCtx, runData)
109+
cancel() // Cancel the context immediately after action completion
110+
111+
if err != nil {
112+
lastErr = err
113+
logger.Errorf("Sequential action %d failed with error: %v", pos, err)
114+
}
115+
116+
if !success {
117+
failedActionsCount++
118+
logger.Debugf("Sequential action %d failed", pos)
119+
}
120+
}
121+
}
122+
123+
if a.fnd.DryRun() {
124+
return true, nil
125+
}
126+
127+
result := true
128+
if failedActionsCount > 0 {
129+
logger.Debugf("Sequential action failed on %d actions", failedActionsCount)
130+
result = false
131+
}
132+
133+
if lastErr != nil {
134+
return result, errors.Errorf("Sequential action failed with error: %v", lastErr)
135+
}
136+
137+
return result, lastErr
138+
}

0 commit comments

Comments
 (0)