From 5eb1e4ad0d41c6ee06db34183689a42e02a77fe0 Mon Sep 17 00:00:00 2001 From: gal-legit <99600389+gal-legit@users.noreply.github.com> Date: Sat, 12 Nov 2022 19:51:53 +0200 Subject: [PATCH] feat: add digest to artifacts info of published docker images (#3540) Extract the digest (sha256) of docker images from the `docker push` command for dockers published to the docker registry. Outputting the digest is required to avoid a race condition when referencing the image, where the image tag is being modified before the reference is done. See this [blog post](https://github.com/goreleaser/goreleaser/issues/3496) for more info. This PR fixes https://github.com/goreleaser/goreleaser/issues/3496. Note that the 'publish' pipe now must run before the 'metadata' pipe, so that the information extracted during the 'publish' pipe would appear in the metadata. Previously, the published docker images metadata wasn't printed (because of the order). It made sense because the content of the published image was just a subset of the local one. Now that it is printed to the metadata, it should have a different name to avoid confusion. As I mentioned, it wasn't printed before - so there shouldn't be any backward-compatibility issues. --- Local tests: ``` go test -v . === RUN TestVersion === RUN TestVersion/only_version === RUN TestVersion/version_and_date === RUN TestVersion/version,_date,_built_by === RUN TestVersion/all_empty === RUN TestVersion/complete --- PASS: TestVersion (0.00s) --- PASS: TestVersion/only_version (0.00s) --- PASS: TestVersion/version_and_date (0.00s) --- PASS: TestVersion/version,_date,_built_by (0.00s) --- PASS: TestVersion/all_empty (0.00s) --- PASS: TestVersion/complete (0.00s) PASS ok github.com/goreleaser/goreleaser 0.764s ``` Output example: ``` { "name": "gallegit/hello-world:latest", "path": "gallegit/hello-world:latest", "goos": "linux", "goarch": "amd64", "internal_type": 10, "type": "Published Docker Image", "extra": { "digest": "sha256:c3f7dd196a046dc061236d3c6ae1e2946269e90da30b0a959240ca799750e632" } } ``` Signed-off-by: Carlos A Becker Co-authored-by: Carlos Alexandro Becker --- internal/artifact/artifact.go | 4 +++- internal/pipe/docker/api.go | 30 ++++++++++++++++++++++++++++- internal/pipe/docker/api_docker.go | 19 ++++++++++++++---- internal/pipe/docker/docker.go | 6 +++++- internal/pipe/docker/docker_test.go | 7 +++++++ internal/pipeline/pipeline.go | 4 ++-- 6 files changed, 61 insertions(+), 9 deletions(-) diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index b351339b389..abd707f6122 100644 --- a/internal/artifact/artifact.go +++ b/internal/artifact/artifact.go @@ -86,8 +86,10 @@ func (t Type) String() string { return "Binary" case LinuxPackage: return "Linux Package" - case PublishableDockerImage, DockerImage: + case PublishableDockerImage: return "Docker Image" + case DockerImage: + return "Published Docker Image" case DockerManifest: return "Docker Manifest" case PublishableSnapcraft, Snapcraft: diff --git a/internal/pipe/docker/api.go b/internal/pipe/docker/api.go index ca5f29c8fc2..4a210471f9b 100644 --- a/internal/pipe/docker/api.go +++ b/internal/pipe/docker/api.go @@ -34,7 +34,7 @@ func registerImager(use string, impl imager) { // imager is something that can build and push docker images. type imager interface { Build(ctx *context.Context, root string, images, flags []string) error - Push(ctx *context.Context, image string, flags []string) error + Push(ctx *context.Context, image string, flags []string) (digest string, err error) } // manifester is something that can create and push docker manifests. @@ -66,3 +66,31 @@ func runCommand(ctx *context.Context, dir, binary string, args ...string) error } return nil } + +func runCommandWithOutput(ctx *context.Context, dir, binary string, args ...string) ([]byte, error) { + fields := log.Fields{ + "cmd": append([]string{binary}, args[0]), + "cwd": dir, + } + + /* #nosec */ + cmd := exec.CommandContext(ctx, binary, args...) + cmd.Dir = dir + cmd.Env = ctx.Env.Strings() + + var b bytes.Buffer + w := gio.Safe(&b) + cmd.Stderr = io.MultiWriter(logext.NewWriter(fields, logext.Error), w) + + log.WithFields(fields).WithField("args", args[1:]).Debug("running") + out, err := cmd.Output() + if out != nil { + // regardless of command success, always print stdout for backward-compatibility with runCommand() + _, _ = io.MultiWriter(logext.NewWriter(fields, logext.Error), w).Write(out) + } + if err != nil { + return nil, fmt.Errorf("%w: %s", err, b.String()) + } + + return out, nil +} diff --git a/internal/pipe/docker/api_docker.go b/internal/pipe/docker/api_docker.go index fe791e65a8c..26e4f7a63c1 100644 --- a/internal/pipe/docker/api_docker.go +++ b/internal/pipe/docker/api_docker.go @@ -2,6 +2,7 @@ package docker import ( "fmt" + "regexp" "github.com/goreleaser/goreleaser/pkg/context" ) @@ -43,11 +44,21 @@ type dockerImager struct { buildx bool } -func (i dockerImager) Push(ctx *context.Context, image string, flags []string) error { - if err := runCommand(ctx, ".", "docker", "push", image); err != nil { - return fmt.Errorf("failed to push %s: %w", image, err) +var dockerDigestPattern = regexp.MustCompile("sha256:[a-z0-9]{64}") + +func (i dockerImager) Push(ctx *context.Context, image string, flags []string) (digest string, err error) { + outBytes, err := runCommandWithOutput(ctx, ".", "docker", "push", image) + if err != nil { + return "", fmt.Errorf("failed to push %s: %w", image, err) } - return nil + + out := string(outBytes) + digest = dockerDigestPattern.FindString(out) + if digest == "" { + return "", fmt.Errorf("failed to find docker digest in docker push output: %v", out) + } + + return digest, nil } func (i dockerImager) Build(ctx *context.Context, root string, images, flags []string) error { diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go index 17f39143284..7f868af3bfb 100644 --- a/internal/pipe/docker/docker.go +++ b/internal/pipe/docker/docker.go @@ -20,6 +20,7 @@ import ( const ( dockerConfigExtra = "DockerConfig" + dockerDigestExtra = "digest" useBuildx = "buildx" useDocker = "docker" @@ -248,7 +249,8 @@ func dockerPush(ctx *context.Context, image *artifact.Artifact) error { return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish: " + image.Name) } - if err := imagers[docker.Use].Push(ctx, image.Name, docker.PushFlags); err != nil { + digest, err := imagers[docker.Use].Push(ctx, image.Name, docker.PushFlags) + if err != nil { return err } @@ -264,6 +266,8 @@ func dockerPush(ctx *context.Context, image *artifact.Artifact) error { if docker.ID != "" { art.Extra[artifact.ExtraID] = docker.ID } + art.Extra[dockerDigestExtra] = digest + ctx.Artifacts.Add(art) return nil } diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go index eb5dace1d8c..76b0dcdf896 100644 --- a/internal/pipe/docker/docker_test.go +++ b/internal/pipe/docker/docker_test.go @@ -1091,6 +1091,13 @@ func TestRunPipe(t *testing.T) { // t.Log("removing docker image", img) require.NoError(t, rmi(img), "could not delete image %s", img) } + + _ = ctx.Artifacts.Filter(artifact.ByType(artifact.DockerImage)).Visit(func(a *artifact.Artifact) error { + digest, err := artifact.Extra[string](*a, dockerDigestExtra) + require.NoError(t, err) + require.NotEmpty(t, digest) + return nil + }) }) } } diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 10f61ed4721..78a247b2208 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -111,10 +111,10 @@ var Pipeline = append( chocolatey.Pipe{}, // create and push docker images docker.Pipe{}, - // creates a metadata.json and an artifacts.json files in the dist folder - metadata.Pipe{}, // publishes artifacts publish.Pipe{}, + // creates a metadata.json and an artifacts.json files in the dist folder + metadata.Pipe{}, // announce releases announce.Pipe{}, )