-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecutor.go
More file actions
201 lines (167 loc) · 6.8 KB
/
executor.go
File metadata and controls
201 lines (167 loc) · 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Package dockercompose implements a platform.Provider that maps abstract
// capability declarations to Docker Compose services, networks, and volumes.
// It uses only the standard library and invokes docker compose via exec.Command.
package dockercompose
import (
"bytes"
"context"
"fmt"
"os/exec"
"strings"
)
// ComposeExecutor defines the interface for running docker compose commands.
// This abstraction allows tests to inject a mock executor.
type ComposeExecutor interface {
// Up starts services defined in the compose file.
Up(ctx context.Context, projectDir string, files ...string) (string, error)
// Down stops and removes services defined in the compose file.
Down(ctx context.Context, projectDir string, files ...string) (string, error)
// Ps lists running containers for the compose project.
Ps(ctx context.Context, projectDir string, files ...string) (string, error)
// Logs retrieves logs from compose services.
Logs(ctx context.Context, projectDir string, service string, files ...string) (string, error)
// Version returns the docker compose version string.
Version(ctx context.Context) (string, error)
// IsAvailable checks whether docker compose is installed and reachable.
IsAvailable(ctx context.Context) error
}
// ShellExecutor executes docker compose commands through the system shell.
type ShellExecutor struct {
// ComposeCommand is the base command to use. Defaults to "docker" with "compose" subcommand.
ComposeCommand string
}
// NewShellExecutor creates a ShellExecutor that uses the system docker compose.
func NewShellExecutor() *ShellExecutor {
return &ShellExecutor{
ComposeCommand: "docker",
}
}
// Up starts services with docker compose up -d.
func (e *ShellExecutor) Up(ctx context.Context, projectDir string, files ...string) (string, error) {
args := e.buildArgs(files, "up", "-d", "--remove-orphans")
return e.run(ctx, projectDir, args...)
}
// Down stops and removes services with docker compose down.
func (e *ShellExecutor) Down(ctx context.Context, projectDir string, files ...string) (string, error) {
args := e.buildArgs(files, "down", "--remove-orphans")
return e.run(ctx, projectDir, args...)
}
// Ps lists containers with docker compose ps.
func (e *ShellExecutor) Ps(ctx context.Context, projectDir string, files ...string) (string, error) {
args := e.buildArgs(files, "ps", "--format", "json")
return e.run(ctx, projectDir, args...)
}
// Logs retrieves logs for a service with docker compose logs.
func (e *ShellExecutor) Logs(ctx context.Context, projectDir string, service string, files ...string) (string, error) {
args := e.buildArgs(files, "logs", "--no-color", service)
return e.run(ctx, projectDir, args...)
}
// Version returns the docker compose version.
func (e *ShellExecutor) Version(ctx context.Context) (string, error) {
return e.run(ctx, "", "compose", "version", "--short")
}
// IsAvailable checks whether docker compose is installed and reachable.
func (e *ShellExecutor) IsAvailable(ctx context.Context) error {
_, err := e.Version(ctx)
if err != nil {
return fmt.Errorf("docker compose is not available: %w", err)
}
return nil
}
func (e *ShellExecutor) buildArgs(files []string, subArgs ...string) []string {
args := []string{"compose"}
for _, f := range files {
args = append(args, "-f", f)
}
args = append(args, subArgs...)
return args
}
func (e *ShellExecutor) run(ctx context.Context, dir string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, e.ComposeCommand, args...) //nolint:gosec // ComposeCommand is set internally, not from user input
if dir != "" {
cmd.Dir = dir
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
combinedErr := strings.TrimSpace(stderr.String())
if combinedErr == "" {
combinedErr = strings.TrimSpace(stdout.String())
}
return "", fmt.Errorf("docker compose %s failed: %s: %w",
strings.Join(args, " "), combinedErr, err)
}
return strings.TrimSpace(stdout.String()), nil
}
// MockExecutor is a test double for ComposeExecutor that records calls and
// returns pre-configured responses.
type MockExecutor struct {
// UpFn is called when Up is invoked. If nil, returns empty string and no error.
UpFn func(ctx context.Context, projectDir string, files ...string) (string, error)
// DownFn is called when Down is invoked.
DownFn func(ctx context.Context, projectDir string, files ...string) (string, error)
// PsFn is called when Ps is invoked.
PsFn func(ctx context.Context, projectDir string, files ...string) (string, error)
// LogsFn is called when Logs is invoked.
LogsFn func(ctx context.Context, projectDir string, service string, files ...string) (string, error)
// VersionFn is called when Version is invoked.
VersionFn func(ctx context.Context) (string, error)
// IsAvailableFn is called when IsAvailable is invoked.
IsAvailableFn func(ctx context.Context) error
// Calls records all method calls for assertion purposes.
Calls []MockCall
}
// MockCall records a single executor method invocation.
type MockCall struct {
Method string
Args []string
}
// Up implements ComposeExecutor.
func (m *MockExecutor) Up(ctx context.Context, projectDir string, files ...string) (string, error) {
m.Calls = append(m.Calls, MockCall{Method: "Up", Args: append([]string{projectDir}, files...)})
if m.UpFn != nil {
return m.UpFn(ctx, projectDir, files...)
}
return "", nil
}
// Down implements ComposeExecutor.
func (m *MockExecutor) Down(ctx context.Context, projectDir string, files ...string) (string, error) {
m.Calls = append(m.Calls, MockCall{Method: "Down", Args: append([]string{projectDir}, files...)})
if m.DownFn != nil {
return m.DownFn(ctx, projectDir, files...)
}
return "", nil
}
// Ps implements ComposeExecutor.
func (m *MockExecutor) Ps(ctx context.Context, projectDir string, files ...string) (string, error) {
m.Calls = append(m.Calls, MockCall{Method: "Ps", Args: append([]string{projectDir}, files...)})
if m.PsFn != nil {
return m.PsFn(ctx, projectDir, files...)
}
return "", nil
}
// Logs implements ComposeExecutor.
func (m *MockExecutor) Logs(ctx context.Context, projectDir string, service string, files ...string) (string, error) {
m.Calls = append(m.Calls, MockCall{Method: "Logs", Args: append([]string{projectDir, service}, files...)})
if m.LogsFn != nil {
return m.LogsFn(ctx, projectDir, service, files...)
}
return "", nil
}
// Version implements ComposeExecutor.
func (m *MockExecutor) Version(ctx context.Context) (string, error) {
m.Calls = append(m.Calls, MockCall{Method: "Version"})
if m.VersionFn != nil {
return m.VersionFn(ctx)
}
return "2.24.0", nil
}
// IsAvailable implements ComposeExecutor.
func (m *MockExecutor) IsAvailable(ctx context.Context) error {
m.Calls = append(m.Calls, MockCall{Method: "IsAvailable"})
if m.IsAvailableFn != nil {
return m.IsAvailableFn(ctx)
}
return nil
}