From c00995463996466bcd5521e1c1625a3a11a4b1a1 Mon Sep 17 00:00:00 2001 From: Valerian Roche Date: Fri, 29 Dec 2023 13:48:31 -0500 Subject: [PATCH] Fix multi-arch build. Merge code with docker --- internal/containers/context.go | 162 +++++++++++++++++ internal/containers/context_test.go | 51 ++++++ internal/containers/platform.go | 62 +++++++ internal/gio/link.go | 18 ++ internal/pipe/container/builder/api.go | 12 +- internal/pipe/container/builder/buildkit.go | 31 ++-- internal/pipe/container/container.go | 188 +++----------------- internal/pipe/docker/docker.go | 154 +++------------- internal/pipe/docker/docker_test.go | 44 ----- internal/pipeline/pipeline.go | 1 + 10 files changed, 357 insertions(+), 366 deletions(-) create mode 100644 internal/containers/context.go create mode 100644 internal/containers/context_test.go create mode 100644 internal/containers/platform.go create mode 100644 internal/gio/link.go diff --git a/internal/containers/context.go b/internal/containers/context.go new file mode 100644 index 00000000000..106788efd0f --- /dev/null +++ b/internal/containers/context.go @@ -0,0 +1,162 @@ +package containers + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/caarlos0/log" + "github.com/goreleaser/goreleaser/internal/gio" + "github.com/goreleaser/goreleaser/internal/pipe" + "github.com/goreleaser/goreleaser/internal/tmpl" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/goreleaser/goreleaser/pkg/context" +) + +type ImageBuildContext struct { + ID string + BuildPath string + BuildFlags []string + PushFlags []string + Platforms []config.ContainerPlatform + Images []string +} + +func (p ImageBuildContext) Log() *log.Entry { + if len(p.Images) > 0 { + return log.WithField("image", p.Images[0]) + } else { + return log.WithField("id", p.ID) + } +} + +func LogEntry(ctx *context.Context, config config.Container) *log.Entry { + images, _ := processImageTemplates(ctx, config.ImageTemplates) + if len(images) > 0 { + return log.WithField("image", images[0]) + } else { + return log.WithField("id", config.ID) + } +} + +func BuildContext(ctx *context.Context, id string, imageDef config.ImageDefinition, platforms []config.ContainerPlatform) (ImageBuildContext, func(), error) { + context := ImageBuildContext{ + ID: id, + Platforms: platforms, + } + + images, err := processImageTemplates(ctx, imageDef.ImageTemplates) + if err != nil { + return context, nil, err + } + + if len(images) == 0 { + return context, nil, pipe.Skip("no image templates found") + } + context.Images = images + + tmp, err := os.MkdirTemp("", "goreleaserdocker") + if err != nil { + return context, nil, fmt.Errorf("failed to create temporary dir: %w", err) + } + context.BuildPath = tmp + + keepContext := false + defer func() { + if !keepContext { + os.RemoveAll(tmp) + } + }() + + log := log.WithField("image", images[0]) + log.Debug("tempdir: " + tmp) + + // This will set all binaries for all architectures within the context + // To ensure multi-arch builds can access the correct binaries, they are copied to $tmp/$TARGETPLATFORM/$name + artifacts := getApplicableArtifacts(ctx, imageDef, platforms) + if len(artifacts.List()) == 0 { + log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work") + } + log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts") + + if err := tmpl.New(ctx).ApplyAll( + &imageDef.Dockerfile, + ); err != nil { + return context, nil, err + } + if err := gio.Copy( + imageDef.Dockerfile, + filepath.Join(tmp, "Dockerfile"), + ); err != nil { + return context, nil, fmt.Errorf("failed to copy dockerfile: %w", err) + } + + for _, file := range imageDef.Files { + if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil { + return context, nil, fmt.Errorf("failed to copy extra file '%s': %w", file, err) + } + if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil { + return context, nil, fmt.Errorf("failed to copy extra file '%s': %w", file, err) + } + } + + for _, art := range artifacts.List() { + target := filepath.Join(tmp, art.Goos, art.Goarch, art.Name) + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return context, nil, fmt.Errorf("failed to make dir for artifact: %w", err) + } + + if err := gio.Copy(art.Path, target); err != nil { + return context, nil, fmt.Errorf("failed to copy artifact: %w", err) + } + + if len(context.Platforms) == 1 { + if err := gio.Link(target, filepath.Join(tmp, art.Name)); err != nil { + return context, nil, fmt.Errorf("failed to link artifact: %w", err) + } + } + } + + buildFlags, err := processBuildFlagTemplates(ctx, imageDef.BuildFlagTemplates) + if err != nil { + return context, nil, err + } + context.BuildFlags = buildFlags + context.PushFlags = imageDef.PushFlags + + keepContext = true + return context, func() { + os.RemoveAll(tmp) + }, nil +} + +func processImageTemplates(ctx *context.Context, templates []string) ([]string, error) { + // nolint:prealloc + var images []string + for _, imageTemplate := range templates { + image, err := tmpl.New(ctx).Apply(imageTemplate) + if err != nil { + return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err) + } + if image == "" { + continue + } + + images = append(images, image) + } + + return images, nil +} + +func processBuildFlagTemplates(ctx *context.Context, flagTemplates []string) ([]string, error) { + // nolint:prealloc + var buildFlags []string + for _, buildFlagTemplate := range flagTemplates { + buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate) + if err != nil { + return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err) + } + buildFlags = append(buildFlags, buildFlag) + } + return buildFlags, nil +} diff --git a/internal/containers/context_test.go b/internal/containers/context_test.go new file mode 100644 index 00000000000..7be1486534b --- /dev/null +++ b/internal/containers/context_test.go @@ -0,0 +1,51 @@ +package containers + +import ( + "testing" + + "github.com/goreleaser/goreleaser/internal/testctx" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/stretchr/testify/require" +) + +func Test_processImageTemplates(t *testing.T) { + ctx := testctx.NewWithCfg( + config.Project{ + Builds: []config.Build{ + { + ID: "default", + }, + }, + Dockers: []config.Docker{ + { + ImageDefinition: config.ImageDefinition{ + Dockerfile: "Dockerfile.foo", + ImageTemplates: []string{ + "user/image:{{.Tag}}", + "gcr.io/image:{{.Tag}}-{{.Env.FOO}}", + "gcr.io/image:v{{.Major}}.{{.Minor}}", + }, + }, + SkipPush: "true", + }, + }, + Env: []string{"FOO=123"}, + }, + testctx.WithVersion("1.0.0"), + testctx.WithCurrentTag("v1.0.0"), + testctx.WithCommit("a1b2c3d4"), + testctx.WithSemver(1, 0, 0, ""), + ) + require.Len(t, ctx.Config.Dockers, 1) + + docker := ctx.Config.Dockers[0] + require.Equal(t, "Dockerfile.foo", docker.Dockerfile) + + images, err := processImageTemplates(ctx, docker.ImageTemplates) + require.NoError(t, err) + require.Equal(t, []string{ + "user/image:v1.0.0", + "gcr.io/image:v1.0.0-123", + "gcr.io/image:v1.0", + }, images) +} diff --git a/internal/containers/platform.go b/internal/containers/platform.go new file mode 100644 index 00000000000..fd4280017af --- /dev/null +++ b/internal/containers/platform.go @@ -0,0 +1,62 @@ +package containers + +import ( + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/pkg/config" + "github.com/goreleaser/goreleaser/pkg/context" +) + +func DefaultPlatforms(platforms []config.ContainerPlatform) []config.ContainerPlatform { + if len(platforms) == 0 { + platforms = make([]config.ContainerPlatform, 1) + } + for i := range platforms { + DefaultPlatform(&platforms[i]) + } + return platforms +} + +func DefaultPlatform(platform *config.ContainerPlatform) { + if platform.Goos == "" { + platform.Goos = "linux" + } + if platform.Goarch == "" { + platform.Goarch = "amd64" + } + if platform.Goarm == "" { + platform.Goarm = "6" + } + if platform.Goamd64 == "" { + platform.Goamd64 = "v1" + } +} + +func getApplicableArtifacts(ctx *context.Context, imageDefinition config.ImageDefinition, platforms []config.ContainerPlatform) *artifact.Artifacts { + var platformFilters []artifact.Filter + for _, platform := range platforms { + filters := []artifact.Filter{ + artifact.ByGoos(platform.Goos), + artifact.ByGoarch(platform.Goarch), + } + switch platform.Goarch { + case "amd64": + filters = append(filters, artifact.ByGoamd64(platform.Goamd64)) + case "arm": + filters = append(filters, artifact.ByGoarm(platform.Goarm)) + } + platformFilters = append(platformFilters, artifact.And( + filters..., + )) + } + filters := []artifact.Filter{ + artifact.Or(platformFilters...), + artifact.Or( + artifact.ByType(artifact.Binary), + artifact.ByType(artifact.LinuxPackage), + ), + } + if len(imageDefinition.IDs) > 0 { + filters = append(filters, artifact.ByIDs(imageDefinition.IDs...)) + } + return ctx.Artifacts.Filter(artifact.And(filters...)) +} diff --git a/internal/gio/link.go b/internal/gio/link.go new file mode 100644 index 00000000000..6f2d007c4de --- /dev/null +++ b/internal/gio/link.go @@ -0,0 +1,18 @@ +package gio + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/caarlos0/log" +) + +// Link creates a hard link and the parent directory if it does not exist yet. +func Link(src, dst string) error { + log.WithField("src", src).WithField("dst", dst).Debug("creating link") + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return fmt.Errorf("failed to make dir for destination: %w", err) + } + return os.Link(src, dst) +} diff --git a/internal/pipe/container/builder/api.go b/internal/pipe/container/builder/api.go index 9742f6e81ea..bcd5735f32e 100644 --- a/internal/pipe/container/builder/api.go +++ b/internal/pipe/container/builder/api.go @@ -2,21 +2,13 @@ package builder import ( "github.com/caarlos0/log" + "github.com/goreleaser/goreleaser/internal/containers" "github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/context" ) -type ImageBuildParameters struct { - ID string - BuildPath string - BuildFlags []string - PushFlags []string - Platforms []config.ContainerPlatform - Images []string -} - type ContainerBuilder interface { - Build(ctx *context.Context, params ImageBuildParameters, importImages bool, pushImages bool, logger *log.Entry) error + Build(ctx *context.Context, buildContext containers.ImageBuildContext, importImages bool, pushImages bool, logger *log.Entry) error // If returning true, Build should only be called once (in Run for `goreleaser build` or `goreleaser release --snapshot`, in Publish for `goreleaser release`) // If returning false, Build should be called once in Run phase and again in Publish phase SkipBuildIfPublish() bool diff --git a/internal/pipe/container/builder/buildkit.go b/internal/pipe/container/builder/buildkit.go index 137d39206f2..ed42633e447 100644 --- a/internal/pipe/container/builder/buildkit.go +++ b/internal/pipe/container/builder/buildkit.go @@ -13,6 +13,7 @@ import ( "github.com/caarlos0/log" "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/internal/containers" "github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/context" ) @@ -83,17 +84,17 @@ func (BuildKitBuilder) SkipBuildIfPublish() bool { return true } -func (b BuildKitBuilder) Build(ctx *context.Context, params ImageBuildParameters, importImages bool, pushImages bool, logger *log.Entry) error { +func (b BuildKitBuilder) Build(ctx *context.Context, buildContext containers.ImageBuildContext, importImages bool, pushImages bool, logger *log.Entry) error { needLoad := importImages && !b.storeImageNatively if !pushImages || needLoad { if needLoad || !b.supportsMultiPlatformsBuild { // If the builder does not support multi-arch builds (e.g. standard docker) or we need to load the results, // we trigger multiple parallel builds // ToDo: for now this loop is not parallel as we don't pass the syncGroup here - for _, platform := range params.Platforms { - logger := logger.WithField("platfrom", fmt.Sprintf("%s/%s", platform.Goos, platform.Goarch)) + for _, platform := range buildContext.Platforms { + logger := logger.WithField("platform", fmt.Sprintf("%s/%s", platform.Goos, platform.Goarch)) logger.Info("building...") - err := b.buildImages(ctx, params, []config.ContainerPlatform{platform}, needLoad) + err := b.buildImages(ctx, buildContext, []config.ContainerPlatform{platform}, needLoad) if err != nil { return fmt.Errorf("failed to build image for platform %v: %w", platform, err) } @@ -101,7 +102,7 @@ func (b BuildKitBuilder) Build(ctx *context.Context, params ImageBuildParameters } } else { logger.Info("building...") - err := b.buildImages(ctx, params, params.Platforms, needLoad) + err := b.buildImages(ctx, buildContext, buildContext.Platforms, needLoad) if err != nil { return fmt.Errorf("failed to build image: %w", err) } @@ -110,7 +111,7 @@ func (b BuildKitBuilder) Build(ctx *context.Context, params ImageBuildParameters } if pushImages { logger.Info("pushing...") - err := b.pushImages(ctx, params) + err := b.pushImages(ctx, buildContext) if err != nil { return fmt.Errorf("failed to push image: %w", err) } @@ -121,7 +122,7 @@ func (b BuildKitBuilder) Build(ctx *context.Context, params ImageBuildParameters return nil } -func (b BuildKitBuilder) pushImages(ctx *context.Context, params ImageBuildParameters) error { +func (b BuildKitBuilder) pushImages(ctx *context.Context, params containers.ImageBuildContext) error { flags := []string{} flags = append(flags, params.BuildFlags...) flags = append(flags, params.PushFlags...) @@ -175,8 +176,8 @@ func (b BuildKitBuilder) pushImages(ctx *context.Context, params ImageBuildParam return nil } -func (b BuildKitBuilder) buildImages(ctx *context.Context, params ImageBuildParameters, platforms []config.ContainerPlatform, loadImages bool) error { - digest, err := build(ctx, params.BuildPath, params.Images, params.BuildFlags, b.BuilderName, platforms, loadImages) +func (b BuildKitBuilder) buildImages(ctx *context.Context, buildContext containers.ImageBuildContext, platforms []config.ContainerPlatform, loadImages bool) error { + digest, err := build(ctx, buildContext.BuildPath, buildContext.Images, buildContext.BuildFlags, b.BuilderName, platforms, loadImages) if err != nil { return err } @@ -185,7 +186,7 @@ func (b BuildKitBuilder) buildImages(ctx *context.Context, params ImageBuildPara } if !b.supportsMultiPlatformsBuild { // Case of non-multiplatform builders - for _, img := range params.Images { + for _, img := range buildContext.Images { art := &artifact.Artifact{ Type: artifact.PublishableDockerImage, Name: img, @@ -196,13 +197,13 @@ func (b BuildKitBuilder) buildImages(ctx *context.Context, params ImageBuildPara artifact.ExtraDigest: digest, }, } - if params.ID != "" { - art.Extra[artifact.ExtraID] = params.ID + if buildContext.ID != "" { + art.Extra[artifact.ExtraID] = buildContext.ID } ctx.Artifacts.Add(art) } } else { - for _, img := range params.Images { + for _, img := range buildContext.Images { art := &artifact.Artifact{ Type: artifact.DockerManifest, Name: img, @@ -211,8 +212,8 @@ func (b BuildKitBuilder) buildImages(ctx *context.Context, params ImageBuildPara artifact.ExtraDigest: digest, }, } - if params.ID != "" { - art.Extra[artifact.ExtraID] = params.ID + if buildContext.ID != "" { + art.Extra[artifact.ExtraID] = buildContext.ID } ctx.Artifacts.Add(art) } diff --git a/internal/pipe/container/container.go b/internal/pipe/container/container.go index 34c5dec24bf..58d79a4b393 100644 --- a/internal/pipe/container/container.go +++ b/internal/pipe/container/container.go @@ -2,13 +2,9 @@ package container import ( "fmt" - "os" - "path/filepath" "strings" - "github.com/caarlos0/log" - "github.com/goreleaser/goreleaser/internal/artifact" - "github.com/goreleaser/goreleaser/internal/gio" + "github.com/goreleaser/goreleaser/internal/containers" "github.com/goreleaser/goreleaser/internal/ids" "github.com/goreleaser/goreleaser/internal/pipe" containerbuilder "github.com/goreleaser/goreleaser/internal/pipe/container/builder" @@ -41,28 +37,7 @@ func (Pipe) Default(ctx *context.Context) error { if container.ID != "" { ids.Inc(container.ID) } - if len(container.Platforms) == 0 { - container.Platforms = []config.ContainerPlatform{{ - Goos: "linux", - Goarch: "amd64", - }} - } else { - for i := range container.Platforms { - platform := &container.Platforms[i] - if platform.Goos == "" { - platform.Goos = "linux" - } - if platform.Goarch == "" { - platform.Goarch = "amd64" - } - if platform.Goarm == "" { - platform.Goarm = "6" - } - if platform.Goamd64 == "" { - platform.Goamd64 = "v1" - } - } - } + container.Platforms = containers.DefaultPlatforms(container.Platforms) if container.Dockerfile == "" { container.Dockerfile = "Dockerfile" } @@ -85,19 +60,22 @@ func (p Pipe) Publish(ctx *context.Context) error { // Build the images only. func (p Pipe) Run(ctx *context.Context) error { - // if !skips.Any(ctx, skips.Publish) { - // return pipe.Skip("images will directly be published later") - // } - - // How to know if we are in build instead? - return p.runContainerBuilds(ctx, releaseRun) + var phase runPhase + switch ctx.Action { + case context.ActionBuild: + phase = buildRun + case context.ActionRelease: + phase = releaseRun + default: + return pipe.Skip("nothing to build for this action") + } + return p.runContainerBuilds(ctx, phase) } func (p Pipe) runContainerBuilds(ctx *context.Context, phase runPhase) error { containerSkips := pipe.SkipMemento{} g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism)) - for i, container := range ctx.Config.Containers { - i := i + for _, container := range ctx.Config.Containers { containerConfig := container builder, err := containerbuilder.New(ctx, containerConfig.Builder) @@ -109,12 +87,8 @@ func (p Pipe) runContainerBuilds(ctx *context.Context, phase runPhase) error { return err } if skip != "" { - images, _ := processImageTemplates(ctx, containerConfig.ImageTemplates) - if len(images) > 0 { - log.WithField("image", images[0]).Infof("skipping: %s", skip) - } else { - log.WithField("index", i).Infof("skipping: %s", skip) - } + log := containers.LogEntry(ctx, containerConfig) + log.Infof("skipping: %s", skip) containerSkips.Remember(pipe.Skip(skip)) continue } @@ -172,135 +146,17 @@ func computeAction(ctx *context.Context, phase runPhase, skipImport, skipPush st return importImages, pushImages, skip, nil } -func getApplicableArtifacts(ctx *context.Context, imageDefinition config.ImageDefinition, platforms []config.ContainerPlatform) *artifact.Artifacts { - var platformFilters []artifact.Filter - for _, platform := range platforms { - filters := []artifact.Filter{ - artifact.ByGoos(platform.Goos), - artifact.ByGoarch(platform.Goarch), - } - switch platform.Goarch { - case "amd64": - filters = append(filters, artifact.ByGoamd64(platform.Goamd64)) - case "arm": - filters = append(filters, artifact.ByGoarm(platform.Goarm)) - } - platformFilters = append(platformFilters, artifact.And( - filters..., - )) - } - filters := []artifact.Filter{ - artifact.Or(platformFilters...), - artifact.Or( - artifact.ByType(artifact.Binary), - artifact.ByType(artifact.LinuxPackage), - ), - } - if len(imageDefinition.IDs) > 0 { - filters = append(filters, artifact.ByIDs(imageDefinition.IDs...)) - } - return ctx.Artifacts.Filter(artifact.And(filters...)) -} - func process(ctx *context.Context, config config.Container, importImages, pushImages bool, builder containerbuilder.ContainerBuilder) error { - artifacts := getApplicableArtifacts(ctx, config.ImageDefinition, config.Platforms).List() - if len(artifacts) == 0 { - log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work") - } - - params := containerbuilder.ImageBuildParameters{ - ID: config.ID, - Platforms: config.Platforms, - } - - tmp, err := os.MkdirTemp("", "goreleaserdocker") - if err != nil { - return fmt.Errorf("failed to create temporary dir: %w", err) - } - defer os.RemoveAll(tmp) - params.BuildPath = tmp - - images, err := processImageTemplates(ctx, config.ImageTemplates) - if err != nil { - return err - } - - if len(images) == 0 { - return pipe.Skip("no image templates found") - } - params.Images = images - - log := log.WithField("image", images[0]) - log.Debug("tempdir: " + tmp) + log := containers.LogEntry(ctx, config) - if err := tmpl.New(ctx).ApplyAll( - &config.Dockerfile, - ); err != nil { - return err - } - if err := gio.Copy( - config.Dockerfile, - filepath.Join(tmp, "Dockerfile"), - ); err != nil { - return fmt.Errorf("failed to copy dockerfile: %w", err) - } - - for _, file := range config.Files { - if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil { - return fmt.Errorf("failed to copy extra file '%s': %w", file, err) - } - if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil { - return fmt.Errorf("failed to copy extra file '%s': %w", file, err) - } - } - for _, art := range artifacts { - target := filepath.Join(tmp, art.Name) - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - return fmt.Errorf("failed to make dir for artifact: %w", err) - } - - if err := gio.Copy(art.Path, target); err != nil { - return fmt.Errorf("failed to copy artifact: %w", err) - } - } - - buildFlags, err := processBuildFlagTemplates(ctx, config.BuildFlagTemplates) + context, cleanup, err := containers.BuildContext(ctx, config.ID, config.ImageDefinition, config.Platforms) if err != nil { - return err - } - params.BuildFlags = buildFlags - params.PushFlags = config.PushFlags - - return builder.Build(ctx, params, importImages, pushImages, log) -} - -func processImageTemplates(ctx *context.Context, templates []string) ([]string, error) { - // nolint:prealloc - var images []string - for _, imageTemplate := range templates { - image, err := tmpl.New(ctx).Apply(imageTemplate) - if err != nil { - return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err) - } - if image == "" { - continue + if pipe.IsSkip(err) { + log.Infof("skipping: %s", err) } - - images = append(images, image) + return nil } + defer cleanup() - return images, nil -} - -func processBuildFlagTemplates(ctx *context.Context, flagTemplates []string) ([]string, error) { - // nolint:prealloc - var buildFlags []string - for _, buildFlagTemplate := range flagTemplates { - buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate) - if err != nil { - return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err) - } - buildFlags = append(buildFlags, buildFlag) - } - return buildFlags, nil + return builder.Build(ctx, context, importImages, pushImages, log) } diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go index 63dddea1821..ce2e5dd8af8 100644 --- a/internal/pipe/docker/docker.go +++ b/internal/pipe/docker/docker.go @@ -4,7 +4,6 @@ import ( "fmt" "io/fs" "net/http" - "os" "path/filepath" "sort" "strings" @@ -12,7 +11,7 @@ import ( "github.com/caarlos0/log" "github.com/goreleaser/goreleaser/internal/artifact" - "github.com/goreleaser/goreleaser/internal/gio" + "github.com/goreleaser/goreleaser/internal/containers" "github.com/goreleaser/goreleaser/internal/ids" "github.com/goreleaser/goreleaser/internal/pipe" "github.com/goreleaser/goreleaser/internal/semerrgroup" @@ -59,18 +58,7 @@ func (Pipe) Default(ctx *context.Context) error { if docker.ID != "" { ids.Inc(docker.ID) } - if docker.Goos == "" { - docker.Goos = "linux" - } - if docker.Goarch == "" { - docker.Goarch = "amd64" - } - if docker.Goarm == "" { - docker.Goarm = "6" - } - if docker.Goamd64 == "" { - docker.Goamd64 = "v1" - } + containers.DefaultPlatform(&docker.ContainerPlatform) if docker.Dockerfile == "" { docker.Dockerfile = "Dockerfile" } @@ -123,27 +111,13 @@ func (Pipe) Run(ctx *context.Context) error { g.Go(func() error { log := log.WithField("index", i) log.Debug("looking for artifacts matching") - filters := []artifact.Filter{ - artifact.ByGoos(docker.Goos), - artifact.ByGoarch(docker.Goarch), - artifact.Or( - artifact.ByType(artifact.Binary), - artifact.ByType(artifact.LinuxPackage), - ), - } - // TODO: properly test this - switch docker.Goarch { - case "amd64": - filters = append(filters, artifact.ByGoamd64(docker.Goamd64)) - case "arm": - filters = append(filters, artifact.ByGoarm(docker.Goarm)) - } - if len(docker.IDs) > 0 { - filters = append(filters, artifact.ByIDs(docker.IDs...)) + + buildContext, cleanup, err := containers.BuildContext(ctx, docker.ID, docker.ImageDefinition, []config.ContainerPlatform{docker.ContainerPlatform}) + if err != nil { + return err } - artifacts := ctx.Artifacts.Filter(artifact.And(filters...)) - log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts") - return process(ctx, docker, artifacts.List()) + defer cleanup() + return process(ctx, buildContext, docker) }) } if err := g.Wait(); err != nil { @@ -155,69 +129,13 @@ func (Pipe) Run(ctx *context.Context) error { return nil } -func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.Artifact) error { - if len(artifacts) == 0 { - log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work") - } - tmp, err := os.MkdirTemp("", "goreleaserdocker") - if err != nil { - return fmt.Errorf("failed to create temporary dir: %w", err) - } - defer os.RemoveAll(tmp) - - images, err := processImageTemplates(ctx, docker) - if err != nil { - return err - } - - if len(images) == 0 { - return pipe.Skip("no image templates found") - } - - log := log.WithField("image", images[0]) - log.Debug("tempdir: " + tmp) - - if err := tmpl.New(ctx).ApplyAll( - &docker.Dockerfile, - ); err != nil { - return err - } - if err := gio.Copy( - docker.Dockerfile, - filepath.Join(tmp, "Dockerfile"), - ); err != nil { - return fmt.Errorf("failed to copy dockerfile: %w", err) - } - - for _, file := range docker.Files { - if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil { - return fmt.Errorf("failed to copy extra file '%s': %w", file, err) - } - if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil { - return fmt.Errorf("failed to copy extra file '%s': %w", file, err) - } - } - for _, art := range artifacts { - target := filepath.Join(tmp, art.Name) - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - return fmt.Errorf("failed to make dir for artifact: %w", err) - } - - if err := gio.Copy(art.Path, target); err != nil { - return fmt.Errorf("failed to copy artifact: %w", err) - } - } - - buildFlags, err := processBuildFlagTemplates(ctx, docker) - if err != nil { - return err - } - +func process(ctx *context.Context, buildContext containers.ImageBuildContext, dockerConfig config.Docker) error { + log := buildContext.Log() log.Info("building docker image") - if err := imagers[docker.Use].Build(ctx, tmp, images, buildFlags); err != nil { + if err := imagers[dockerConfig.Use].Build(ctx, buildContext.BuildPath, buildContext.Images, buildContext.BuildFlags); err != nil { if isFileNotFoundError(err.Error()) { var files []string - _ = filepath.Walk(tmp, func(_ string, info fs.FileInfo, _ error) error { + _ = filepath.Walk(buildContext.BuildPath, func(_ string, info fs.FileInfo, _ error) error { if info.IsDir() { return nil } @@ -233,7 +151,7 @@ files in that dir: %s Previous error: -%w`, tmp, strings.Join(files, "\n "), err) +%w`, buildContext.BuildPath, strings.Join(files, "\n "), err) } if isBuildxContextError(err.Error()) { return fmt.Errorf("docker buildx is not set to default context - please switch with 'docker context use default'") @@ -241,16 +159,21 @@ Previous error: return err } - for _, img := range images { + if len(buildContext.Platforms) != 1 { + return fmt.Errorf("docker builder supports only single-platform builds") + } + platform := buildContext.Platforms[0] + + for _, img := range buildContext.Images { ctx.Artifacts.Add(&artifact.Artifact{ Type: artifact.PublishableDockerImage, Name: img, Path: img, - Goarch: docker.Goarch, - Goos: docker.Goos, - Goarm: docker.Goarm, + Goarch: platform.Goarch, + Goos: platform.Goos, + Goarm: platform.Goarm, Extra: map[string]interface{}{ - dockerConfigExtra: docker, + dockerConfigExtra: dockerConfig, }, }) } @@ -269,37 +192,6 @@ func isBuildxContextError(out string) bool { return strings.Contains(out, "to switch to context") } -func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { - // nolint:prealloc - var images []string - for _, imageTemplate := range docker.ImageTemplates { - image, err := tmpl.New(ctx).Apply(imageTemplate) - if err != nil { - return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err) - } - if image == "" { - continue - } - - images = append(images, image) - } - - return images, nil -} - -func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { - // nolint:prealloc - var buildFlags []string - for _, buildFlagTemplate := range docker.BuildFlagTemplates { - buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate) - if err != nil { - return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err) - } - buildFlags = append(buildFlags, buildFlag) - } - return buildFlags, nil -} - func dockerPush(ctx *context.Context, image *artifact.Artifact) error { log.WithField("image", image.Name).Info("pushing") diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go index f324b83fafd..e6654a743ab 100644 --- a/internal/pipe/docker/docker_test.go +++ b/internal/pipe/docker/docker_test.go @@ -1482,50 +1482,6 @@ func TestDefaultSet(t *testing.T) { require.Equal(t, "Dockerfile.foo", docker.Dockerfile) } -func Test_processImageTemplates(t *testing.T) { - ctx := testctx.NewWithCfg( - config.Project{ - Builds: []config.Build{ - { - ID: "default", - }, - }, - Dockers: []config.Docker{ - { - ImageDefinition: config.ImageDefinition{ - Dockerfile: "Dockerfile.foo", - ImageTemplates: []string{ - "user/image:{{.Tag}}", - "gcr.io/image:{{.Tag}}-{{.Env.FOO}}", - "gcr.io/image:v{{.Major}}.{{.Minor}}", - }, - }, - SkipPush: "true", - }, - }, - Env: []string{"FOO=123"}, - }, - testctx.WithVersion("1.0.0"), - testctx.WithCurrentTag("v1.0.0"), - testctx.WithCommit("a1b2c3d4"), - testctx.WithSemver(1, 0, 0, ""), - ) - - require.NoError(t, Pipe{}.Default(ctx)) - require.Len(t, ctx.Config.Dockers, 1) - - docker := ctx.Config.Dockers[0] - require.Equal(t, "Dockerfile.foo", docker.Dockerfile) - - images, err := processImageTemplates(ctx, docker) - require.NoError(t, err) - require.Equal(t, []string{ - "user/image:v1.0.0", - "gcr.io/image:v1.0.0-123", - "gcr.io/image:v1.0", - }, images) -} - func TestSkip(t *testing.T) { t.Run("image", func(t *testing.T) { t.Run("skip", func(t *testing.T) { diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index ee2b0e88c50..2c8b4ccd3c8 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -133,6 +133,7 @@ var Pipeline = append( reportsizes.Pipe{}, // create and push docker images docker.Pipe{}, + // create and push container images container.Pipe{}, // publishes artifacts publish.New(),