compose.go

v0.6.1
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
// ComposeAction is a docker compose subcommand. Read-only actions (Ps, Logs)
13
// report Outcome.Changed:false; state-changing ones (Up, Down, Pull, Restart)
14
// report Changed:true.
15
type ComposeAction string
16
17
const (
18
	ComposeUp      ComposeAction = "up"
19
	ComposeDown    ComposeAction = "down"
20
	ComposePull    ComposeAction = "pull"
21
	ComposePs      ComposeAction = "ps"
22
	ComposeLogs    ComposeAction = "logs"
23
	ComposeRestart ComposeAction = "restart"
24
)
25
26
// changesState reports whether the action mutates the stack (so Apply can set
27
// Outcome.Changed). Read-only inspection actions return false.
28
func (a ComposeAction) changesState() bool {
29
	switch a {
30
	case ComposePs, ComposeLogs:
31
		return false
32
	default:
33
		return true
34
	}
35
}
36
37
// Compose runs `docker compose <action>` in a stack's directory. It is a thin,
38
// opinion-free typed wrapper over a structural legatus.Cmd with Cmd.Dir set
39
// (PC2): it adds no stack/service policy of its own, only the `up -d` flag when
40
// Detach is set. It always applies (no idempotency guard); read-only actions
41
// report Changed:false so the engine's tally stays honest.
42
//
43
// When Stream is set, stdout/stderr also stream live to FlowCtx.Out() as the
44
// command runs (for `up`/`logs` output), while stdout is still captured for
45
// Outcome.Data via a tee — mirroring Command's streaming contract.
46
type Compose struct {
47
	legatus.AlwaysApply
48
	Dir      string
49
	Action   ComposeAction
50
	Services []string
51
	Detach   bool
52
	Stream   bool
53
}
54
55
// Name returns the step name.
56
func (c Compose) Name() string { return "compose:" + string(c.Action) }
57
58
// build renders the structural docker compose Cmd: `docker compose <action>
59
// [-d] [services...]` with Dir set (G8). The argv is structural — no sh -c
60
// concatenation — so service names can never re-split (IV1).
61
func (c Compose) build() legatus.Cmd {
62
	args := []string{"compose", string(c.Action)}
63
	if c.Action == ComposeUp && c.Detach {
64
		args = append(args, "-d")
65
	}
66
	args = append(args, c.Services...)
67
	return legatus.Cmd{Path: "docker", Args: args, Dir: c.Dir}
68
}
69
70
// Apply runs the compose command in Dir and reports whether it changed state.
71
// An empty Action fails loud rather than running `docker compose` with no
72
// subcommand.
73
func (c Compose) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
74
	if c.Action == "" {
75
		return legatus.Outcome{}, culpa.WithCode(culpa.New("compose: empty action"), "COMPOSE_RUN")
76
	}
77
	cmd := c.build()
78
	var (
79
		res    legatus.ExecResult
80
		err    error
81
		out    string // captured stdout (the Data payload)
82
		errOut string // captured stderr (for the failure message only)
83
	)
84
	if c.Stream {
85
		// Tee both streams: live to fc.Out(), captured for Data (stdout) and the
86
		// non-zero-exit error message (stderr). ExecStream leaves res.Stdout/
87
		// res.Stderr nil because both writers are non-nil.
88
		var outBuf, errBuf bytes.Buffer
89
		res, err = fc.Conn().ExecStream(fc.Ctx(), cmd,
90
			io.MultiWriter(fc.Out(), &outBuf), io.MultiWriter(fc.Out(), &errBuf))
91
		out, errOut = outBuf.String(), errBuf.String()
92
	} else {
93
		res, err = fc.Conn().Exec(fc.Ctx(), cmd)
94
		out, errOut = string(res.Stdout), string(res.Stderr)
95
	}
96
	if err != nil {
97
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "compose %s", c.Action), "COMPOSE_RUN")
98
	}
99
	if res.ExitCode != 0 {
100
		return legatus.Outcome{}, culpa.WithCode(
101
			culpa.Errorf("compose %s: exit %d: %s", c.Action, res.ExitCode, strings.TrimSpace(out+errOut)),
102
			"COMPOSE_RUN")
103
	}
104
	return legatus.Outcome{Changed: c.Action.changesState(), Message: "compose " + string(c.Action), Data: out}, nil
105
}
106

Source Files