Skip to content

Commit aa94a31

Browse files
committed
Lots of improvements:
- Added a logger and using `github.com/fatih/color` - Can now run a phase `Steps` in parallel by specifying `run_parallel: true` in the phase yaml - Added quick start guide in README
1 parent 9463e27 commit aa94a31

8 files changed

+442
-50
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
11
# yaml-script-runner
22
Just a basic script runner to easily abort/continue on previous step failure
33

4-
# WIP: Working on first implementation
4+
## Quick Start
5+
6+
### /path/to/my-setup.yaml:
7+
8+
```
9+
variables:
10+
COMMAND_VAR: where python && echo Hallo
11+
12+
Phase 1:
13+
continue_on_failure: true
14+
inherit_environment: true
15+
run_parallel: true
16+
AdditionalEnvironment:
17+
- MyEnviron1=Value1
18+
executor: ["sh", "-c"]
19+
steps:
20+
- where python
21+
- $COMMAND_VAR && echo test123
22+
```
23+
24+
### Install and run
25+
26+
```
27+
go get -u github.com/golang-devops/yaml-script-runner
28+
yaml-script-runner "/path/to/my-setup.yaml"
29+
```

logger.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"github.com/fatih/color"
5+
"io"
6+
"log"
7+
"os"
8+
"sync"
9+
)
10+
11+
var logger = NewLogger(os.Stdout /*ioutil.Discard*/, os.Stdout, os.Stdout, os.Stderr)
12+
13+
func NewLogger(traceHandle, infoHandle, warningHandle, errorHandle io.Writer) *Logger {
14+
return &Logger{
15+
colorTraceSprintfFunc: color.New().SprintfFunc(),
16+
colorInfoSprintfFunc: color.New(color.FgHiGreen).SprintfFunc(),
17+
colorWarningSprintfFunc: color.New(color.FgHiYellow).SprintfFunc(),
18+
colorErrSprintfFunc: color.New(color.FgRed).SprintfFunc(),
19+
20+
trace: log.New(traceHandle, "[T] ", log.Ldate|log.Ltime|log.Lshortfile),
21+
info: log.New(infoHandle, "[I] ", log.Ldate|log.Ltime|log.Lshortfile),
22+
warning: log.New(warningHandle, "[W] ", log.Ldate|log.Ltime|log.Lshortfile),
23+
err: log.New(errorHandle, "[E] ", log.Ldate|log.Ltime|log.Lshortfile),
24+
}
25+
}
26+
27+
type Logger struct {
28+
sync.RWMutex
29+
30+
colorTraceSprintfFunc func(format string, args ...interface{}) string
31+
colorInfoSprintfFunc func(format string, args ...interface{}) string
32+
colorWarningSprintfFunc func(format string, args ...interface{}) string
33+
colorErrSprintfFunc func(format string, args ...interface{}) string
34+
35+
trace *log.Logger
36+
info *log.Logger
37+
warning *log.Logger
38+
err *log.Logger
39+
}
40+
41+
func (l *Logger) Tracelnf(format string, args ...interface{}) {
42+
l.trace.Println(l.colorTraceSprintfFunc(format+"\n", args...))
43+
}
44+
func (l *Logger) Infolnf(format string, args ...interface{}) {
45+
l.info.Println(l.colorInfoSprintfFunc(format+"\n", args...))
46+
}
47+
func (l *Logger) Warninglnf(format string, args ...interface{}) {
48+
l.warning.Println(l.colorWarningSprintfFunc(format+"\n", args...))
49+
}
50+
func (l *Logger) Errorlnf(format string, args ...interface{}) {
51+
l.err.Println(l.colorErrSprintfFunc(format+"\n", args...))
52+
}
53+
func (l *Logger) Fatallnf(format string, args ...interface{}) {
54+
l.err.Fatalln(l.colorErrSprintfFunc(format+"\n", args...))
55+
}
56+
57+
func (l *Logger) PrintCommandOutput(commandName string, output string) {
58+
l.Lock()
59+
defer l.Unlock()
60+
61+
l.Infolnf("[BEGIN OUTPUT of %s]", commandName)
62+
color.New(color.FgHiCyan).Println(string(output))
63+
l.Infolnf("[END OUTPUT of %s]", commandName)
64+
}

