feat: docker stop action
This commit is contained in:
@@ -99,6 +99,7 @@ type Step struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
|
Detach bool `json:"detach"`
|
||||||
Stdin bool `json:"stdin"`
|
Stdin bool `json:"stdin"`
|
||||||
Command []string `json:"command"`
|
Command []string `json:"command"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ var killTimeout = 5 * time.Second
|
|||||||
const (
|
const (
|
||||||
actionRun = "run"
|
actionRun = "run"
|
||||||
actionExec = "exec"
|
actionExec = "exec"
|
||||||
|
actionStop = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Docker engine executes a specific sandbox command
|
// 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
|
|||||||
|
|
||||||
// getBox selects an appropriate box for the step (if any).
|
// getBox selects an appropriate box for the step (if any).
|
||||||
func (e *Docker) getBox(step *config.Step, req Request) (*config.Box, error) {
|
func (e *Docker) getBox(step *config.Step, req Request) (*config.Box, error) {
|
||||||
if step.Action == actionExec {
|
if step.Action != actionRun {
|
||||||
// exec steps use existing instances
|
// steps other than "run" use existing containers
|
||||||
// and do not spin up new boxes
|
// and do not spin up new ones
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var boxName string
|
var boxName string
|
||||||
@@ -244,11 +245,14 @@ func (e *Docker) exec(box *config.Box, step *config.Step, req Request, dir strin
|
|||||||
// buildArgs prepares the arguments for the `docker` command.
|
// buildArgs prepares the arguments for the `docker` command.
|
||||||
func (e *Docker) buildArgs(box *config.Box, step *config.Step, req Request, dir string) []string {
|
func (e *Docker) buildArgs(box *config.Box, step *config.Step, req Request, dir string) []string {
|
||||||
var args []string
|
var args []string
|
||||||
if step.Action == actionRun {
|
switch step.Action {
|
||||||
|
case actionRun:
|
||||||
args = dockerRunArgs(box, step, req, dir)
|
args = dockerRunArgs(box, step, req, dir)
|
||||||
} else if step.Action == actionExec {
|
case actionExec:
|
||||||
args = dockerExecArgs(step)
|
args = dockerExecArgs(step, req)
|
||||||
} else {
|
case actionStop:
|
||||||
|
args = dockerStopArgs(step, req)
|
||||||
|
default:
|
||||||
// should never happen if the config is valid
|
// should never happen if the config is valid
|
||||||
args = []string{"version"}
|
args = []string{"version"}
|
||||||
}
|
}
|
||||||
@@ -271,12 +275,15 @@ func dockerRunArgs(box *config.Box, step *config.Step, req Request, dir string)
|
|||||||
"--pids-limit", strconv.Itoa(box.NProc),
|
"--pids-limit", strconv.Itoa(box.NProc),
|
||||||
"--user", step.User,
|
"--user", step.User,
|
||||||
}
|
}
|
||||||
if !box.Writable {
|
if step.Detach {
|
||||||
args = append(args, "--read-only")
|
args = append(args, "--detach")
|
||||||
}
|
}
|
||||||
if step.Stdin {
|
if step.Stdin {
|
||||||
args = append(args, "--interactive")
|
args = append(args, "--interactive")
|
||||||
}
|
}
|
||||||
|
if !box.Writable {
|
||||||
|
args = append(args, "--read-only")
|
||||||
|
}
|
||||||
if box.Storage != "" {
|
if box.Storage != "" {
|
||||||
args = append(args, "--storage-opt", fmt.Sprintf("size=%s", box.Storage))
|
args = append(args, "--storage-opt", fmt.Sprintf("size=%s", box.Storage))
|
||||||
}
|
}
|
||||||
@@ -300,14 +307,23 @@ func dockerRunArgs(box *config.Box, step *config.Step, req Request, dir string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dockerExecArgs prepares the arguments for the `docker exec` command.
|
// dockerExecArgs prepares the arguments for the `docker exec` command.
|
||||||
func dockerExecArgs(step *config.Step) []string {
|
func dockerExecArgs(step *config.Step, req Request) []string {
|
||||||
|
// :name means executing in the container passed in the request
|
||||||
|
box := strings.Replace(step.Box, ":name", req.ID, 1)
|
||||||
return []string{
|
return []string{
|
||||||
actionExec, "--interactive",
|
actionExec, "--interactive",
|
||||||
"--user", step.User,
|
"--user", step.User,
|
||||||
step.Box,
|
box,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dockerStopArgs prepares the arguments for the `docker stop` command.
|
||||||
|
func dockerStopArgs(step *config.Step, req Request) []string {
|
||||||
|
// :name means executing in the container passed in the request
|
||||||
|
box := strings.Replace(step.Box, ":name", req.ID, 1)
|
||||||
|
return []string{actionStop, box}
|
||||||
|
}
|
||||||
|
|
||||||
// filesReader creates a reader over an in-memory collection of files.
|
// filesReader creates a reader over an in-memory collection of files.
|
||||||
func filesReader(files Files) io.Reader {
|
func filesReader(files Files) io.Reader {
|
||||||
var input strings.Builder
|
var input strings.Builder
|
||||||
|
|||||||
@@ -59,6 +59,27 @@ var dockerCfg = &config.Config{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Commands: map[string]config.SandboxCommands{
|
Commands: map[string]config.SandboxCommands{
|
||||||
|
"alpine": map[string]*config.Command{
|
||||||
|
"echo": {
|
||||||
|
Engine: "docker",
|
||||||
|
Before: &config.Step{
|
||||||
|
Box: "alpine", User: "sandbox", Action: "run", Detach: true,
|
||||||
|
Command: []string{"echo", "before"},
|
||||||
|
NOutput: 4096,
|
||||||
|
},
|
||||||
|
Steps: []*config.Step{
|
||||||
|
{
|
||||||
|
Box: ":name", User: "sandbox", Action: "exec",
|
||||||
|
Command: []string{"sh", "main.sh"},
|
||||||
|
NOutput: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
After: &config.Step{
|
||||||
|
Box: ":name", User: "sandbox", Action: "stop",
|
||||||
|
NOutput: 4096,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"go": map[string]*config.Command{
|
"go": map[string]*config.Command{
|
||||||
"run": {
|
"run": {
|
||||||
Engine: "docker",
|
Engine: "docker",
|
||||||
@@ -297,6 +318,49 @@ func TestDockerExec(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDockerStop(t *testing.T) {
|
||||||
|
logx.Mock()
|
||||||
|
commands := map[string]execy.CmdOut{
|
||||||
|
"docker run": {Stdout: "c958ff2", Stderr: "", Err: nil},
|
||||||
|
"docker exec": {Stdout: "hello", Stderr: "", Err: nil},
|
||||||
|
"docker stop": {Stdout: "alpine_42", Stderr: "", Err: nil},
|
||||||
|
}
|
||||||
|
mem := execy.Mock(commands)
|
||||||
|
engine := NewDocker(dockerCfg, "alpine", "echo")
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
req := Request{
|
||||||
|
ID: "alpine_42",
|
||||||
|
Sandbox: "alpine",
|
||||||
|
Command: "echo",
|
||||||
|
Files: map[string]string{
|
||||||
|
"": "echo hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out := engine.Exec(req)
|
||||||
|
|
||||||
|
if out.ID != req.ID {
|
||||||
|
t.Errorf("ID: expected %s, got %s", req.ID, out.ID)
|
||||||
|
}
|
||||||
|
if !out.OK {
|
||||||
|
t.Error("OK: expected true")
|
||||||
|
}
|
||||||
|
want := "hello"
|
||||||
|
if out.Stdout != want {
|
||||||
|
t.Errorf("Stdout: expected %q, got %q", want, out.Stdout)
|
||||||
|
}
|
||||||
|
if out.Stderr != "" {
|
||||||
|
t.Errorf("Stderr: expected %q, got %q", "", out.Stdout)
|
||||||
|
}
|
||||||
|
if out.Err != nil {
|
||||||
|
t.Errorf("Err: expected nil, got %v", out.Err)
|
||||||
|
}
|
||||||
|
mem.MustHave(t, "docker run --rm --name alpine_42", "--detach")
|
||||||
|
mem.MustHave(t, "docker exec --interactive --user sandbox alpine_42 sh main.sh")
|
||||||
|
mem.MustHave(t, "docker stop alpine_42")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_expandVars(t *testing.T) {
|
func Test_expandVars(t *testing.T) {
|
||||||
const name = "codapi_01"
|
const name = "codapi_01"
|
||||||
commands := map[string]string{
|
commands := map[string]string{
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ func (m *Memory) WriteString(s string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Has returns true if the memory has the message.
|
// Has returns true if the memory has the message.
|
||||||
func (m *Memory) Has(msg string) bool {
|
func (m *Memory) Has(message ...string) bool {
|
||||||
for _, line := range m.Lines {
|
for _, line := range m.Lines {
|
||||||
if strings.Contains(line, msg) {
|
containsAll := true
|
||||||
|
for _, part := range message {
|
||||||
|
if !strings.Contains(line, part) {
|
||||||
|
containsAll = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if containsAll {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +47,18 @@ func (m *Memory) Has(msg string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MustHave checks if the memory has the message.
|
// MustHave checks if the memory has the message.
|
||||||
func (m *Memory) MustHave(t *testing.T, msg string) {
|
// If the message consists of several parts,
|
||||||
if !m.Has(msg) {
|
// they must all be in the same memory line.
|
||||||
t.Errorf("%s must have: %s", m.Name, msg)
|
func (m *Memory) MustHave(t *testing.T, message ...string) {
|
||||||
|
if !m.Has(message...) {
|
||||||
|
t.Errorf("%s must have: %v", m.Name, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustNotHave checks if the memory does not have the message.
|
// MustNotHave checks if the memory does not have the message.
|
||||||
func (m *Memory) MustNotHave(t *testing.T, msg string) {
|
func (m *Memory) MustNotHave(t *testing.T, message ...string) {
|
||||||
if m.Has(msg) {
|
if m.Has(message...) {
|
||||||
t.Errorf("%s must NOT have: %s", m.Name, msg)
|
t.Errorf("%s must NOT have: %v", m.Name, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,23 @@ func TestMemory_Has(t *testing.T) {
|
|||||||
if !mem.Has("hello world") {
|
if !mem.Has("hello world") {
|
||||||
t.Error("Has: unexpected false")
|
t.Error("Has: unexpected false")
|
||||||
}
|
}
|
||||||
|
_, _ = mem.Write([]byte("one two three four"))
|
||||||
|
if !mem.Has("one two") {
|
||||||
|
t.Error("Has: one two: unexpected false")
|
||||||
|
}
|
||||||
|
if !mem.Has("two three") {
|
||||||
|
t.Error("Has: two three: unexpected false")
|
||||||
|
}
|
||||||
|
if mem.Has("one three") {
|
||||||
|
t.Error("Has: one three: unexpected true")
|
||||||
|
}
|
||||||
|
if !mem.Has("one", "three") {
|
||||||
|
t.Error("Has: [one three]: unexpected false")
|
||||||
|
}
|
||||||
|
if !mem.Has("one", "three", "four") {
|
||||||
|
t.Error("Has: [one three four]: unexpected false")
|
||||||
|
}
|
||||||
|
if !mem.Has("four", "three") {
|
||||||
|
t.Error("Has: [four three]: unexpected false")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user