From 1fc564503ba57f01da9e747d617fe5c4b1696f60 Mon Sep 17 00:00:00 2001 From: Ayush Jain Date: Mon, 25 Mar 2019 15:28:51 +0530 Subject: [PATCH] Add support for executing multiple commands --- .dunner.yaml | 18 ++++++----- pkg/config/config.go | 14 ++++----- pkg/docker/docker.go | 74 +++++++++++++++++++++++++++++++++++++------- pkg/dunner/dunner.go | 67 +++++++++++++++++++-------------------- 4 files changed, 114 insertions(+), 59 deletions(-) diff --git a/.dunner.yaml b/.dunner.yaml index c64c3a1..14fd7d7 100644 --- a/.dunner.yaml +++ b/.dunner.yaml @@ -1,15 +1,18 @@ build: - image: node:10.15.0 - command: ["node", "--version"] - - image: node:10.15.0 - command: ["npm", "--version"] + commands: + - ["node", "--version"] + - ["npm", "--version"] - image: alpine dir: pkg - command: ["pwd"] + commands: + - ["pwd"] - image: alpine - command: ["apk", "update"] + commands: + - ["apk", "update"] - image: alpine - command: ["printenv"] + commands: + - ["printenv"] envs: - PERM=775 - ID=dunner @@ -19,7 +22,8 @@ build: - '/root' show: - image: alpine - command: ["ls", "$1"] + commands: + - ["ls", "$1"] mounts: - '~/Downloads:/root/down' - ~/Pictures:/root/pics:wr diff --git a/pkg/config/config.go b/pkg/config/config.go index aeaa83f..b475bca 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,13 +27,13 @@ type DirMount struct { // Task describes a single task to be run in a docker container type Task struct { - Name string `yaml:"name"` - Image string `yaml:"image"` - SubDir string `yaml:"dir"` - Command []string `yaml:"command"` - Envs []string `yaml:"envs"` - Mounts []string `yaml:"mounts"` - Args []string `yaml:"args"` + Name string `yaml:"name"` + Image string `yaml:"image"` + SubDir string `yaml:"dir"` + Commands [][]string `yaml:"commands"` + Envs []string `yaml:"envs"` + Mounts []string `yaml:"mounts"` + Args []string `yaml:"args"` } // Configs describes the parsed information from the dunner file diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go index a7dae36..b4abdc9 100644 --- a/pkg/docker/docker.go +++ b/pkg/docker/docker.go @@ -1,17 +1,21 @@ package docker import ( + "bytes" "context" - "io" + "fmt" "io/ioutil" "os" "path/filepath" + "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/term" "github.com/leopardslab/dunner/internal/logger" "github.com/spf13/viper" @@ -24,7 +28,7 @@ type Step struct { Task string Name string Image string - Command []string + Commands [][]string Env []string WorkDir string Volumes map[string]string @@ -32,8 +36,15 @@ type Step struct { Args []string } +// Result stores the output of commands run using docker exec +type Result struct { + Command string + Output string + Error string +} + // Exec method is used to execute the task described in the corresponding step -func (step Step) Exec() (*io.ReadCloser, error) { +func (step Step) Exec() (*[]Result, error) { var ( hostMountFilepath = "./" @@ -87,7 +98,7 @@ func (step Step) Exec() (*io.ReadCloser, error) { ctx, &container.Config{ Image: step.Image, - Cmd: step.Command, + Cmd: []string{"tail", "-f", "/dev/null"}, Env: step.Env, WorkingDir: containerWorkingDir, }, @@ -103,27 +114,66 @@ func (step Step) Exec() (*io.ReadCloser, error) { log.Fatal(err) } + if len(resp.Warnings) > 0 { + for warning := range resp.Warnings { + log.Warn(warning) + } + } + if err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { log.Fatal(err) } - statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) - select { - case err = <-errCh: + defer func() { + dur, err := time.ParseDuration("-1ns") // Negative duration means no force termination + if err != nil { + log.Fatal(err) + } + if err = cli.ContainerStop(ctx, resp.ID, &dur); err != nil { + log.Fatal(err) + } + }() + + var results []Result + for _, cmd := range step.Commands { + r, err := runCmd(ctx, cli, resp.ID, cmd) if err != nil { log.Fatal(err) } - case <-statusCh: + results = append(results, *r) } - out, err = cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, + return &results, nil +} + +func runCmd(ctx context.Context, cli *client.Client, containerID string, command []string) (*Result, error) { + if len(command) == 0 { + return nil, fmt.Errorf(`config: Command cannot be empty`) + } + + exec, err := cli.ContainerExecCreate(ctx, containerID, types.ExecConfig{ + Cmd: command, + AttachStdout: true, + AttachStderr: true, }) if err != nil { log.Fatal(err) } - return &out, nil + resp, err := cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{}) + if err != nil { + log.Fatal(err) + } + defer resp.Close() + var out, errOut bytes.Buffer + if _, err = stdcopy.StdCopy(&out, &errOut, resp.Reader); err != nil { + log.Fatal(err) + } + var result = Result{ + Command: strings.Join(command, " "), + Output: out.String(), + Error: errOut.String(), + } + return &result, nil } diff --git a/pkg/dunner/dunner.go b/pkg/dunner/dunner.go index c30e2ce..0a334cf 100644 --- a/pkg/dunner/dunner.go +++ b/pkg/dunner/dunner.go @@ -1,13 +1,12 @@ package dunner import ( - "os" + "fmt" "regexp" "strconv" "strings" "sync" - "github.com/docker/docker/pkg/stdcopy" "github.com/leopardslab/dunner/internal/logger" "github.com/leopardslab/dunner/pkg/config" "github.com/leopardslab/dunner/pkg/docker" @@ -44,13 +43,13 @@ func execTask(configs *config.Configs, taskName string, args []string) { wg.Add(1) } step := docker.Step{ - Task: taskName, - Name: stepDefinition.Name, - Image: stepDefinition.Image, - Command: stepDefinition.Command, - Env: stepDefinition.Envs, - WorkDir: stepDefinition.SubDir, - Args: stepDefinition.Args, + Task: taskName, + Name: stepDefinition.Name, + Image: stepDefinition.Image, + Commands: stepDefinition.Commands, + Env: stepDefinition.Envs, + WorkDir: stepDefinition.SubDir, + Args: stepDefinition.Args, } if err := config.DecodeMount(stepDefinition.Mounts, &step); err != nil { @@ -95,38 +94,40 @@ func process(configs *config.Configs, s *docker.Step, wg *sync.WaitGroup, args [ log.Fatalf(`dunner: image repository name cannot be empty`) } - pout, err := (*s).Exec() + results, err := (*s).Exec() if err != nil { log.Fatal(err) } - log.Infof( - "Running task '%+v' on '%+v' Docker with command '%+v'", - s.Task, - s.Image, - strings.Join(s.Command, " "), - ) - - if _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, *pout); err != nil { - log.Fatal(err) - } - - if err = (*pout).Close(); err != nil { - log.Fatal(err) + for _, res := range *results { + log.Infof( + "Running task '%+v' on '%+v' Docker with command '%+v'", + s.Task, + s.Image, + res.Command, + ) + if res.Output != "" { + fmt.Printf(`OUT: %s`, res.Output) + } + if res.Error != "" { + fmt.Printf(`ERR: %s`, res.Error) + } } } func passArgs(s *docker.Step, args *[]string) error { - for i, subStr := range s.Command { - regex := regexp.MustCompile(`\$[1-9][0-9]*`) - subStr = regex.ReplaceAllStringFunc(subStr, func(str string) string { - j, err := strconv.Atoi(strings.Trim(str, "$")) - if err != nil { - log.Fatal(err) - } - return (*args)[j-1] - }) - s.Command[i] = subStr + for i, cmd := range s.Commands { + for j, subStr := range cmd { + regex := regexp.MustCompile(`\$[1-9][0-9]*`) + subStr = regex.ReplaceAllStringFunc(subStr, func(str string) string { + j, err := strconv.Atoi(strings.Trim(str, "$")) + if err != nil { + log.Fatal(err) + } + return (*args)[j-1] + }) + s.Commands[i][j] = subStr + } } return nil }