diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c362e7725d..c1d2945decd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,8 @@ on: jobs: goreleaser: runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" steps: - name: Checkout diff --git a/.goreleaser.yml b/.goreleaser.yml index 10cf16535c6..592dc12dca2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -35,12 +35,12 @@ changelog: - go mod tidy dockers: - image_templates: - - 'goreleaser/goreleaser:{{ .Tag }}-cgo' - - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-cgo' - - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo' - - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo' - - 'goreleaser/goreleaser:latest-cgo' - - 'ghcr.io/goreleaser/goreleaser:latest-cgo' + - 'goreleaser/goreleaser:{{ .Tag }}-cgo-amd64' + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-cgo-amd64' + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo-amd64' + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo-amd64' + - 'goreleaser/goreleaser:latest-cgo-amd64' + - 'ghcr.io/goreleaser/goreleaser:latest-cgo-amd64' dockerfile: Dockerfile.cgo binaries: - goreleaser @@ -50,15 +50,61 @@ dockers: - "--label=org.opencontainers.image.name={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + - "--build-arg" + - "ARCH=amd64" + extra_files: + - scripts/entrypoint.sh +- image_templates: + - 'goreleaser/goreleaser:{{ .Tag }}-cgo-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-cgo-arm64v8' + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-cgo-arm64v8' + - 'goreleaser/goreleaser:latest-cgo-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:latest-cgo-arm64v8' + dockerfile: Dockerfile.cgo + binaries: + - goreleaser + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.name={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + - "--build-arg" + - "ARCH=arm64v8" + goarch: arm64 + extra_files: + - scripts/entrypoint.sh +- image_templates: + - 'goreleaser/goreleaser:{{ .Tag }}-amd64' + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64' + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-amd64' + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-amd64' + - 'goreleaser/goreleaser:latest-amd64' + - 'ghcr.io/goreleaser/goreleaser:latest-amd64' + dockerfile: Dockerfile + binaries: + - goreleaser + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.name={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.source={{.GitURL}}" + - "--build-arg" + - "ARCH=amd64" extra_files: - scripts/entrypoint.sh - image_templates: - - 'goreleaser/goreleaser:{{ .Tag }}' - - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}' - - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}' - - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}' - - 'goreleaser/goreleaser:latest' - - 'ghcr.io/goreleaser/goreleaser:latest' + - 'goreleaser/goreleaser:{{ .Tag }}-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64v8' + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-arm64v8' + - 'goreleaser/goreleaser:latest-arm64v8' + - 'ghcr.io/goreleaser/goreleaser:latest-arm64v8' dockerfile: Dockerfile binaries: - goreleaser @@ -69,16 +115,36 @@ dockers: - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.source={{.GitURL}}" - - "--label=com.github.actions.name={{.ProjectName}}" - - "--label=com.github.actions.description=Deliver Go binaries as fast and easily as possible" - - "--label=com.github.actions.icon=terminal" - - "--label=com.github.actions.color=blue" - - "--label=repository=http://github.com/goreleaser/goreleaser" - - "--label=homepage=http://goreleaser.com" - - "--label=maintainer=Carlos Becker " - + - "--build-arg" + - "ARCH=arm64v8" + goarch: arm64 extra_files: - scripts/entrypoint.sh +docker_manifests: +- name_template: 'goreleaser/goreleaser:{{ .Tag }}' + image_templates: + - 'goreleaser/goreleaser:{{ .Tag }}-amd64' + - 'goreleaser/goreleaser:{{ .Tag }}-arm64v8' +- name_template: 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}' + image_templates: + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64' + - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64v8' +- name_template: 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}' + image_templates: + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-amd64' + - 'goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-arm64v8' +- name_template: 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}' + image_templates: + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-amd64' + - 'ghcr.io/goreleaser/goreleaser:v{{ .Major }}.{{ .Minor }}-arm64v8' +- name_template: 'goreleaser/goreleaser:latest' + image_templates: + - 'goreleaser/goreleaser:latest-amd64' + - 'goreleaser/goreleaser:latest-arm64v8' +- name_template: 'ghcr.io/goreleaser/goreleaser:latest' + image_templates: + - 'ghcr.io/goreleaser/goreleaser:latest-amd64' + - 'ghcr.io/goreleaser/goreleaser:latest-arm64v8' archives: - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' replacements: diff --git a/Dockerfile b/Dockerfile index ac98d739652..35ac1a8a5ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM golang:1.15-alpine +ARG ARCH +FROM ${ARCH}/golang:1.15-alpine RUN apk add --no-cache bash \ curl \ diff --git a/Dockerfile.cgo b/Dockerfile.cgo index 6d1ae7ab9ef..0a282b14ecd 100644 --- a/Dockerfile.cgo +++ b/Dockerfile.cgo @@ -1,4 +1,5 @@ -FROM golang:1.15-alpine +ARG ARCH +FROM ${ARCH}/golang:1.15-alpine RUN apk add --no-cache bash \ build-base \ diff --git a/internal/pipe/docker/doc.go b/internal/pipe/docker/doc.go new file mode 100644 index 00000000000..4e8963a2b77 --- /dev/null +++ b/internal/pipe/docker/doc.go @@ -0,0 +1,3 @@ +// Package docker provides a Pipe that creates and pushes Docker images and +// manifests. +package docker diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go index d1eec2c88f7..7fcae54afe9 100644 --- a/internal/pipe/docker/docker.go +++ b/internal/pipe/docker/docker.go @@ -1,4 +1,3 @@ -// Package docker provides a Pipe that creates and pushes a Docker image package docker import ( diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go index ee48fd4120e..40e5ede2ca8 100644 --- a/internal/pipe/docker/docker_test.go +++ b/internal/pipe/docker/docker_test.go @@ -60,6 +60,7 @@ func killAndRm(t *testing.T) { _ = exec.Command("docker", "rm", "alt_registry").Run() } +// TODO: this test is too big... split in smaller tests? Mainly the manifest ones... func TestRunPipe(t *testing.T) { type errChecker func(*testing.T, error) var shouldErr = func(msg string) errChecker { @@ -89,13 +90,168 @@ func TestRunPipe(t *testing.T) { var noLabels = func(t *testing.T, count int) {} var table = map[string]struct { - dockers []config.Docker - env map[string]string - expect []string - assertImageLabels imageLabelFinder - assertError errChecker - pubAssertError errChecker + dockers []config.Docker + manifests []config.DockerManifest + env map[string]string + expect []string + assertImageLabels imageLabelFinder + assertError errChecker + pubAssertError errChecker + manifestAssertError errChecker }{ + "multiarch": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch:test-amd64"}, + Goos: "linux", + Goarch: "amd64", + Dockerfile: "testdata/Dockerfile.arch", + Binaries: []string{"mybin"}, + BuildFlagTemplates: []string{"--build-arg", "ARCH=amd64"}, + }, + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch:test-arm64v8"}, + Goos: "linux", + Goarch: "arm64", + Dockerfile: "testdata/Dockerfile.arch", + Binaries: []string{"mybin"}, + BuildFlagTemplates: []string{"--build-arg", "ARCH=arm64v8"}, + }, + }, + manifests: []config.DockerManifest{ + { + // XXX: fails if :latest https://github.com/docker/distribution/issues/3100 + NameTemplate: registry + "goreleaser/test_multiarch:test", + ImageTemplates: []string{ + registry + "goreleaser/test_multiarch:test-amd64", + registry + "goreleaser/test_multiarch:test-arm64v8", + }, + CreateFlags: []string{"--insecure"}, + PushFlags: []string{"--insecure"}, + }, + }, + expect: []string{ + registry + "goreleaser/test_multiarch:test-amd64", + registry + "goreleaser/test_multiarch:test-arm64v8", + }, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, + assertImageLabels: noLabels, + }, + "multiarch image not found": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch_fail:latest-arm64v8"}, + Goos: "linux", + Goarch: "arm64", + Dockerfile: "testdata/Dockerfile.arch", + Binaries: []string{"mybin"}, + BuildFlagTemplates: []string{"--build-arg", "ARCH=arm64v8"}, + }, + }, + manifests: []config.DockerManifest{ + { + NameTemplate: registry + "goreleaser/test_multiarch_fail:test", + ImageTemplates: []string{registry + "goreleaser/test_multiarch_fail:latest-amd64"}, + CreateFlags: []string{"--insecure"}, + PushFlags: []string{"--insecure"}, + }, + }, + expect: []string{registry + "goreleaser/test_multiarch_fail:latest-arm64v8"}, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldErr("failed to create docker manifest: localhost:5000/goreleaser/test_multiarch_fail:test"), + assertImageLabels: noLabels, + }, + "multiarch manifest template error": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch_manifest_tmpl_error"}, + Goos: "linux", + Goarch: "arm64", + Dockerfile: "testdata/Dockerfile", + Binaries: []string{"mybin"}, + }, + }, + manifests: []config.DockerManifest{ + { + NameTemplate: registry + "goreleaser/test_multiarch_manifest_tmpl_error:{{ .Goos }", + ImageTemplates: []string{registry + "goreleaser/test_multiarch_manifest_tmpl_error"}, + }, + }, + expect: []string{registry + "goreleaser/test_multiarch_manifest_tmpl_error"}, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`), + assertImageLabels: noLabels, + }, + "multiarch image template error": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch_img_tmpl_error"}, + Goos: "linux", + Goarch: "arm64", + Dockerfile: "testdata/Dockerfile", + Binaries: []string{"mybin"}, + }, + }, + manifests: []config.DockerManifest{ + { + NameTemplate: registry + "goreleaser/test_multiarch_img_tmpl_error", + ImageTemplates: []string{registry + "goreleaser/test_multiarch_img_tmpl_error:{{ .Goos }"}, + }, + }, + expect: []string{registry + "goreleaser/test_multiarch_img_tmpl_error"}, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldErr(`template: tmpl:1: unexpected "}" in operand`), + assertImageLabels: noLabels, + }, + "multiarch missing manifest name": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch_no_mainifest_name"}, + Goos: "linux", + Goarch: "arm64", + Dockerfile: "testdata/Dockerfile", + Binaries: []string{"mybin"}, + }, + }, + manifests: []config.DockerManifest{ + { + NameTemplate: " ", + ImageTemplates: []string{registry + "goreleaser/test_multiarch_no_mainifest_name"}, + }, + }, + expect: []string{registry + "goreleaser/test_multiarch_no_mainifest_name"}, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: testlib.AssertSkipped, + assertImageLabels: noLabels, + }, + "multiarch missing images": { + dockers: []config.Docker{ + { + ImageTemplates: []string{registry + "goreleaser/test_multiarch_no_mainifest_images"}, + Dockerfile: "testdata/Dockerfile", + Goos: "linux", + Goarch: "arm64", + Binaries: []string{"mybin"}, + }, + }, + manifests: []config.DockerManifest{ + { + NameTemplate: "ignored", + ImageTemplates: []string{" ", " ", ""}, + }, + }, + expect: []string{registry + "goreleaser/test_multiarch_no_mainifest_images"}, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: testlib.AssertSkipped, + assertImageLabels: noLabels, + }, "valid": { env: map[string]string{ "FOO": "123", @@ -149,8 +305,9 @@ func TestRunPipe(t *testing.T) { "label=org.label-schema.vcs-ref=a1b2c3d4", "label=org.label-schema.name=mybin", ), - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "valid-with-builds": { dockers: []config.Docker{ @@ -168,9 +325,10 @@ func TestRunPipe(t *testing.T) { expect: []string{ registry + "goreleaser/test_run_pipe_build:latest", }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "multiple images with same extra file": { dockers: []config.Docker{ @@ -199,9 +357,10 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/multiplefiles1:latest", registry + "goreleaser/multiplefiles2:latest", }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "multiple images with same dockerfile": { dockers: []config.Docker{ @@ -229,8 +388,9 @@ func TestRunPipe(t *testing.T) { registry + "goreleaser/test_run_pipe:latest", registry + "goreleaser/test_run_pipe2:latest", }, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "valid_skip_push": { dockers: []config.Docker{ @@ -295,9 +455,10 @@ func TestRunPipe(t *testing.T) { expect: []string{ registry + "goreleaser/test_run_pipe:1.0.0", }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "valid build args": { dockers: []config.Docker{ @@ -317,9 +478,10 @@ func TestRunPipe(t *testing.T) { expect: []string{ registry + "goreleaser/test_build_args:latest", }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, }, "bad build args": { dockers: []config.Docker{ @@ -457,9 +619,10 @@ func TestRunPipe(t *testing.T) { expect: []string{ "docker.io/nope:latest", }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldErr(`requested access to the resource is denied`), + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldErr(`requested access to the resource is denied`), + manifestAssertError: shouldNotErr, }, "dockerfile_doesnt_exist": { dockers: []config.Docker{ @@ -513,9 +676,10 @@ func TestRunPipe(t *testing.T) { Dockerfile: "testdata/Dockerfile.multiple", }, }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, expect: []string{ registry + "goreleaser/multiple:latest", }, @@ -534,9 +698,10 @@ func TestRunPipe(t *testing.T) { Dockerfile: "testdata/Dockerfile", }, }, - assertImageLabels: noLabels, - assertError: shouldNotErr, - pubAssertError: shouldNotErr, + assertImageLabels: noLabels, + assertError: shouldNotErr, + pubAssertError: shouldNotErr, + manifestAssertError: shouldNotErr, expect: []string{ registry + "goreleaser/templatedbins:latest", }, @@ -575,9 +740,10 @@ func TestRunPipe(t *testing.T) { require.NoError(tt, err) var ctx = context.New(config.Project{ - ProjectName: "mybin", - Dist: dist, - Dockers: docker.dockers, + ProjectName: "mybin", + Dist: dist, + Dockers: docker.dockers, + DockerManifests: docker.manifests, }) ctx.Parallelism = 1 ctx.Env = docker.env @@ -592,7 +758,7 @@ func TestRunPipe(t *testing.T) { Patch: 0, } for _, os := range []string{"linux", "darwin"} { - for _, arch := range []string{"amd64", "386"} { + for _, arch := range []string{"amd64", "386", "arm64"} { for _, bin := range []string{"mybin", "anotherbin"} { ctx.Artifacts.Add(&artifact.Artifact{ Name: bin, @@ -618,6 +784,7 @@ func TestRunPipe(t *testing.T) { docker.assertError(tt, err) if err == nil { docker.pubAssertError(tt, Pipe{}.Publish(ctx)) + docker.manifestAssertError(tt, ManifestPipe{}.Publish(ctx)) } for _, d := range docker.dockers { diff --git a/internal/pipe/docker/manifest.go b/internal/pipe/docker/manifest.go new file mode 100644 index 00000000000..6eb4da572ea --- /dev/null +++ b/internal/pipe/docker/manifest.go @@ -0,0 +1,107 @@ +package docker + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/apex/log" + "github.com/goreleaser/goreleaser/internal/pipe" + "github.com/goreleaser/goreleaser/internal/semerrgroup" + "github.com/goreleaser/goreleaser/internal/tmpl" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/goreleaser/goreleaser/pkg/context" +) + +// ManifestPipe is beta implementation of for the docker manifest feature, +// allowing to publish multi-arch docker images. +type ManifestPipe struct{} + +func (ManifestPipe) String() string { + return "docker manifests" +} + +// Publish the docker manifests. +func (ManifestPipe) Publish(ctx *context.Context) error { + if ctx.SkipPublish { + return pipe.ErrSkipPublishEnabled + } + var g = semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism)) + for _, manifest := range ctx.Config.DockerManifests { + manifest := manifest + g.Go(func() error { + name, err := manifestName(ctx, manifest) + if err != nil { + return err + } + images, err := manifestImages(ctx, manifest) + if err != nil { + return err + } + if err := dockerManifestCreate(ctx, name, images, manifest.CreateFlags); err != nil { + return err + } + return dockerManifestPush(ctx, name, manifest.PushFlags) + }) + } + return g.Wait() +} + +func manifestName(ctx *context.Context, manifest config.DockerManifest) (string, error) { + name, err := tmpl.New(ctx).Apply(manifest.NameTemplate) + if err != nil { + return name, err + } + if strings.TrimSpace(name) == "" { + return name, pipe.Skip("manifest name is empty") + } + return name, nil +} + +func manifestImages(ctx *context.Context, manifest config.DockerManifest) ([]string, error) { + var imgs []string + for _, img := range manifest.ImageTemplates { + str, err := tmpl.New(ctx).Apply(img) + if err != nil { + return []string{}, err + } + imgs = append(imgs, str) + } + if strings.TrimSpace(strings.Join(manifest.ImageTemplates, "")) == "" { + return imgs, pipe.Skip("manifest has no images") + } + return imgs, nil +} + +func dockerManifestCreate(ctx *context.Context, manifest string, images, flags []string) error { + log.WithField("manifest", manifest).Info("creating docker manifest") + var args = []string{"manifest", "create", manifest} + for _, img := range images { + args = append(args, "--amend", img) + } + args = append(args, flags...) + /* #nosec */ + var cmd = exec.CommandContext(ctx, "docker", args...) + log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to create docker manifest: %s: \n%s: %w", manifest, string(out), err) + } + log.Debugf("docker manifest output: \n%s", string(out)) + return nil +} + +func dockerManifestPush(ctx *context.Context, manifest string, flags []string) error { + log.WithField("manifest", manifest).Info("pushing docker manifest") + var args = []string{"manifest", "push", manifest} + args = append(args, flags...) + /* #nosec */ + var cmd = exec.CommandContext(ctx, "docker", args...) + log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to push docker manifest: %s: \n%s: %w", manifest, string(out), err) + } + log.Debugf("docker manifest output: \n%s", string(out)) + return nil +} diff --git a/internal/pipe/docker/testdata/Dockerfile.arch b/internal/pipe/docker/testdata/Dockerfile.arch new file mode 100644 index 00000000000..588858ed86c --- /dev/null +++ b/internal/pipe/docker/testdata/Dockerfile.arch @@ -0,0 +1,3 @@ +ARG ARCH +FROM ${ARCH}/alpine +ADD mybin / diff --git a/internal/pipe/publish/publish.go b/internal/pipe/publish/publish.go index b8cfb25c194..223e4ef10f3 100644 --- a/internal/pipe/publish/publish.go +++ b/internal/pipe/publish/publish.go @@ -40,6 +40,7 @@ var publishers = []Publisher{ custompublishers.Pipe{}, artifactory.Pipe{}, docker.Pipe{}, + docker.ManifestPipe{}, snapcraft.Pipe{}, // This should be one of the last steps release.Pipe{}, diff --git a/pkg/config/config.go b/pkg/config/config.go index c5eca187e84..851721ccfab 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -490,6 +490,14 @@ type Docker struct { BuildFlagTemplates []string `yaml:"build_flag_templates,omitempty"` } +// DockerManifest config. +type DockerManifest struct { + NameTemplate string `yaml:"name_template,omitempty"` + ImageTemplates []string `yaml:"image_templates,omitempty"` + CreateFlags []string `yaml:"create_flags,omitempty"` + PushFlags []string `yaml:"push_flags,omitempty"` +} + // Filters config. type Filters struct { Exclude []string `yaml:",omitempty"` @@ -563,29 +571,30 @@ type Source struct { // Project includes all project configuration. type Project struct { - ProjectName string `yaml:"project_name,omitempty"` - Env []string `yaml:",omitempty"` - Release Release `yaml:",omitempty"` - Milestones []Milestone `yaml:",omitempty"` - Brews []Homebrew `yaml:",omitempty"` - Scoop Scoop `yaml:",omitempty"` - Builds []Build `yaml:",omitempty"` - Archives []Archive `yaml:",omitempty"` - NFPMs []NFPM `yaml:"nfpms,omitempty"` - Snapcrafts []Snapcraft `yaml:",omitempty"` - Snapshot Snapshot `yaml:",omitempty"` - Checksum Checksum `yaml:",omitempty"` - Dockers []Docker `yaml:",omitempty"` - Artifactories []Upload `yaml:",omitempty"` - Uploads []Upload `yaml:",omitempty"` - Blobs []Blob `yaml:"blobs,omitempty"` - Publishers []Publisher `yaml:"publishers,omitempty"` - Changelog Changelog `yaml:",omitempty"` - Dist string `yaml:",omitempty"` - Signs []Sign `yaml:",omitempty"` - EnvFiles EnvFiles `yaml:"env_files,omitempty"` - Before Before `yaml:",omitempty"` - Source Source `yaml:",omitempty"` + ProjectName string `yaml:"project_name,omitempty"` + Env []string `yaml:",omitempty"` + Release Release `yaml:",omitempty"` + Milestones []Milestone `yaml:",omitempty"` + Brews []Homebrew `yaml:",omitempty"` + Scoop Scoop `yaml:",omitempty"` + Builds []Build `yaml:",omitempty"` + Archives []Archive `yaml:",omitempty"` + NFPMs []NFPM `yaml:"nfpms,omitempty"` + Snapcrafts []Snapcraft `yaml:",omitempty"` + Snapshot Snapshot `yaml:",omitempty"` + Checksum Checksum `yaml:",omitempty"` + Dockers []Docker `yaml:",omitempty"` + DockerManifests []DockerManifest `yaml:"docker_manifests,omitempty"` + Artifactories []Upload `yaml:",omitempty"` + Uploads []Upload `yaml:",omitempty"` + Blobs []Blob `yaml:"blobs,omitempty"` + Publishers []Publisher `yaml:"publishers,omitempty"` + Changelog Changelog `yaml:",omitempty"` + Dist string `yaml:",omitempty"` + Signs []Sign `yaml:",omitempty"` + EnvFiles EnvFiles `yaml:"env_files,omitempty"` + Before Before `yaml:",omitempty"` + Source Source `yaml:",omitempty"` // this is a hack ¯\_(ツ)_/¯ SingleBuild Build `yaml:"build,omitempty"` diff --git a/www/docs/customization/docker_manifest.md b/www/docs/customization/docker_manifest.md new file mode 100644 index 00000000000..b35f4cbb27d --- /dev/null +++ b/www/docs/customization/docker_manifest.md @@ -0,0 +1,119 @@ +--- +title: Docker Manifest +--- + +Since [v0.148.0](https://github.com/goreleaser/goreleaser/releases/tag/v0.148.0), +GoReleaser supports building and pushing Docker multi-platform images through +the `docker manifest` tool. + +For it to work, it [has to be enabled in the client configurations](https://github.com/docker/cli/blob/master/experimental/README.md). + +Please make sure `docker manifest` works before opening issues. + +!!! warning + Please note that this is a beta feature, and it may change or be removed + at any time. + +## Customization + +You can create several manifests in a single GoReleaser run, here are all the +options available: + +```yaml +# .goreleaser.yml +docker_manifests: + # You can have multiple Docker manifests. +- + # Name template for the manifest. + # Defaults to empty. + name_template: foo/bar:{{ .Version }} + + # Image name templates to be added to this manifest. + # Defaults to empty. + image_templates: + - foo/bar:{{ .Version }}-amd64 + - foo/bar:{{ .Version }}-arm64v8 + + # Extra flags to be passed down to the manifest create command. + # Defaults to empty. + create_flags: + - --insecure + + # Extra flags to be passed down to the manifest push command. + # Defaults to empty. + push_flags: + - --insecure +``` + +!!! tip + Learn more about the [name template engine](/customization/templates/). + +## How it works + +We basically build and push our images as usual, but we also add a new +section to our config defining which images are part of which manifests. + +GoReleaser will create and publish the manifest in its publish phase. + +!!! warning + Unfortunately, the manifest tool needs the images to be pushed to create + the manifest, that's why we both create and push it in the publish phase. + +## Example config + +In this example we will use Docker's `--build-arg` passing an `ARCH` argument. +This way we can use the same `Dockerfile` for both the `amd64` and the `arm64` +images: + +```dockerfile +# Dockerfile +ARG ARCH +FROM ${ARCH}/alpine +COPY mybin /usr/bin/mybin +ENTRYPOINT ["/usr/bin/mybin"] +``` + +Then, on our GoReleaser config file, we need to define both the `dockers` and +the `docker_manifests` section: + +```yaml +# goreleaser.yml +builds: +- env: + - CGO_ENABLED=0 + binary: mybin + goos: + - linux + goarch: + - amd64 + - arm64 +dockers: +- image_templates: + - "foo/bar:{{ .Version }}-amd64" + binaries: + - mybin + dockerfile: Dockerfile + build_flag_templates: + - "--build-arg" + - "ARCH=amd64" +- image_templates: + - "foo/bar:{{ .Version }}-arm64v8" + binaries: + - mybin + goarch: arm64 + dockerfile: Dockerfile + build_flag_templates: + - "--build-arg" + - "ARCH=arm64v8" +docker_manifests: +- name_template: foo/bar:{{ .Version }} + image_templates: + - foo/bar:{{ .Version }}-amd64 + - foo/bar:{{ .Version }}-arm64v8 +``` + +!!! warning + Notice that `ARCH` needs to be in the Docker arch format, not Go's. + +That config will build the 2 Docker images defined, as well as the manifest, +and push everything to Docker Hub. diff --git a/www/mkdocs.yml b/www/mkdocs.yml index 7f4e844746d..1fcdf167c3f 100644 --- a/www/mkdocs.yml +++ b/www/mkdocs.yml @@ -66,6 +66,7 @@ nav: - customization/checksum.md - customization/publishers.md - customization/docker.md + - customization/docker_manifest.md - customization/env.md - customization/hooks.md - customization/homebrew.md