From 29f6a2e5be3c8e18d0835819c20a8cd45240d9da Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Sat, 11 Sep 2021 00:45:29 +0200 Subject: [PATCH] feat(docker): Add support for multi-platform build --- go.mod | 1 + pb/api.proto | 1 + server/core/job.go | 1 + server/core/repo.go | 1 + server/scheduler/scheduler.go | 1 + server/store/build/build.go | 50 ++++++++++++++++++----------------- worker/app/server.go | 4 +-- worker/docker/docker.go | 9 ++++--- worker/docker/image.go | 6 +++-- worker/docker/platform.go | 29 ++++++++++++++++++++ 10 files changed, 71 insertions(+), 32 deletions(-) create mode 100644 worker/docker/platform.go diff --git a/go.mod b/go.mod index 96d40821..9d7b9356 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/narqo/go-badge v0.0.0-20190124110329-d9415e4e1e9f + github.com/opencontainers/image-spec v1.0.1 github.com/shirou/gopsutil v3.20.10+incompatible github.com/spf13/cobra v1.1.1 github.com/spf13/viper v1.7.0 diff --git a/pb/api.proto b/pb/api.proto index 0922bca1..40874d88 100644 --- a/pb/api.proto +++ b/pb/api.proto @@ -69,6 +69,7 @@ message Job { string sshPrivateKey = 21; bool sshClone = 22; string branch = 23; + string platform = 24; } message Command { diff --git a/server/core/job.go b/server/core/job.go index d6f5c593..49231c21 100644 --- a/server/core/job.go +++ b/server/core/job.go @@ -10,6 +10,7 @@ type ( Image string `json:"image"` Env string `json:"env"` Mount string `json:"mount"` + Platform string `json:"platform"` StartTime *time.Time `json:"startTime"` EndTime *time.Time `json:"endTime"` Status string `gorm:"not null;size:20;default:'queued'" json:"status"` // queued | running | passing | failing diff --git a/server/core/repo.go b/server/core/repo.go index 343c5717..0003e688 100644 --- a/server/core/repo.go +++ b/server/core/repo.go @@ -35,6 +35,7 @@ type ( Provider Provider `json:"-"` EnvVariables []EnvVariable `json:"-"` Mounts []*Mount `json:"mounts"` + Platforms string `json:"platforms"` Perms Perms `json:"perms"` Timestamp } diff --git a/server/scheduler/scheduler.go b/server/scheduler/scheduler.go index f2eb1065..728d8a8a 100644 --- a/server/scheduler/scheduler.go +++ b/server/scheduler/scheduler.go @@ -333,6 +333,7 @@ func (s *scheduler) startJob(job *core.Job, worker *core.Worker) { Mount: strings.Split(job.Mount, ","), SshPrivateKey: job.Build.Repository.SSHPrivateKey, SshClone: job.Build.Repository.UseSSH, + Platform: job.Platform, } s.mu.Lock() diff --git a/server/store/build/build.go b/server/store/build/build.go index 30f5e27f..e2ee1ba7 100644 --- a/server/store/build/build.go +++ b/server/store/build/build.go @@ -318,31 +318,33 @@ func (s buildStore) TriggerBuild(opts core.TriggerBuildOpts) ([]*core.Job, error } var jobs []*core.Job - for _, j := range pjobs { - commands, err := protojson.Marshal(j.Commands) - if err != nil { - return nil, err - } - - job := &core.Job{ - Image: j.Image, - Commands: string(commands), - Env: strings.Join(j.Env, " "), - Mount: strings.Join(mnts, ","), - Stage: j.Stage, - BuildID: build.ID, - Cache: strings.Join(j.Cache, ","), - } - if err := s.jobs.Create(job); err != nil { - return nil, err - } - job, err = s.jobs.Find(job.ID) - if err != nil { - return nil, err + for platform := range strings.Split(repo.Platforms, ";") { + for _, j := range pjobs { + commands, err := protojson.Marshal(j.Commands) + if err != nil { + return nil, err + } + + job := &core.Job{ + Image: j.Image, + Commands: string(commands), + Env: strings.Join(j.Env, " "), + Mount: strings.Join(mnts, ","), + Stage: j.Stage, + BuildID: build.ID, + Cache: strings.Join(j.Cache, ","), + Platform: strings.Split(repo.Platforms, ";")[platform], + } + if err := s.jobs.Create(job); err != nil { + return nil, err + } + job, err = s.jobs.Find(job.ID) + if err != nil { + return nil, err + } + + jobs = append(jobs, job) } - - jobs = append(jobs, job) } - return jobs, nil } diff --git a/worker/app/server.go b/worker/app/server.go index 3e2270bd..0522813c 100644 --- a/worker/app/server.go +++ b/worker/app/server.go @@ -233,8 +233,8 @@ func (s *Server) StartJob(job *pb.Job, stream pb.API_StartJobServer) error { } logch <- []byte(yellow("done\r\n")) - logch <- []byte(yellow(fmt.Sprintf("==> Pulling image %s... ", image))) - if err := docker.PullImage(image, s.config.Registry); err != nil { + logch <- []byte(yellow(fmt.Sprintf("==> Pulling image %s (%s)... ", image, job.Platform))) + if err := docker.PullImage(image, job.Platform, s.config.Registry); err != nil { logch <- []byte(fmt.Sprintf("%s\r\n", err.Error())) } else { logch <- []byte(yellow("done\r\n")) diff --git a/worker/docker/docker.go b/worker/docker/docker.go index a1742139..fffeda08 100644 --- a/worker/docker/docker.go +++ b/worker/docker/docker.go @@ -29,14 +29,14 @@ func RunContainer(name, image string, job *api.Job, config *config.Config, env [ defer close(logch) var shell string - resp, err := createContainer(cli, name, image, dir, []string{"/bin/bash"}, env, job.GetMount()) + resp, err := createContainer(cli, name, image, dir, []string{"/bin/bash"}, env, job.GetMount(), job.Platform) if err != nil { logch <- []byte(fmt.Sprintf("%s\r\n", err.Error())) return err } if !isContainerRunning(cli, resp.ID) { if err := startContainer(cli, resp.ID); err != nil { - resp, err = createContainer(cli, name, image, dir, []string{"/bin/sh"}, env, job.GetMount()) + resp, err = createContainer(cli, name, image, dir, []string{"/bin/sh"}, env, job.GetMount(), job.Platform) if err != nil { logch <- []byte(fmt.Sprintf("%s\r\n", err.Error())) return err @@ -244,7 +244,7 @@ func startContainer(cli *client.Client, id string) error { } // CreateContainer creates new Docker container. -func createContainer(cli *client.Client, name, image, dir string, cmd []string, env []string, mountdir []string) (container.ContainerCreateCreatedBody, error) { +func createContainer(cli *client.Client, name, image, dir string, cmd []string, env []string, mountdir []string, platform string) (container.ContainerCreateCreatedBody, error) { if id, exists := ContainerExists(name); exists { if err := cli.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true}); err != nil { return container.ContainerCreateCreatedBody{}, err @@ -266,6 +266,7 @@ func createContainer(cli *client.Client, name, image, dir string, cmd []string, }) } + p := getPlatform(platform) return cli.ContainerCreate(context.Background(), &container.Config{ Image: image, Cmd: cmd, @@ -274,7 +275,7 @@ func createContainer(cli *client.Client, name, image, dir string, cmd []string, WorkingDir: "/build", }, &container.HostConfig{ Mounts: mounts, - }, nil, nil, name) + }, nil, &p, name) } // IsContainerRunning returns true if container is running. diff --git a/worker/docker/image.go b/worker/docker/image.go index 9ae82639..0fc60c1e 100644 --- a/worker/docker/image.go +++ b/worker/docker/image.go @@ -84,14 +84,16 @@ func PushImage(tag string) (io.ReadCloser, error) { } // PullImage pulls image from the registry. -func PullImage(image string, config *config.Registry) error { +func PullImage(image string, platform string, config *config.Registry) error { ctx := context.Background() cli, err := client.NewClientWithOpts() if err != nil { panic(err) } - opts := types.ImagePullOptions{} + opts := types.ImagePullOptions{ + Platform: platform, + } if cfg.Username != "" && cfg.Password != "" { authConfig := types.AuthConfig{Username: cfg.Username, Password: cfg.Password} diff --git a/worker/docker/platform.go b/worker/docker/platform.go new file mode 100644 index 00000000..85f5b8ad --- /dev/null +++ b/worker/docker/platform.go @@ -0,0 +1,29 @@ +package docker + +import ( + "runtime" + "strings" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func getPlatform(platform string) v1.Platform { + s := strings.Split(platform, "/") + if len(s) == 2 { + return v1.Platform{ + OS: s[0], + Architecture: s[1], + } + } + if len(s) == 3 { + return v1.Platform{ + OS: s[0], + Architecture: s[1], + Variant: s[2], + } + } + return v1.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } +}