exec.go

v0.6.1
Doc Versions Source
1
package steps
2
3
import (
4
	"strings"
5
6
	"go.bigb.es/auxilia/culpa"
7
	"go.bigb.es/auxilia/legatus"
8
)
9
10
// Exec runs a command with idempotency guards. Check evaluates the guards
11
// (PC4); Apply runs the command. Guards (highest precedence first):
12
//   - Creates: skip when the path already exists.
13
//   - Unless:  skip when the shell command exits 0.
14
//   - OnlyIf:  skip when the shell command exits non-zero.
15
//
16
// With no guard, the command always runs.
17
type Exec struct {
18
	Cmd     string
19
	Args    []string
20
	Creates string
21
	Unless  string
22
	OnlyIf  string
23
	// Sudo controls privilege-wrapping; the zero value (SudoDefault) is no sudo,
24
	// matching the previous bool zero value.
25
	Sudo SudoPolicy
26
}
27
28
// Name returns the step name.
29
func (e Exec) Name() string { return "exec:" + e.Cmd }
30
31
// Check evaluates the guards and reports whether the command should run.
32
func (e Exec) Check(fc *legatus.FlowCtx) (bool, error) {
33
	sudo := e.Sudo.resolve(fc.Host(), false)
34
	if e.Creates != "" {
35
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "test", Args: []string{"-e", e.Creates}, Sudo: sudo})
36
		if err != nil {
37
			return false, culpa.WithCode(culpa.Wrapf(err, "creates guard %s", e.Creates), "EXEC_GUARD")
38
		}
39
		if res.ExitCode == 0 {
40
			return false, nil // target exists ⇒ nothing to do.
41
		}
42
	}
43
44
	if e.Unless != "" {
45
		code, err := runGuard(fc.Ctx(), fc.Conn(), e.Unless, sudo)
46
		if err != nil {
47
			return false, err
48
		}
49
		if code == 0 {
50
			return false, nil // unless succeeded ⇒ skip.
51
		}
52
	}
53
54
	if e.OnlyIf != "" {
55
		code, err := runGuard(fc.Ctx(), fc.Conn(), e.OnlyIf, sudo)
56
		if err != nil {
57
			return false, err
58
		}
59
		if code != 0 {
60
			return false, nil // onlyif failed ⇒ skip.
61
		}
62
	}
63
64
	return true, nil
65
}
66
67
// Apply runs the command and reports it changed state.
68
func (e Exec) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
69
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: e.Cmd, Args: e.Args, Sudo: e.Sudo.resolve(fc.Host(), false)})
70
	if err != nil {
71
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "exec %s", e.Cmd), "EXEC_RUN")
72
	}
73
	if res.ExitCode != 0 {
74
		return legatus.Outcome{}, culpa.WithCode(
75
			culpa.Errorf("exec %s: exit %d: %s", e.Cmd, res.ExitCode, strings.TrimSpace(string(res.Combined()))),
76
			"EXEC_RUN")
77
	}
78
	return legatus.Outcome{Changed: true, Message: "ran " + e.Cmd, Data: res}, nil
79
}
80

Source Files