Skip to content

Commit ddc9c8d

Browse files
committed
Added a repeat functionality on the script step level.
1 parent 3c40593 commit ddc9c8d

File tree

6 files changed

+114
-21
lines changed

6 files changed

+114
-21
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Just a basic script runner to easily abort/continue on previous step failure
88
```
99
variables:
1010
COMMAND_VAR: where python && echo Hallo
11+
Machines: ["machine1", "machine2"]
1112
1213
Phase 1:
1314
continue_on_failure: true
@@ -19,6 +20,7 @@ Phase 1:
1920
steps:
2021
- where python
2122
- $COMMAND_VAR && echo test123
23+
- 'repeat::Machines echo {{.}}'
2224
```
2325

2426
### Install and run

main.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import (
88
"sync"
99
)
1010

11-
func runCommand(wg *sync.WaitGroup, commandIndex int, phase nodeData, cmd *exec.Cmd) {
11+
func runCommand(wg *sync.WaitGroup, commandIndex int, phase *nodeData, cmd *exec.Cmd) {
1212
if wg != nil {
1313
defer wg.Done()
1414
}
1515

16+
commandName := strings.TrimSpace(fmt.Sprintf(`(INDEX %d) "%s" %+v`, commandIndex, cmd.Path, cmd.Args))
17+
1618
out, err := cmd.CombinedOutput()
1719
if err != nil {
18-
errMsg := fmt.Sprintf("ERROR (continue=%t): %s. OUT: %s\n", phase.ContinueOnFailure, err.Error(), string(out))
20+
errMsg := fmt.Sprintf("ERROR (continue=%t): %s. OUT: %s. COMMAND: %s\n", phase.ContinueOnFailure, err.Error(), string(out), commandName)
1921
if !phase.ContinueOnFailure {
2022
logger.Fatallnf(errMsg)
2123
} else {
@@ -24,14 +26,13 @@ func runCommand(wg *sync.WaitGroup, commandIndex int, phase nodeData, cmd *exec.
2426
}
2527
}
2628

27-
commandName := strings.TrimSpace(fmt.Sprintf(`(INDEX %d) "%s" %+v`, commandIndex, cmd.Path, cmd.Args))
2829
logger.PrintCommandOutput(commandName, string(out))
2930
}
3031

31-
func runPhase(setup *setup, phaseName string, phase nodeData) {
32+
func runPhase(setup *setup, phaseName string, phase *nodeData) {
3233
var wg sync.WaitGroup
3334

34-
cmds, err := phase.GetExecCommandsFromSteps(setup.Variables)
35+
cmds, err := phase.GetExecCommandsFromSteps(setup.StringVariablesOnly(), setup.Variables)
3536
if err != nil {
3637
logger.Fatallnf(err.Error())
3738
}

setup.go

+34-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import (
77
"strings"
88
)
99

10-
type phasesMap map[string]nodeData
10+
type phasesMap map[string]*nodeData
1111

1212
type setup struct {
1313
Phases phasesMap
14-
Variables map[string]string
14+
Variables map[string]interface{}
1515
}
1616

1717
//TODO: Do we need a check the `ContinueOnFailure==true` when `RunParallel==true`. Because if continue is false we will probably exit while another command is still busy in parallel
@@ -26,33 +26,62 @@ func (s *setup) Validate() error {
2626
return nil
2727
}
2828

29+
func (s *setup) ExpandRepeatingSteps() error {
30+
for _, node := range s.Phases {
31+
err := node.ExpandRepeatingSteps(s.Variables)
32+
if err != nil {
33+
return err
34+
}
35+
}
36+
return nil
37+
}
38+
39+
func (s *setup) StringVariablesOnly() map[string]string {
40+
m := make(map[string]string)
41+
for k, v := range s.Variables {
42+
if str, ok := v.(string); ok {
43+
m[k] = str
44+
}
45+
}
46+
return m
47+
}
48+
2949
func ParseYamlFile(filePath string) (*setup, error) {
3050
yamlBytes, err := ioutil.ReadFile(filePath)
3151
if err != nil {
3252
return nil, err
3353
}
3454

35-
tmpPhases := make(phasesMap)
55+
tmpPhases := make(map[string]nodeData)
3656
if err = yaml.Unmarshal(yamlBytes, &tmpPhases); err != nil {
3757
return nil, err
3858
}
3959
deleteVariablesFromPhasesMap(tmpPhases)
4060

4161
tmpVariables := &struct {
42-
Variables map[string]string
62+
Variables map[string]interface{}
4363
}{}
4464
if err = yaml.Unmarshal(yamlBytes, tmpVariables); err != nil {
4565
return nil, err
4666
}
4767

68+
pointerPhasesMap := make(phasesMap)
69+
for key, phase := range tmpPhases {
70+
pointerPhasesMap[key] = &phase
71+
}
72+
4873
s := &setup{
49-
tmpPhases,
74+
pointerPhasesMap,
5075
tmpVariables.Variables,
5176
}
5277

5378
if err = s.Validate(); err != nil {
5479
return nil, err
5580
}
5681

82+
if err = s.ExpandRepeatingSteps(); err != nil {
83+
return nil, err
84+
}
85+
5786
return s, nil
5887
}

setup_node_data.go

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56
"os/exec"
7+
"strings"
68
)
79

810
type nodeData struct {
@@ -14,11 +16,57 @@ type nodeData struct {
1416
Steps []nodeDataStep
1517
}
1618

17-
func (n *nodeData) GetExecCommandsFromSteps(variables map[string]string) ([]*exec.Cmd, error) {
19+
func (n *nodeData) ExpandRepeatingSteps(variables map[string]interface{}) error {
20+
expandedSteps := []nodeDataStep{}
21+
22+
for _, step := range n.Steps {
23+
repeatPrefix := "repeat::"
24+
if strings.HasPrefix(string(step), repeatPrefix) {
25+
varNameAndStepLine := strings.TrimPrefix(string(step), repeatPrefix)
26+
27+
indexFirstSpace := strings.Index(varNameAndStepLine, " ")
28+
if indexFirstSpace == -1 {
29+
return fmt.Errorf("Unable to find a space after the repeat prefix '%s' in step line '%s'", repeatPrefix, string(step))
30+
}
31+
32+
varName := strings.TrimSpace(varNameAndStepLine[0:indexFirstSpace])
33+
if varName == "" {
34+
return fmt.Errorf("The variable name is blank in the repeat prefixed step line '%s'", string(step))
35+
}
36+
37+
var varSlice []interface{}
38+
if val, ok := variables[varName]; !ok {
39+
return fmt.Errorf("The variable '%s' is not found in variables list %#v", varName, variables)
40+
} else if varSlice, ok = val.([]interface{}); !ok {
41+
return fmt.Errorf("The variable '%s' with value '%#v' cannot be type-casted to string slice", varName, val)
42+
}
43+
44+
repeatingStep := strings.TrimSpace(varNameAndStepLine[indexFirstSpace:])
45+
if repeatingStep == "" {
46+
return fmt.Errorf("The remaining step line after the repeat prefix '%s' is empty or white space in step line '%s'", repeatPrefix, string(step))
47+
}
48+
49+
for _, v := range varSlice {
50+
expandedStep, err := execTemplateToString(repeatingStep, v)
51+
if err != nil {
52+
return err
53+
}
54+
expandedSteps = append(expandedSteps, nodeDataStep(strings.TrimSpace(expandedStep)))
55+
}
56+
} else {
57+
expandedSteps = append(expandedSteps, step)
58+
}
59+
}
60+
n.Steps = expandedSteps
61+
62+
return nil
63+
}
64+
65+
func (n *nodeData) GetExecCommandsFromSteps(stringVariables map[string]string, allVariables map[string]interface{}) ([]*exec.Cmd, error) {
1866
cmds := []*exec.Cmd{}
1967

2068
for _, step := range n.Steps {
21-
splittedStep, err := step.SplitAndReplaceVariables(variables)
69+
splittedStep, err := step.SplitAndReplaceVariables(stringVariables, allVariables)
2270
if err != nil {
2371
return nil, err
2472
}

setup_node_data_step.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@ import (
66

77
type nodeDataStep string
88

9-
//TODO: Is the best practice to replace variables after splitting or before?
10-
func (n nodeDataStep) SplitAndReplaceVariables(variables map[string]string) ([]string, error) {
11-
preReplacedVars := replaceVariables(string(n), variables)
12-
splittedStep, err := parsecommand.Parse(preReplacedVars)
9+
func (n nodeDataStep) SplitAndReplaceVariables(stringVariables map[string]string, allVariables map[string]interface{}) ([]string, error) {
10+
step := replaceVariables(string(n), stringVariables)
11+
12+
splittedStep, err := parsecommand.Parse(step)
1313
if err != nil {
1414
return nil, err
1515
}
1616

17-
/*for i, _ := range splittedStep {
18-
splittedStep[i] = replaceVariables(splittedStep[i], variables)
19-
}*/
20-
2117
return splittedStep, nil
2218
}

util.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"bytes"
45
"fmt"
56
"strings"
7+
"text/template"
68
)
79

810
func splitEnvironKeyValue(pair string) (string, string, error) {
@@ -71,7 +73,7 @@ func appendEnvironment(environ []string, toAppend ...string) ([]string, error) {
7173
return newSlice, nil
7274
}
7375

74-
func deleteVariablesFromPhasesMap(m phasesMap) {
76+
func deleteVariablesFromPhasesMap(m map[string]nodeData) {
7577
for key, _ := range m {
7678
if strings.EqualFold(key, "variables") {
7779
delete(m, key)
@@ -87,3 +89,18 @@ func replaceVariables(s string, variables map[string]string) string {
8789
}
8890
return returnStr
8991
}
92+
93+
func execTemplateToString(templateString string, data interface{}) (string, error) {
94+
t, err := template.New("").Parse(templateString)
95+
if err != nil {
96+
return "", err
97+
}
98+
99+
var doc bytes.Buffer
100+
err = t.Execute(&doc, data)
101+
if err != nil {
102+
return "", err
103+
}
104+
105+
return doc.String(), nil
106+
}

0 commit comments

Comments
 (0)