main.go

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,72 @@ package main
22

33
import (
44
"fmt"
5-
"log"
65
"os"
6+
"os/exec"
7+
"strings"
8+
"sync"
79
)
810

11+
func runCommand(wg *sync.WaitGroup, commandIndex int, phase nodeData, cmd *exec.Cmd) {
12+
if wg != nil {
13+
defer wg.Done()
14+
}
15+
16+
out, err := cmd.CombinedOutput()
17+
if err != nil {
18+
errMsg := fmt.Sprintf("ERROR (continue=%t): %s. OUT: %s\n", phase.ContinueOnFailure, err.Error(), string(out))
19+
if !phase.ContinueOnFailure {
20+
logger.Fatallnf(errMsg)
21+
} else {
22+
logger.Errorlnf(errMsg)
23+
return
24+
}
25+
}
26+
27+
commandName := strings.TrimSpace(fmt.Sprintf(`(INDEX %d) "%s" %+v`, commandIndex, cmd.Path, cmd.Args))
28+
logger.PrintCommandOutput(commandName, string(out))
29+
}
30+
31+
func runPhase(setup *setup, phaseName string, phase nodeData) {
32+
var wg sync.WaitGroup
33+
34+
cmds, err := phase.GetExecCommandsFromSteps(setup.Variables)
35+
if err != nil {
36+
logger.Fatallnf(err.Error())
37+
}
38+
39+
if phase.RunParallel {
40+
wg.Add(len(cmds))
41+
}
42+
43+
logger.Infolnf("Running step %s", phaseName)
44+
for ind, c := range cmds {
45+
var wgToUse *sync.WaitGroup = nil
46+
if phase.RunParallel {
47+
wgToUse = &wg
48+
}
49+
50+
go runCommand(wgToUse, ind, phase, c)
51+
}
52+
53+
if phase.RunParallel {
54+
wg.Wait()
55+
}
56+
}
57+
958
func main() {
1059
if len(os.Args) < 2 {
11-
log.Fatal("The first command-line argument must be the YAML file path.")
60+
logger.Fatallnf("The first command-line argument must be the YAML file path.")
1261
}
1362

1463
yamlFilePath := os.Args[1]
1564

1665
setup, err := ParseYamlFile(yamlFilePath)
1766
if err != nil {
18-
log.Fatal(err)
67+
logger.Fatallnf(err.Error())
1968
}
2069

21-
cmds, err := setup.GetExecCommandsFromSteps()
22-
if err != nil {
23-
log.Fatal(err)
24-
}
25-
26-
for _, c := range cmds {
27-
out, err := c.CombinedOutput()
28-
if err != nil {
29-
errMsg := fmt.Sprintf("ERROR (continue=%t): %s. OUT: %s\n", setup.ContinueIfStepFailed, err.Error(), string(out))
30-
if !setup.ContinueIfStepFailed {
31-
log.Fatalln(errMsg)
32-
} else {
33-
log.Println(errMsg)
34-
continue
35-
}
36-
}
37-
38-
fmt.Println(string(out))
70+
for name, phase := range setup.Phases {
71+
runPhase(setup, name, phase)
3972
}
4073
}

setup.go

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,29 @@
11
package main
22

33
import (
4+
"fmt"
45
"github.com/ghodss/yaml"
5-
"github.com/golang-devops/parsecommand"
66
"io/ioutil"
7-
"os/exec"
7+
"strings"
88
)
99

10+
type phasesMap map[string]nodeData
11+
1012
type setup struct {
11-
ContinueIfStepFailed bool `json:"continue_if_step_failed"`
12-
Executor []string
13-
Steps []string
13+
Phases phasesMap
14+
Variables map[string]string
1415
}
1516

16-
func (s *setup) GetExecCommandsFromSteps() ([]*exec.Cmd, error) {
17-
cmds := []*exec.Cmd{}
18-
19-
for _, step := range s.Steps {
20-
splittedStep, err := parsecommand.Parse(step)
21-
if err != nil {
22-
return nil, err
23-
}
24-
25-
allArgs := s.Executor
26-
allArgs = append(allArgs, splittedStep...)
27-
28-
exe := allArgs[0]
29-
args := []string{}
30-
if len(allArgs) > 1 {
31-
args = allArgs[1:]
17+
//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
18+
func (s *setup) Validate() error {
19+
for _, node := range s.Phases {
20+
for _, e := range node.AdditionalEnvironment {
21+
if !strings.Contains(e, "=") {
22+
return fmt.Errorf("Environment string must be in format key=value. Invalid string: '%s'", e)
23+
}
3224
}
33-
34-
cmds = append(cmds, exec.Command(exe, args...))
3525
}
36-
37-
return cmds, nil
26+
return nil
3827
}
3928

4029
func ParseYamlFile(filePath string) (*setup, error) {
@@ -43,9 +32,25 @@ func ParseYamlFile(filePath string) (*setup, error) {
4332
return nil, err
4433
}
4534

46-
s := &setup{}
47-
err = yaml.Unmarshal(yamlBytes, s)
48-
if err != nil {
35+
tmpPhases := make(phasesMap)
36+
if err = yaml.Unmarshal(yamlBytes, &tmpPhases); err != nil {
37+
return nil, err
38+
}
39+
deleteVariablesFromPhasesMap(tmpPhases)
40+
41+
tmpVariables := &struct {
42+
Variables map[string]string
43+
}{}
44+
if err = yaml.Unmarshal(yamlBytes, tmpVariables); err != nil {
45+
return nil, err
46+
}
47+
48+
s := &setup{
49+
tmpPhases,
50+
tmpVariables.Variables,
51+
}
52+
53+
if err = s.Validate(); err != nil {
4954
return nil, err
5055
}
5156

setup_node_data.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
)
7+
8+
type nodeData struct {
9+
ContinueOnFailure bool `json:"continue_on_failure"`
10+
InheritEnvironment bool `json:"inherit_environment"`
11+
AdditionalEnvironment []string
12+
RunParallel bool `json:"run_parallel"`
13+
Executor []string
14+
Steps []nodeDataStep
15+
}
16+
17+
func (n *nodeData) GetExecCommandsFromSteps(variables map[string]string) ([]*exec.Cmd, error) {
18+
cmds := []*exec.Cmd{}
19+
20+
for _, step := range n.Steps {
21+
splittedStep, err := step.SplitAndReplaceVariables(variables)
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
allArgs := n.Executor
27+
allArgs = append(allArgs, splittedStep...)
28+
29+
exe := allArgs[0]
30+
args := []string{}
31+
if len(allArgs) > 1 {
32+
args = allArgs[1:]
33+
}
34+
35+
c := exec.Command(exe, args...)
36+
37+
if n.InheritEnvironment {
38+
c.Env, err = appendEnvironment(c.Env, os.Environ()...)
39+
if err != nil {
40+
return nil, err
41+
}
42+
}
43+
44+
for _, e := range n.AdditionalEnvironment {
45+
c.Env, err = appendEnvironment(c.Env, e)
46+
if err != nil {
47+
return nil, err
48+
}
49+
}
50+
51+
cmds = append(cmds, c)
52+
}
53+
54+
return cmds, nil
55+
}

setup_node_data_step.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"github.com/golang-devops/parsecommand"
5+
)
6+
7+
type nodeDataStep string
8+
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)
13+
if err != nil {
14+
return nil, err
15+
}
16+
17+
/*for i, _ := range splittedStep {
18+
splittedStep[i] = replaceVariables(splittedStep[i], variables)
19+
}*/
20+
21+
return splittedStep, nil
22+
}

0 commit comments

Comments
 (0)