command.go

v0.4.0
Doc Versions Source
1
package steps
2
3
import (
4
	"bytes"
5
	"io"
6
	"strings"
7
8
	"go.bigb.es/auxilia/culpa"
9
	"go.bigb.es/auxilia/legatus"
10
)
11
12
// Command runs a command unconditionally and captures its stdout. Unlike Exec
13
// it carries no idempotency guard (it embeds AlwaysApply), so it is the right
14
// tool for read-style commands whose output feeds later steps. Apply stores the
15
// trimmed stdout in Outcome.Data and in the FlowCtx param bag under Name().
16
//
17
// When Stream is set, stdout/stderr also stream live to FlowCtx.Out() as the
18
// command runs (for deploy hooks like post_push), while stdout is still captured
19
// for Outcome.Data/the param bag via a tee.
20
type Command struct {
21
	legatus.AlwaysApply
22
	Cmd    string
23
	Args   []string
24
	Sudo   bool
25
	Stream bool
26
}
27
28
// Name returns the step name.
29
func (c Command) Name() string { return "command:" + c.Cmd }
30
31
// Apply runs the command, records its stdout, and exposes it as a param. With
32
// Stream set, output is teed live to FlowCtx.Out() while stdout is still captured.
33
func (c Command) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
34
	cmd := legatus.Cmd{Path: c.Cmd, Args: c.Args, Sudo: c.Sudo}
35
	var (
36
		res    legatus.ExecResult
37
		err    error
38
		out    string // captured stdout (the param/Data payload)
39
		errOut string // captured stderr (for the failure message only)
40
	)
41
	if c.Stream {
42
		// Tee both streams: live to fc.Out(), captured for the param (stdout) and
43
		// the non-zero-exit error message (stderr). ExecStream leaves res.Stdout/
44
		// res.Stderr nil because both writers are non-nil.
45
		var outBuf, errBuf bytes.Buffer
46
		res, err = fc.Conn().ExecStream(fc.Ctx(), cmd,
47
			io.MultiWriter(fc.Out(), &outBuf), io.MultiWriter(fc.Out(), &errBuf))
48
		out, errOut = outBuf.String(), errBuf.String()
49
	} else {
50
		res, err = fc.Conn().Exec(fc.Ctx(), cmd)
51
		out, errOut = string(res.Stdout), string(res.Stderr)
52
	}
53
	if err != nil {
54
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "command %s", c.Cmd), "COMMAND_RUN")
55
	}
56
	if res.ExitCode != 0 {
57
		return legatus.Outcome{}, culpa.WithCode(
58
			culpa.Errorf("command %s: exit %d: %s", c.Cmd, res.ExitCode, strings.TrimSpace(out+errOut)),
59
			"COMMAND_RUN")
60
	}
61
	legatus.SetParam(fc, c.Name(), out)
62
	return legatus.Outcome{Changed: true, Message: "ran " + c.Cmd, Data: out}, nil
63
}
64

Source Files