Skip to content

Commit 49dffc8

Browse files
committed
feat: docker stop action
1 parent d6945f0 commit 49dffc8

5 files changed

Lines changed: 128 additions & 19 deletions

File tree

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ type Step struct {
9999
Version string `json:"version"`
100100
User string `json:"user"`
101101
Action string `json:"action"`
102+
Detach bool `json:"detach"`
102103
Stdin bool `json:"stdin"`
103104
Command []string `json:"command"`
104105
Timeout int `json:"timeout"`

internal/engine/docker.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var killTimeout = 5 * time.Second
2323
const (
2424
actionRun = "run"
2525
actionExec = "exec"
26+
actionStop = "stop"
2627
)
2728

2829
// A Docker engine executes a specific sandbox command
@@ -125,9 +126,9 @@ func (e *Docker) execStep(step *config.Step, req Request, dir string, files File
125126

126127
// getBox selects an appropriate box for the step (if any).
127128
func (e *Docker) getBox(step *config.Step, req Request) (*config.Box, error) {
128-
if step.Action == actionExec {
129-
// exec steps use existing instances
130-
// and do not spin up new boxes
129+
if step.Action != actionRun {
130+
// steps other than "run" use existing containers
131+
// and do not spin up new ones
131132
return nil, nil
132133
}
133134
var boxName string
@@ -244,11 +245,14 @@ func (e *Docker) exec(box *config.Box, step *config.Step, req Request, dir strin
244245
// buildArgs prepares the arguments for the `docker` command.
245246
func (e *Docker) buildArgs(box *config.Box, step *config.Step, req Request, dir string) []string {
246247
var args []string
247-
if step.Action == actionRun {
248+
switch step.Action {
249+
case actionRun:
248250
args = dockerRunArgs(box, step, req, dir)
249-
} else if step.Action == actionExec {
250-
args = dockerExecArgs(step)
251-
} else {
251+
case actionExec:
252+
args = dockerExecArgs(step, req)
253+
case actionStop:
254+
args = dockerStopArgs(step, req)
255+
default:
252256
// should never happen if the config is valid
253257
args = []string{"version"}
254258
}
@@ -271,12 +275,15 @@ func dockerRunArgs(box *config.Box, step *config.Step, req Request, dir string)
271275
"--pids-limit", strconv.Itoa(box.NProc),
272276
"--user", step.User,
273277
}
274-
if !box.Writable {
275-
args = append(args, "--read-only")
278+
if step.Detach {
279+
args = append(args, "--detach")
276280
}
277281
if step.Stdin {
278282
args = append(args, "--interactive")
279283
}
284+
if !box.Writable {
285+
args = append(args, "--read-only")
286+
}
280287
if box.Storage != "" {
281288
args = append(args, "--storage-opt", fmt.Sprintf("size=%s", box.Storage))
282289
}
@@ -300,14 +307,23 @@ func dockerRunArgs(box *config.Box, step *config.Step, req Request, dir string)
300307
}
301308

302309
// dockerExecArgs prepares the arguments for the `docker exec` command.
303-
func dockerExecArgs(step *config.Step) []string {
310+
func dockerExecArgs(step *config.Step, req Request) []string {
311+
// :name means executing in the container passed in the request
312+
box := strings.Replace(step.Box, ":name", req.ID, 1)
304313
return []string{
305314
actionExec, "--interactive",
306315
"--user", step.User,
307-
step.Box,
316+
box,
308317
}
309318
}
310319

320+
// dockerStopArgs prepares the arguments for the `docker stop` command.
321+
func dockerStopArgs(step *config.Step, req Request) []string {
322+
// :name means executing in the container passed in the request
323+
box := strings.Replace(step.Box, ":name", req.ID, 1)
324+
return []string{actionStop, box}
325+
}
326+
311327
// filesReader creates a reader over an in-memory collection of files.
312328
func filesReader(files Files) io.Reader {
313329
var input strings.Builder

internal/engine/docker_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ var dockerCfg = &config.Config{
5959
},
6060
},
6161
Commands: map[string]config.SandboxCommands{
62+
"alpine": map[string]*config.Command{
63+
"echo": {
64+
Engine: "docker",
65+
Before: &config.Step{
66+
Box: "alpine", User: "sandbox", Action: "run", Detach: true,
67+
Command: []string{"echo", "before"},
68+
NOutput: 4096,
69+
},
70+
Steps: []*config.Step{
71+
{
72+
Box: ":name", User: "sandbox", Action: "exec",
73+
Command: []string{"sh", "main.sh"},
74+
NOutput: 4096,
75+
},
76+
},
77+
After: &config.Step{
78+
Box: ":name", User: "sandbox", Action: "stop",
79+
NOutput: 4096,
80+
},
81+
},
82+
},
6283
"go": map[string]*config.Command{
6384
"run": {
6485
Engine: "docker",
@@ -297,6 +318,49 @@ func TestDockerExec(t *testing.T) {
297318
})
298319
}
299320

321+
func TestDockerStop(t *testing.T) {
322+
logx.Mock()
323+
commands := map[string]execy.CmdOut{
324+
"docker run": {Stdout: "c958ff2", Stderr: "", Err: nil},
325+
"docker exec": {Stdout: "hello", Stderr: "", Err: nil},
326+
"docker stop": {Stdout: "alpine_42", Stderr: "", Err: nil},
327+
}
328+
mem := execy.Mock(commands)
329+
engine := NewDocker(dockerCfg, "alpine", "echo")
330+
331+
t.Run("success", func(t *testing.T) {
332+
req := Request{
333+
ID: "alpine_42",
334+
Sandbox: "alpine",
335+
Command: "echo",
336+
Files: map[string]string{
337+
"": "echo hello",
338+
},
339+
}
340+
out := engine.Exec(req)
341+
342+
if out.ID != req.ID {
343+
t.Errorf("ID: expected %s, got %s", req.ID, out.ID)
344+
}
345+
if !out.OK {
346+
t.Error("OK: expected true")
347+
}
348+
want := "hello"
349+
if out.Stdout != want {
350+
t.Errorf("Stdout: expected %q, got %q", want, out.Stdout)
351+
}
352+
if out.Stderr != "" {
353+
t.Errorf("Stderr: expected %q, got %q", "", out.Stdout)
354+
}
355+
if out.Err != nil {
356+
t.Errorf("Err: expected nil, got %v", out.Err)
357+
}
358+
mem.MustHave(t, "docker run --rm --name alpine_42", "--detach")
359+
mem.MustHave(t, "docker exec --interactive --user sandbox alpine_42 sh main.sh")
360+
mem.MustHave(t, "docker stop alpine_42")
361+
})
362+
}
363+
300364
func Test_expandVars(t *testing.T) {
301365
const name = "codapi_01"
302366
commands := map[string]string{

internal/logx/memory.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,35 @@ func (m *Memory) WriteString(s string) {
3030
}
3131

3232
// Has returns true if the memory has the message.
33-
func (m *Memory) Has(msg string) bool {
33+
func (m *Memory) Has(message ...string) bool {
3434
for _, line := range m.Lines {
35-
if strings.Contains(line, msg) {
35+
containsAll := true
36+
for _, part := range message {
37+
if !strings.Contains(line, part) {
38+
containsAll = false
39+
break
40+
}
41+
}
42+
if containsAll {
3643
return true
3744
}
3845
}
3946
return false
4047
}
4148

4249
// MustHave checks if the memory has the message.
43-
func (m *Memory) MustHave(t *testing.T, msg string) {
44-
if !m.Has(msg) {
45-
t.Errorf("%s must have: %s", m.Name, msg)
50+
// If the message consists of several parts,
51+
// they must all be in the same memory line.
52+
func (m *Memory) MustHave(t *testing.T, message ...string) {
53+
if !m.Has(message...) {
54+
t.Errorf("%s must have: %v", m.Name, message)
4655
}
4756
}
4857

4958
// MustNotHave checks if the memory does not have the message.
50-
func (m *Memory) MustNotHave(t *testing.T, msg string) {
51-
if m.Has(msg) {
52-
t.Errorf("%s must NOT have: %s", m.Name, msg)
59+
func (m *Memory) MustNotHave(t *testing.T, message ...string) {
60+
if m.Has(message...) {
61+
t.Errorf("%s must NOT have: %v", m.Name, message)
5362
}
5463
}
5564

internal/logx/memory_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,23 @@ func TestMemory_Has(t *testing.T) {
4040
if !mem.Has("hello world") {
4141
t.Error("Has: unexpected false")
4242
}
43+
_, _ = mem.Write([]byte("one two three four"))
44+
if !mem.Has("one two") {
45+
t.Error("Has: one two: unexpected false")
46+
}
47+
if !mem.Has("two three") {
48+
t.Error("Has: two three: unexpected false")
49+
}
50+
if mem.Has("one three") {
51+
t.Error("Has: one three: unexpected true")
52+
}
53+
if !mem.Has("one", "three") {
54+
t.Error("Has: [one three]: unexpected false")
55+
}
56+
if !mem.Has("one", "three", "four") {
57+
t.Error("Has: [one three four]: unexpected false")
58+
}
59+
if !mem.Has("four", "three") {
60+
t.Error("Has: [four three]: unexpected false")
61+
}
4362
}

0 commit comments

Comments
 (0)