-
Notifications
You must be signed in to change notification settings - Fork 3
/
action.go
349 lines (307 loc) · 8.33 KB
/
action.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// Copyright 2018 Daniel Theophanes. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package task handles running a sequence of tasks. State context
// is separated from script actions. Native context support.
package task
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// Action is a unit of work that gets run.
type Action interface {
Run(ctx context.Context, st *State, sc Script) error
}
// ActionFunc runs a function like an Action.
type ActionFunc func(ctx context.Context, st *State, sc Script) error
// Run the ActionFunc.
func (f ActionFunc) Run(ctx context.Context, st *State, sc Script) error {
return f(ctx, st, sc)
}
// Script is a list of actions. If an action
type Script interface {
Add(a ...Action) // Add normal actions to the script.
Rollback(a ...Action) // Add actions to only be run on rollback.
Defer(a ...Action) // Add actions to be run at the end, both on error and on normal run.
RunAction(ctx context.Context, st *State, a Action) error // Run a single action on the script.
Run(ctx context.Context, st *State, parent Script) error // Run current script under givent state.
}
// Run is the entry point for actions. It is a short-cut
// for ScriptAdd and Run.
func Run(ctx context.Context, st *State, a Action) error {
sc := NewScript(a)
return sc.Run(ctx, st, nil)
}
type script struct {
at int
list []Action
rollback *script
}
// NewScript creates a script and appends the given actions to it.
func NewScript(a ...Action) Script {
sc := &script{}
sc.list = append(sc.list, a...)
return sc
}
// Add creates a script if nil and appends the given actions to it.
func (sc *script) Add(a ...Action) {
sc.list = append(sc.list, a...)
}
// Rollback adds actions to be done on failure.
func (sc *script) Rollback(a ...Action) {
if sc.rollback == nil {
sc.rollback = &script{}
}
sc.rollback.Add(a...)
}
// Defer executes the given actions both in the event of a rollback or
// for normal execution.
func (sc *script) Defer(a ...Action) {
if sc.rollback == nil {
sc.rollback = &script{}
}
sc.rollback.Add(a...)
sc.Add(a...)
}
// Rollback adds actions to the current rollback script.
func Rollback(a ...Action) Action {
return ActionFunc(func(ctx context.Context, st *State, sc Script) error {
sc.Rollback(a...)
return nil
})
}
// Defer actions to the current end of the script. Always execute on error or success.
func Defer(a ...Action) Action {
return ActionFunc(func(ctx context.Context, st *State, sc Script) error {
sc.Defer(a...)
return nil
})
}
// Branch represents a branch condition used in Switch.
type Branch int64
// Typical branch values.
const (
BranchUnset Branch = iota
BranchTrue
BranchFalse
BranchCommit
BranchRollback
// BranchCustom is the smallest custom branch value that may be used.
BranchCustom Branch = 1024
)
// Policy describes the current error policy.
type Policy byte
// Policies may be combined together. The default policy is to fail on error
// and run any rollback acitions. If Continue is selected then normal execution
// will proceed and a rollback will not be triggered. If Log is selected
// any error will be logged to the ErrorLogger. If SkipRollback is selected
// then a failure will not trigger the rollback actions. If both Continue
// and SkipRollbackk are selected, execution will continue and SkipRollback
// will be ignored.
const (
PolicyFail Policy = 0
PolicyContinue Policy = 1 << iota
PolicyLog
PolicySkipRollback
// Fail
// Fail + Log
// Fail + Log + SkipRollback
// Fail + SkipRollback
// Continue
// Continue + Log
//
// Continue + SkipRollback will ignore skip rollback.
)
// State of the current task.
type State struct {
Env map[string]string
Dir string // Current working directory.
Stdout io.Writer
Stderr io.Writer
Branch Branch
Policy Policy
ErrorLogger func(err error) // Logger to use when Error is called.
MsgLogger func(msg string) // Logger to use when Log or Logf is called.
bucket map[string]interface{}
}
// Values of the state.
func (st *State) Values() map[string]interface{} {
return st.bucket
}
// Environ calls os.Environ and maps it to key value pairs.
func Environ() map[string]string {
envList := os.Environ()
envMap := make(map[string]string, len(envList))
for _, env := range envList {
ss := strings.SplitN(env, "=", 2)
if len(ss) != 2 {
continue
}
envMap[ss[0]] = ss[1]
}
return envMap
}
// DefaultState creates a new states with the current OS env.
func DefaultState() *State {
wd, _ := os.Getwd()
return &State{
Env: Environ(),
Dir: wd,
Stdout: os.Stdout,
Stderr: os.Stderr,
ErrorLogger: func(err error) {
fmt.Fprint(os.Stderr, err, "\n")
},
MsgLogger: func(msg string) {
fmt.Fprint(os.Stdout, msg, "\n")
},
}
}
// Log a message to the MsgLogger if present.
func (st *State) Log(msg string) {
if st.MsgLogger == nil {
return
}
st.MsgLogger(msg)
}
// Logf logs a formatted message to the MsgLogger if present.
func (st *State) Logf(f string, v ...interface{}) {
st.Log(fmt.Sprintf(f, v...))
}
// Error reports an error to the ErrorLogger if present.
func (st *State) Error(err error) {
if st.ErrorLogger == nil {
return
}
st.ErrorLogger(err)
}
// Filepath returns filename if absolute, or State.Dir + filename if not.
func (st *State) Filepath(filename string) string {
if filepath.IsAbs(filename) {
return filename
}
return filepath.Join(st.Dir, filename)
}
func (st *State) init() {
if st.bucket == nil {
st.bucket = make(map[string]interface{})
}
}
// Get the variable called name from the state bucket.
func (st *State) Get(name string) interface{} {
st.init()
return st.bucket[name]
}
// Default gets the variable called name from the state bucket. If
// no value is present, return v.
func (st *State) Default(name string, v interface{}) interface{} {
st.init()
if got, found := st.bucket[name]; found {
return got
}
return v
}
// Set the variable v to the name.
func (st *State) Set(name string, v interface{}) {
st.init()
st.bucket[name] = v
}
// Delete the variable called name.
func (st *State) Delete(name string) {
st.init()
delete(st.bucket, name)
}
// RunAction runs the given action in the current script's context.
func (sc *script) RunAction(ctx context.Context, st *State, a Action) error {
if sc == nil {
return nil
}
select {
default:
case <-ctx.Done():
return ctx.Err()
}
err := a.Run(ctx, st, sc)
if err == nil {
return nil
}
if st.Policy&PolicyLog != 0 {
st.Error(err)
}
if st.Policy&PolicyContinue != 0 {
err = nil
}
if st.Policy&PolicySkipRollback != 0 {
return err
}
if err == nil {
return err
}
rberr := sc.rollback.Run(context.Background(), st, sc)
if rberr == nil {
return err
}
return fmt.Errorf("%v, rollback failed: %v", err, rberr)
}
func (sc *script) runNext(ctx context.Context, st *State) error {
if sc.at >= len(sc.list) {
return io.EOF
}
a := sc.list[sc.at]
sc.at++
return sc.RunAction(ctx, st, a)
}
// Run the items in the method script. The parent script is ignored.
func (sc *script) Run(ctx context.Context, st *State, parent Script) error {
if sc == nil {
return nil
}
var err error
for {
err = sc.runNext(ctx, st)
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}
// AddRollback adds rollback actions to the current Script. Rollback actions
// are only executed on failure under non-Continue policies.
func AddRollback(a ...Action) Action {
return ActionFunc(func(ctx context.Context, st *State, sc Script) error {
sc.Rollback(a...)
return nil
})
}
// Switch will run the f action, read the state branch value, and then
// execute the given action in sw.
func Switch(f Action, sw map[Branch]Action) Action {
return ActionFunc(func(ctx context.Context, st *State, sc Script) error {
err := sc.RunAction(ctx, st, f)
if err != nil {
return err
}
br := st.Branch
st.Branch = BranchUnset
if next, ok := sw[br]; ok {
return sc.RunAction(ctx, st, next)
}
return nil
})
}
// WithPolicy sets the state policy for a single action.
func WithPolicy(p Policy, a Action) Action {
return ActionFunc(func(ctx context.Context, st *State, sc Script) error {
orig := st.Policy
st.Policy = p
err := sc.RunAction(ctx, st, a)
st.Policy = orig
return err
})
}