forked from kubernetes/kubernetes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
runner.go
483 lines (405 loc) · 15.1 KB
/
runner.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
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package workflow
import (
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// phaseSeparator defines the separator to be used when concatenating nested
// phase names
const phaseSeparator = "/"
// RunnerOptions defines the options supported during the execution of a
// kubeadm composable workflows
type RunnerOptions struct {
// FilterPhases defines the list of phases to be executed (if empty, all).
FilterPhases []string
// SkipPhases defines the list of phases to be excluded by execution (if empty, none).
SkipPhases []string
}
// RunData defines the data shared among all the phases included in the workflow, that is any type.
type RunData = interface{}
// Runner implements management of composable kubeadm workflows.
type Runner struct {
// Options that regulate the runner behavior.
Options RunnerOptions
// Phases composing the workflow to be managed by the runner.
Phases []Phase
// runDataInitializer defines a function that creates the runtime data shared
// among all the phases included in the workflow
runDataInitializer func(*cobra.Command, []string) (RunData, error)
// runData is part of the internal state of the runner and it is used for implementing
// a singleton in the InitData methods (thus avoiding to initialize data
// more than one time)
runData RunData
// runCmd is part of the internal state of the runner and it is used to track the
// command that will trigger the runner (only if the runner is BindToCommand).
runCmd *cobra.Command
// cmdAdditionalFlags holds additional, shared flags that could be added to the subcommands generated
// for phases. Flags could be inherited from the parent command too or added directly to each phase
cmdAdditionalFlags *pflag.FlagSet
// phaseRunners is part of the internal state of the runner and provides
// a list of wrappers to phases composing the workflow with contextual
// information supporting phase execution.
phaseRunners []*phaseRunner
}
// phaseRunner provides a wrapper to a Phase with the addition of a set
// of contextual information derived by the workflow managed by the Runner.
// TODO: If we ever decide to get more sophisticated we can swap this type with a well defined dag or tree library.
type phaseRunner struct {
// Phase provide access to the phase implementation
Phase
// provide access to the parent phase in the workflow managed by the Runner.
parent *phaseRunner
// level define the level of nesting of this phase into the workflow managed by
// the Runner.
level int
// selfPath contains all the elements of the path that identify the phase into
// the workflow managed by the Runner.
selfPath []string
// generatedName is the full name of the phase, that corresponds to the absolute
// path of the phase in the workflow managed by the Runner.
generatedName string
// use is the phase usage string that will be printed in the workflow help.
// It corresponds to the relative path of the phase in the workflow managed by the Runner.
use string
}
// NewRunner return a new runner for composable kubeadm workflows.
func NewRunner() *Runner {
return &Runner{
Phases: []Phase{},
}
}
// AppendPhase adds the given phase to the ordered sequence of phases managed by the runner.
func (e *Runner) AppendPhase(t Phase) {
e.Phases = append(e.Phases, t)
}
// computePhaseRunFlags return a map defining which phase should be run and which not.
// PhaseRunFlags are computed according to RunnerOptions.
func (e *Runner) computePhaseRunFlags() (map[string]bool, error) {
// Initialize support data structure
phaseRunFlags := map[string]bool{}
phaseHierarchy := map[string][]string{}
e.visitAll(func(p *phaseRunner) error {
// Initialize phaseRunFlags assuming that all the phases should be run.
phaseRunFlags[p.generatedName] = true
// Initialize phaseHierarchy for the current phase (the list of phases
// depending on the current phase
phaseHierarchy[p.generatedName] = []string{}
// Register current phase as part of its own parent hierarchy
parent := p.parent
for parent != nil {
phaseHierarchy[parent.generatedName] = append(phaseHierarchy[parent.generatedName], p.generatedName)
parent = parent.parent
}
return nil
})
// If a filter option is specified, set all phaseRunFlags to false except for
// the phases included in the filter and their hierarchy of nested phases.
if len(e.Options.FilterPhases) > 0 {
for i := range phaseRunFlags {
phaseRunFlags[i] = false
}
for _, f := range e.Options.FilterPhases {
if _, ok := phaseRunFlags[f]; !ok {
return phaseRunFlags, errors.Errorf("invalid phase name: %s", f)
}
phaseRunFlags[f] = true
for _, c := range phaseHierarchy[f] {
phaseRunFlags[c] = true
}
}
}
// If a phase skip option is specified, set the corresponding phaseRunFlags
// to false and apply the same change to the underlying hierarchy
for _, f := range e.Options.SkipPhases {
if _, ok := phaseRunFlags[f]; !ok {
return phaseRunFlags, errors.Errorf("invalid phase name: %s", f)
}
phaseRunFlags[f] = false
for _, c := range phaseHierarchy[f] {
phaseRunFlags[c] = false
}
}
return phaseRunFlags, nil
}
// SetDataInitializer allows to setup a function that initialize the runtime data shared
// among all the phases included in the workflow.
// The method will receive in input the cmd that triggers the Runner (only if the runner is BindToCommand)
func (e *Runner) SetDataInitializer(builder func(cmd *cobra.Command, args []string) (RunData, error)) {
e.runDataInitializer = builder
}
// InitData triggers the creation of runtime data shared among all the phases included in the workflow.
// This action can be executed explicitly out, when it is necessary to get the RunData
// before actually executing Run, or implicitly when invoking Run.
func (e *Runner) InitData(args []string) (RunData, error) {
if e.runData == nil && e.runDataInitializer != nil {
var err error
if e.runData, err = e.runDataInitializer(e.runCmd, args); err != nil {
return nil, err
}
}
return e.runData, nil
}
// Run the kubeadm composable kubeadm workflows.
func (e *Runner) Run(args []string) error {
e.prepareForExecution()
// determine which phase should be run according to RunnerOptions
phaseRunFlags, err := e.computePhaseRunFlags()
if err != nil {
return err
}
// builds the runner data
var data RunData
if data, err = e.InitData(args); err != nil {
return err
}
err = e.visitAll(func(p *phaseRunner) error {
// if the phase should not be run, skip the phase.
if run, ok := phaseRunFlags[p.generatedName]; !run || !ok {
return nil
}
// Errors if phases that are meant to create special subcommands only
// are wrongly assigned Run Methods
if p.RunAllSiblings && (p.RunIf != nil || p.Run != nil) {
return errors.Wrapf(err, "phase marked as RunAllSiblings can not have Run functions %s", p.generatedName)
}
// If the phase defines a condition to be checked before executing the phase action.
if p.RunIf != nil {
// Check the condition and returns if the condition isn't satisfied (or fails)
ok, err := p.RunIf(data)
if err != nil {
return errors.Wrapf(err, "error execution run condition for phase %s", p.generatedName)
}
if !ok {
return nil
}
}
// Runs the phase action (if defined)
if p.Run != nil {
if err := p.Run(data); err != nil {
return errors.Wrapf(err, "error execution phase %s", p.generatedName)
}
}
return nil
})
return err
}
// Help returns text with the list of phases included in the workflow.
func (e *Runner) Help(cmdUse string) string {
e.prepareForExecution()
// computes the max length of for each phase use line
maxLength := 0
e.visitAll(func(p *phaseRunner) error {
if !p.Hidden && !p.RunAllSiblings {
length := len(p.use)
if maxLength < length {
maxLength = length
}
}
return nil
})
// prints the list of phases indented by level and formatted using the maxlength
// the list is enclosed in a mardown code block for ensuring better readability in the public web site
line := fmt.Sprintf("The %q command executes the following phases:\n", cmdUse)
line += "```\n"
offset := 2
e.visitAll(func(p *phaseRunner) error {
if !p.Hidden && !p.RunAllSiblings {
padding := maxLength - len(p.use) + offset
line += strings.Repeat(" ", offset*p.level) // indentation
line += p.use // name + aliases
line += strings.Repeat(" ", padding) // padding right up to max length (+ offset for spacing)
line += p.Short // phase short description
line += "\n"
}
return nil
})
line += "```"
return line
}
// SetAdditionalFlags allows to define flags to be added
// to the subcommands generated for each phase (but not existing in the parent command).
// Please note that this command needs to be done before BindToCommand.
// Nb. if a flag is used only by one phase, please consider using phase LocalFlags.
func (e *Runner) SetAdditionalFlags(fn func(*pflag.FlagSet)) {
// creates a new NewFlagSet
e.cmdAdditionalFlags = pflag.NewFlagSet("phaseAdditionalFlags", pflag.ContinueOnError)
// invokes the function that sets additional flags
fn(e.cmdAdditionalFlags)
}
// BindToCommand bind the Runner to a cobra command by altering
// command help, adding phase related flags and by adding phases subcommands
// Please note that this command needs to be done once all the phases are added to the Runner.
func (e *Runner) BindToCommand(cmd *cobra.Command) {
// keep track of the command triggering the runner
e.runCmd = cmd
// return early if no phases were added
if len(e.Phases) == 0 {
return
}
e.prepareForExecution()
// adds the phases subcommand
phaseCommand := &cobra.Command{
Use: "phase",
Short: fmt.Sprintf("use this command to invoke single phase of the %s workflow", cmd.Name()),
}
cmd.AddCommand(phaseCommand)
// generate all the nested subcommands for invoking single phases
subcommands := map[string]*cobra.Command{}
e.visitAll(func(p *phaseRunner) error {
// skip hidden phases
if p.Hidden {
return nil
}
// initialize phase selector
phaseSelector := p.generatedName
// if requested, set the phase to run all the sibling phases
if p.RunAllSiblings {
phaseSelector = p.parent.generatedName
}
// creates phase subcommand
phaseCmd := &cobra.Command{
Use: strings.ToLower(p.Name),
Short: p.Short,
Long: p.Long,
Example: p.Example,
Aliases: p.Aliases,
Run: func(cmd *cobra.Command, args []string) {
// if the phase has subphases, print the help and exits
if len(p.Phases) > 0 {
cmd.Help()
return
}
// overrides the command triggering the Runner using the phaseCmd
e.runCmd = cmd
e.Options.FilterPhases = []string{phaseSelector}
if err := e.Run(args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
// makes the new command inherits local flags from the parent command
// Nb. global flags will be inherited automatically
inheritsFlags(cmd.Flags(), phaseCmd.Flags(), p.InheritFlags)
// makes the new command inherits additional flags for phases
if e.cmdAdditionalFlags != nil {
inheritsFlags(e.cmdAdditionalFlags, phaseCmd.Flags(), p.InheritFlags)
}
// If defined, added phase local flags
if p.LocalFlags != nil {
p.LocalFlags.VisitAll(func(f *pflag.Flag) {
phaseCmd.Flags().AddFlag(f)
})
}
// if this phase has children (not a leaf) it doesn't accept any args
if len(p.Phases) > 0 {
phaseCmd.Args = cobra.NoArgs
}
// adds the command to parent
if p.level == 0 {
phaseCommand.AddCommand(phaseCmd)
} else {
subcommands[p.parent.generatedName].AddCommand(phaseCmd)
}
subcommands[p.generatedName] = phaseCmd
return nil
})
// alters the command description to show available phases
if cmd.Long != "" {
cmd.Long = fmt.Sprintf("%s\n\n%s\n", cmd.Long, e.Help(cmd.Use))
} else {
cmd.Long = fmt.Sprintf("%s\n\n%s\n", cmd.Short, e.Help(cmd.Use))
}
// adds phase related flags to the main command
cmd.Flags().StringSliceVar(&e.Options.SkipPhases, "skip-phases", nil, "List of phases to be skipped")
}
func inheritsFlags(sourceFlags, targetFlags *pflag.FlagSet, cmdFlags []string) {
// If the list of flag to be inherited from the parent command is not defined, no flag is added
if cmdFlags == nil {
return
}
// add all the flags to be inherited to the target flagSet
sourceFlags.VisitAll(func(f *pflag.Flag) {
for _, c := range cmdFlags {
if f.Name == c {
targetFlags.AddFlag(f)
}
}
})
}
// visitAll provides a utility method for visiting all the phases in the workflow
// in the execution order and executing a func on each phase.
// Nested phase are visited immediately after their parent phase.
func (e *Runner) visitAll(fn func(*phaseRunner) error) error {
for _, currentRunner := range e.phaseRunners {
if err := fn(currentRunner); err != nil {
return err
}
}
return nil
}
// prepareForExecution initialize the internal state of the Runner (the list of phaseRunner).
func (e *Runner) prepareForExecution() {
e.phaseRunners = []*phaseRunner{}
var parentRunner *phaseRunner
for _, phase := range e.Phases {
// skips phases that are meant to create special subcommands only
if phase.RunAllSiblings {
continue
}
// add phases to the execution list
addPhaseRunner(e, parentRunner, phase)
}
}
// addPhaseRunner adds the phaseRunner for a given phase to the phaseRunners list
func addPhaseRunner(e *Runner, parentRunner *phaseRunner, phase Phase) {
// computes contextual information derived by the workflow managed by the Runner.
use := cleanName(phase.Name)
generatedName := use
selfPath := []string{generatedName}
if parentRunner != nil {
generatedName = strings.Join([]string{parentRunner.generatedName, generatedName}, phaseSeparator)
use = fmt.Sprintf("%s%s", phaseSeparator, use)
selfPath = append(parentRunner.selfPath, selfPath...)
}
// creates the phaseRunner
currentRunner := &phaseRunner{
Phase: phase,
parent: parentRunner,
level: len(selfPath) - 1,
selfPath: selfPath,
generatedName: generatedName,
use: use,
}
// adds to the phaseRunners list
e.phaseRunners = append(e.phaseRunners, currentRunner)
// iterate for the nested, ordered list of phases, thus storing
// phases in the expected executing order (child phase are stored immediately after their parent phase).
for _, childPhase := range phase.Phases {
addPhaseRunner(e, currentRunner, childPhase)
}
}
// cleanName makes phase name suitable for the runner help, by lowercasing the name
// and removing args descriptors, if any
func cleanName(name string) string {
ret := strings.ToLower(name)
if pos := strings.Index(ret, " "); pos != -1 {
ret = ret[:pos]
}
return ret
}