From 2620b4597fc9eb9d5689a3fcf43e609257953de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20Cl=C3=A9ment?= Date: Mon, 6 Nov 2023 18:58:03 +0100 Subject: [PATCH] Allow multiple steps for jobs, fix error in template --- client/jobfile/jobfile.go | 26 ++-- client/jobfile/reader.go | 50 +++--- .../tests/jobfile/invalid_env_keys.yaml | 6 +- .../tests/jobfile/invalid_image_context.yaml | 6 +- .../jobfile/invalid_image_dockerfile.yaml | 6 +- .../invalid_missing_image_context.yaml | 4 +- .../invalid_missing_image_dockerfile.yaml | 4 +- .../tests/jobfile/invalid_missing_name.yaml | 6 +- .../invalid_missing_services_health_cmd.yaml | 6 +- .../invalid_missing_services_image.yaml | 6 +- .../jobfile/tests/jobfile/invalid_name.yaml | 6 +- .../jobfile/invalid_services_env_keys.yaml | 6 +- .../invalid_services_health_timeout.yaml | 6 +- .../tests/jobfile/invalid_services_keys.yaml | 6 +- .../tests/jobfile/invalid_services_map.yaml | 6 +- .../tests/jobfile/invalid_version.yaml | 6 +- .../tests/jobfile/valid_full_featured.yaml | 6 +- .../tests/jobfile/valid_minimalist.yaml | 6 +- client/main.go | 2 +- client/run.go | 13 +- hack/job.sh | 2 +- hack/job.yaml | 10 +- proto/alfred.pb.go | 12 +- proto/alfred.proto | 2 +- provisioner/internal/docker.go | 145 ++++++++++-------- provisioner/openstack/node.go | 6 +- 26 files changed, 192 insertions(+), 168 deletions(-) diff --git a/client/jobfile/jobfile.go b/client/jobfile/jobfile.go index 3603718..7fe2967 100644 --- a/client/jobfile/jobfile.go +++ b/client/jobfile/jobfile.go @@ -16,7 +16,7 @@ type Jobfile struct { Version string Name string - Image JobfileImage + Steps []JobfileImage Env map[string]string Services map[string]JobfileService Tasks []string @@ -53,18 +53,20 @@ func (jobfile Jobfile) Validate() error { return fmt.Errorf("name must be a valid identifier") } - if jobfile.Image.Dockerfile == "" { - return fmt.Errorf("image.dockerfile is required") - } - if _, err := os.Stat(path.Join(jobfile.path, jobfile.Image.Dockerfile)); os.IsNotExist(err) { - return fmt.Errorf("image.dockerfile must be an existing file on disk") - } + for _, step := range jobfile.Steps { + if step.Dockerfile == "" { + return fmt.Errorf("image.dockerfile is required") + } + if _, err := os.Stat(path.Join(jobfile.path, step.Dockerfile)); os.IsNotExist(err) { + return fmt.Errorf("image.dockerfile must be an existing file on disk") + } - if jobfile.Image.Context == "" { - return fmt.Errorf("image.context is required") - } - if _, err := os.Stat(path.Join(jobfile.path, jobfile.Image.Context)); os.IsNotExist(err) { - return fmt.Errorf("image.context must be an existing folder on disk") + if step.Context == "" { + return fmt.Errorf("image.context is required") + } + if _, err := os.Stat(path.Join(jobfile.path, step.Context)); os.IsNotExist(err) { + return fmt.Errorf("image.context must be an existing folder on disk") + } } for key := range jobfile.Env { diff --git a/client/jobfile/reader.go b/client/jobfile/reader.go index 4fe7054..75e5c94 100644 --- a/client/jobfile/reader.go +++ b/client/jobfile/reader.go @@ -56,23 +56,20 @@ func Read(file string, options ReadOptions) (job *proto.Job, err error) { return nil, UnmarshalError{fmt.Errorf("validate: %w", err), source} } - // Name job.Name = jobfile.Name - - // Tasks job.Tasks = jobfile.Tasks - - // Image - if job.Image, err = buildImage( - path.Join(workDir, jobfile.Image.Dockerfile), - path.Join(workDir, jobfile.Image.Context), - jobfile.Image.Options, - options, - ); err != nil { - return nil, fmt.Errorf("build: %w", err) + job.Steps = make([]string, len(jobfile.Steps)) + for i, step := range jobfile.Steps { + if job.Steps[i], err = buildImage( + path.Join(workDir, step.Dockerfile), + path.Join(workDir, step.Context), + step.Options, + options, + ); err != nil { + return nil, fmt.Errorf("build (%d): %w", i+1, err) + } } - // Env if len(jobfile.Env) > 0 { job.Env = lo.MapToSlice(jobfile.Env, func(key string, value string) *proto.Job_Env { return &proto.Job_Env{ @@ -82,7 +79,6 @@ func Read(file string, options ReadOptions) (job *proto.Job, err error) { }) } - // Services for name, service := range jobfile.Services { jobService := &proto.Job_Service{ Name: name, @@ -123,20 +119,25 @@ type TemplateData struct { } func evaluateTemplate(source string, dir string, options ReadOptions) (string, error) { + var userError error tmpl, err := template.New("jobfile").Funcs(template.FuncMap{ "base64": func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }, - "error": func(err string) error { - return errors.New(err) + "error": func(err string) any { + userError = errors.New(err) + panic(nil) }, - "exec": func(args ...string) (string, error) { + "exec": func(args ...string) string { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr - output, err := cmd.Output() - return strings.TrimRight(string(output), "\n\r"), err + if output, err := cmd.Output(); err != nil { + panic(err) + } else { + return strings.TrimRight(string(output), "\n\r") + } }, "join": func(sep string, s []string) string { return strings.Join(s, sep) @@ -148,13 +149,16 @@ func evaluateTemplate(source string, dir string, options ReadOptions) (string, e "lines": func(s string) []string { return strings.Split(s, "\n") }, - "shell": func(script string) (string, error) { + "shell": func(script string) string { cmd := exec.Command("/bin/sh", "-c", script) cmd.Dir = dir cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr - output, err := cmd.Output() - return strings.TrimRight(string(output), "\n\r"), err + if output, err := cmd.Output(); err != nil { + panic(err) + } else { + return strings.TrimRight(string(output), "\n\r") + } }, "split": func(sep string, s string) []string { return strings.Split(s, sep) @@ -178,7 +182,7 @@ func evaluateTemplate(source string, dir string, options ReadOptions) (string, e var output strings.Builder if err := tmpl.Execute(&output, data); err != nil { - return "", fmt.Errorf("failed to execute template: %w", err) + return "", lo.Ternary(userError != nil, userError, err) } return output.String(), nil diff --git a/client/jobfile/tests/jobfile/invalid_env_keys.yaml b/client/jobfile/tests/jobfile/invalid_env_keys.yaml index fc11418..79a0e78 100644 --- a/client/jobfile/tests/jobfile/invalid_env_keys.yaml +++ b/client/jobfile/tests/jobfile/invalid_env_keys.yaml @@ -1,7 +1,7 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." env: 0/2: "foo" diff --git a/client/jobfile/tests/jobfile/invalid_image_context.yaml b/client/jobfile/tests/jobfile/invalid_image_context.yaml index 43b9f65..4d55570 100644 --- a/client/jobfile/tests/jobfile/invalid_image_context.yaml +++ b/client/jobfile/tests/jobfile/invalid_image_context.yaml @@ -1,5 +1,5 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "./toto" +steps: + - dockerfile: "a.dockerfile" + context: "./toto" diff --git a/client/jobfile/tests/jobfile/invalid_image_dockerfile.yaml b/client/jobfile/tests/jobfile/invalid_image_dockerfile.yaml index 235af75..95f0988 100644 --- a/client/jobfile/tests/jobfile/invalid_image_dockerfile.yaml +++ b/client/jobfile/tests/jobfile/invalid_image_dockerfile.yaml @@ -1,5 +1,5 @@ version: "1" name: "test" -image: - dockerfile: "toto.dockerfile" - context: "." +steps: + - dockerfile: "toto.dockerfile" + context: "." diff --git a/client/jobfile/tests/jobfile/invalid_missing_image_context.yaml b/client/jobfile/tests/jobfile/invalid_missing_image_context.yaml index b9fd757..554399e 100644 --- a/client/jobfile/tests/jobfile/invalid_missing_image_context.yaml +++ b/client/jobfile/tests/jobfile/invalid_missing_image_context.yaml @@ -1,4 +1,4 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" +steps: + - dockerfile: "a.dockerfile" diff --git a/client/jobfile/tests/jobfile/invalid_missing_image_dockerfile.yaml b/client/jobfile/tests/jobfile/invalid_missing_image_dockerfile.yaml index 81a380e..43ac177 100644 --- a/client/jobfile/tests/jobfile/invalid_missing_image_dockerfile.yaml +++ b/client/jobfile/tests/jobfile/invalid_missing_image_dockerfile.yaml @@ -1,4 +1,4 @@ version: "1" name: "test" -image: - context: "." +steps: + - context: "." diff --git a/client/jobfile/tests/jobfile/invalid_missing_name.yaml b/client/jobfile/tests/jobfile/invalid_missing_name.yaml index 86f7c0f..1461daa 100644 --- a/client/jobfile/tests/jobfile/invalid_missing_name.yaml +++ b/client/jobfile/tests/jobfile/invalid_missing_name.yaml @@ -1,4 +1,4 @@ version: "1" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." diff --git a/client/jobfile/tests/jobfile/invalid_missing_services_health_cmd.yaml b/client/jobfile/tests/jobfile/invalid_missing_services_health_cmd.yaml index 2e3fb3d..be73f91 100644 --- a/client/jobfile/tests/jobfile/invalid_missing_services_health_cmd.yaml +++ b/client/jobfile/tests/jobfile/invalid_missing_services_health_cmd.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: mysql: image: "mysql" diff --git a/client/jobfile/tests/jobfile/invalid_missing_services_image.yaml b/client/jobfile/tests/jobfile/invalid_missing_services_image.yaml index 8853905..41f37d2 100644 --- a/client/jobfile/tests/jobfile/invalid_missing_services_image.yaml +++ b/client/jobfile/tests/jobfile/invalid_missing_services_image.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: mysql: env: diff --git a/client/jobfile/tests/jobfile/invalid_name.yaml b/client/jobfile/tests/jobfile/invalid_name.yaml index 6c88413..8b62456 100644 --- a/client/jobfile/tests/jobfile/invalid_name.yaml +++ b/client/jobfile/tests/jobfile/invalid_name.yaml @@ -1,5 +1,5 @@ version: "1" name: "Toto ../" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." diff --git a/client/jobfile/tests/jobfile/invalid_services_env_keys.yaml b/client/jobfile/tests/jobfile/invalid_services_env_keys.yaml index 9c6d331..fafce8d 100644 --- a/client/jobfile/tests/jobfile/invalid_services_env_keys.yaml +++ b/client/jobfile/tests/jobfile/invalid_services_env_keys.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: mysql: image: "mysql" diff --git a/client/jobfile/tests/jobfile/invalid_services_health_timeout.yaml b/client/jobfile/tests/jobfile/invalid_services_health_timeout.yaml index 3535b5b..53af90b 100644 --- a/client/jobfile/tests/jobfile/invalid_services_health_timeout.yaml +++ b/client/jobfile/tests/jobfile/invalid_services_health_timeout.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: mysql: image: "mysql" diff --git a/client/jobfile/tests/jobfile/invalid_services_keys.yaml b/client/jobfile/tests/jobfile/invalid_services_keys.yaml index e773246..359b349 100644 --- a/client/jobfile/tests/jobfile/invalid_services_keys.yaml +++ b/client/jobfile/tests/jobfile/invalid_services_keys.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: 0/2: image: "mysql" diff --git a/client/jobfile/tests/jobfile/invalid_services_map.yaml b/client/jobfile/tests/jobfile/invalid_services_map.yaml index c15ff06..330e542 100644 --- a/client/jobfile/tests/jobfile/invalid_services_map.yaml +++ b/client/jobfile/tests/jobfile/invalid_services_map.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: - "mysql" - "redis" diff --git a/client/jobfile/tests/jobfile/invalid_version.yaml b/client/jobfile/tests/jobfile/invalid_version.yaml index 80dd0db..c946617 100644 --- a/client/jobfile/tests/jobfile/invalid_version.yaml +++ b/client/jobfile/tests/jobfile/invalid_version.yaml @@ -1,5 +1,5 @@ version: "42" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." diff --git a/client/jobfile/tests/jobfile/valid_full_featured.yaml b/client/jobfile/tests/jobfile/valid_full_featured.yaml index 1372bd3..31057b8 100644 --- a/client/jobfile/tests/jobfile/valid_full_featured.yaml +++ b/client/jobfile/tests/jobfile/valid_full_featured.yaml @@ -1,8 +1,8 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." services: redis: image: "redis:7.0.5-alpine" diff --git a/client/jobfile/tests/jobfile/valid_minimalist.yaml b/client/jobfile/tests/jobfile/valid_minimalist.yaml index d727418..8508429 100644 --- a/client/jobfile/tests/jobfile/valid_minimalist.yaml +++ b/client/jobfile/tests/jobfile/valid_minimalist.yaml @@ -1,5 +1,5 @@ version: "1" name: "test" -image: - dockerfile: "a.dockerfile" - context: "." +steps: + - dockerfile: "a.dockerfile" + context: "." diff --git a/client/main.go b/client/main.go index 90a8828..62bd83d 100644 --- a/client/main.go +++ b/client/main.go @@ -46,7 +46,7 @@ var alfredCmd = &cobra.Command{ port = "25373" } sshTunneling := lo.Must(cmd.Flags().GetBool("ssh-tunneling")) - if host == "127.0.0.1" && !cmd.Flags().Changed("ssh-tunneling") { + if (host == "127.0.0.1" || host == "localhost") && !cmd.Flags().Changed("ssh-tunneling") { sshTunneling = false } diff --git a/client/run.go b/client/run.go index c326ce4..9e3652b 100644 --- a/client/run.go +++ b/client/run.go @@ -52,13 +52,14 @@ var runCmd = &cobra.Command{ return yaml.NewEncoder(cmd.OutOrStdout()).Encode(j) } - spinner = ui.NewSpinner("Uploading image to server") - if err = sendImageToServer(cmd, j.Image); err != nil { - spinner.Fail() - return fmt.Errorf("failed to send image to server: %w", err) - } else { - spinner.Success() + spinner = ui.NewSpinner("Uploading images to server") + for _, image := range j.Steps { + if err = sendImageToServer(cmd, image); err != nil { + spinner.Fail() + return fmt.Errorf("failed to send image '%s' to server: %w", image, err) + } } + spinner.Success() spinner = ui.NewSpinner("Scheduling job") job, err := client.ScheduleJob(cmd.Context(), &proto.ScheduleJobRequest{Job: j}) diff --git a/hack/job.sh b/hack/job.sh index 898e9da..2d61076 100644 --- a/hack/job.sh +++ b/hack/job.sh @@ -3,4 +3,4 @@ #mysql -u root -proot -h mysql mysql <<< 'SHOW DATABASES;' # ^- binary ^- host ^- database -echo "Hello, $ALFRED_TASK" > $ALFRED_OUTPUT/hello.txt \ No newline at end of file +echo "Hello, $ALFRED_TASK" >> $ALFRED_OUTPUT/hello.txt \ No newline at end of file diff --git a/hack/job.yaml b/hack/job.yaml index 63fbb87..c281a71 100644 --- a/hack/job.yaml +++ b/hack/job.yaml @@ -1,8 +1,5 @@ version: "1" name: "test" -image: - dockerfile: "job.dockerfile" - context: "." services: # redis: # image: "redis:7.0.5-alpine" @@ -20,8 +17,13 @@ services: # timeout: "10s" # interval: "2s" # retries: 20 +steps: + - dockerfile: "job.dockerfile" + context: "." + - dockerfile: "job.dockerfile" + context: "." tasks: - {{range (lines (shell "sloth instance --app tipee list -o json | jq -r '.[] | select(.features.tests) | .id' | tail -n 2"))}} + {{range (lines (shell "sloth instance --app tipee list -o json | jq -r '.[] | select(.features.tests) | .id' | tail -n 1"))}} - {{.}} {{end}} diff --git a/proto/alfred.pb.go b/proto/alfred.pb.go index d1ffe2e..c91493b 100644 --- a/proto/alfred.pb.go +++ b/proto/alfred.pb.go @@ -203,7 +203,7 @@ type Job struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` About string `protobuf:"bytes,2,opt,name=about,proto3" json:"about,omitempty"` - Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"` + Steps []string `protobuf:"bytes,3,rep,name=steps,proto3" json:"steps,omitempty"` Env []*Job_Env `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` Services []*Job_Service `protobuf:"bytes,5,rep,name=services,proto3" json:"services,omitempty"` Tasks []string `protobuf:"bytes,6,rep,name=tasks,proto3" json:"tasks,omitempty"` @@ -255,11 +255,11 @@ func (x *Job) GetAbout() string { return "" } -func (x *Job) GetImage() string { +func (x *Job) GetSteps() []string { if x != nil { - return x.Image + return x.Steps } - return "" + return nil } func (x *Job) GetEnv() []*Job_Env { @@ -1715,8 +1715,8 @@ var file_proto_alfred_proto_rawDesc = []byte{ 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x73, + 0x74, 0x65, 0x70, 0x73, 0x12, 0x20, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4a, 0x6f, 0x62, 0x2e, 0x45, 0x6e, 0x76, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x2e, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, diff --git a/proto/alfred.proto b/proto/alfred.proto index afb5768..22886fe 100644 --- a/proto/alfred.proto +++ b/proto/alfred.proto @@ -19,7 +19,7 @@ service Alfred { message Job { string name = 1; string about = 2; - string image = 3; + repeated string steps = 3; repeated Env env = 4; repeated Service services = 5; repeated string tasks = 6; diff --git a/provisioner/internal/docker.go b/provisioner/internal/docker.go index fcc6674..a80156d 100644 --- a/provisioner/internal/docker.go +++ b/provisioner/internal/docker.go @@ -60,58 +60,12 @@ func RunContainer( func() error { return taskFs.Delete("/") }, ) - for _, dir := range []string{"output"} { + for _, dir := range []string{"output", "shared"} { if err := taskFs.MkDir("/" + dir); err != nil { return -1, fmt.Errorf("failed to create workspace directory '%s': %w", dir, err) } } - // Create main container - resp, err := docker.ContainerCreate( - ctx, - &container.Config{ - Image: task.Job.Image, - Env: append( - lo.Map(task.Job.Env, func(jobEnv *proto.Job_Env, _ int) string { - return fmt.Sprintf("%s=%s", jobEnv.Key, jobEnv.Value) - }), - []string{ - fmt.Sprintf("ALFRED_TASK=%s", task.Name), - "ALFRED_OUTPUT=/alfred/output", - }..., - ), - }, - &container.HostConfig{ - AutoRemove: false, // Otherwise this will remove the container before we can get the logs - Mounts: []mount.Mount{ - { - Type: mount.TypeBind, - Source: taskFs.HostPath("/"), - Target: "/alfred", - }, - }, - }, - &network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{ - networkName: { - NetworkID: networkId, - }, - }, - }, - nil, - fmt.Sprintf("alfred-%s", task.FQN()), - ) - if err != nil { - return -1, fmt.Errorf("failed to create container: %w", err) - } - defer cleanup( - "main container", - func() error { - return docker.ContainerRemove(context.Background(), resp.ID, - types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) - }, - ) - // Environment variables for each service serviceEnv := map[string][]string{} // Container IDs for each service @@ -274,28 +228,87 @@ func RunContainer( return -1, fmt.Errorf("service: %w", err) } - // Start main container - wait, errChan := docker.ContainerWait(ctx, resp.ID, container.WaitConditionNextExit) - err = docker.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) - if err != nil { - return -1, fmt.Errorf("failed to start container: %w", err) - } + // Create and execute steps containers + var status container.WaitResponse + for i, image := range task.Job.Steps { + // Using a func here so that defer are called between each iteration + if err := func(stepIndex int) error { + resp, err := docker.ContainerCreate( + ctx, + &container.Config{ + Image: image, + Env: append( + lo.Map(task.Job.Env, func(jobEnv *proto.Job_Env, _ int) string { + return fmt.Sprintf("%s=%s", jobEnv.Key, jobEnv.Value) + }), + []string{ + fmt.Sprintf("ALFRED_TASK=%s", task.Name), + "ALFRED_SHARED=/alfred/shared", + "ALFRED_OUTPUT=/alfred/output", + }..., + ), + }, + &container.HostConfig{ + AutoRemove: false, // Otherwise this will remove the container before we can get the logs + Mounts: []mount.Mount{ + { + Type: mount.TypeBind, + Source: taskFs.HostPath("/"), + Target: "/alfred", + }, + }, + }, + &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + networkName: { + NetworkID: networkId, + }, + }, + }, + nil, + fmt.Sprintf("alfred-%s-%d", task.FQN(), stepIndex), + ) + if err != nil { + return fmt.Errorf("failed to create step %d container: %w", stepIndex, err) + } + defer cleanup( + "step container", + func() error { + return docker.ContainerRemove(context.Background(), resp.ID, + types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) + }, + ) - out, err := docker.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Timestamps: true, Details: true}) - if err != nil { - return -1, fmt.Errorf("failed to get container logs: %w", err) - } - defer out.Close() + // Start main container + wait, errChan := docker.ContainerWait(ctx, resp.ID, container.WaitConditionNextExit) + err = docker.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) + if err != nil { + return fmt.Errorf("failed to start step %d container: %w", stepIndex, err) + } - _, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, out) + out, err := docker.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Timestamps: true, Details: true}) + if err != nil { + return fmt.Errorf("failed to get container step %d logs: %w", stepIndex, err) + } + defer out.Close() - // Wait for the container to finish - var status container.WaitResponse - select { - case status = <-wait: - // Container is done - case err := <-errChan: - return -1, fmt.Errorf("failed to wait for container: %w", err) + _, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, out) + + // Wait for the container to finish + select { + case status = <-wait: + // Container is done + if status.StatusCode != 0 { + break + } + case err := <-errChan: + return fmt.Errorf("failed to wait for step %d container: %w", stepIndex, err) + } + + return nil + }(i + 1); err != nil { + return -1, err + } } // Preserve artifacts diff --git a/provisioner/openstack/node.go b/provisioner/openstack/node.go index 7b4dcaa..870c259 100644 --- a/provisioner/openstack/node.go +++ b/provisioner/openstack/node.go @@ -143,8 +143,10 @@ func (n *Node) connect(server *servers.Server) (err error) { } func (n *Node) RunTask(task *scheduler.Task, runConfig scheduler.RunTaskConfig) (int, error) { - if err := n.ensureNodeHasImage(task.Job.Image); err != nil { - return -1, fmt.Errorf("node has image: %w", err) + for _, image := range task.Job.Steps { + if err := n.ensureNodeHasImage(image); err != nil { + return -1, fmt.Errorf("failed to ensure node has image '%s': %w", image, err) + } } return internal.RunContainer(context.TODO(), n.log, n.docker, task, n.fs, runConfig)