mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-08 03:31:59 +02:00
65ffbf1921
* refactor: remove pkg/errors Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * refactor: remove pkg/errors Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
296 lines
8.4 KiB
Go
296 lines
8.4 KiB
Go
// Package docker provides a Pipe that creates and pushes a Docker image
|
|
package docker
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/goreleaser/goreleaser/internal/artifact"
|
|
"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"
|
|
)
|
|
|
|
// ErrNoDocker is shown when docker cannot be found in $PATH.
|
|
var ErrNoDocker = errors.New("docker not present in $PATH")
|
|
|
|
// Pipe for docker.
|
|
type Pipe struct{}
|
|
|
|
func (Pipe) String() string {
|
|
return "docker images"
|
|
}
|
|
|
|
// Default sets the pipe defaults.
|
|
func (Pipe) Default(ctx *context.Context) error {
|
|
for i := range ctx.Config.Dockers {
|
|
var docker = &ctx.Config.Dockers[i]
|
|
|
|
if docker.Goos == "" {
|
|
docker.Goos = "linux"
|
|
}
|
|
if docker.Goarch == "" {
|
|
docker.Goarch = "amd64"
|
|
}
|
|
for _, f := range docker.Files {
|
|
if f == "." || strings.HasPrefix(f, ctx.Config.Dist) {
|
|
return fmt.Errorf("invalid docker.files: can't be . or inside dist folder: %s", f)
|
|
}
|
|
}
|
|
}
|
|
// only set defaults if there is exactly 1 docker setup in the config file.
|
|
if len(ctx.Config.Dockers) != 1 {
|
|
return nil
|
|
}
|
|
if len(ctx.Config.Dockers[0].Binaries) == 0 {
|
|
ctx.Config.Dockers[0].Binaries = []string{
|
|
ctx.Config.Builds[0].Binary,
|
|
}
|
|
}
|
|
if ctx.Config.Dockers[0].Dockerfile == "" {
|
|
ctx.Config.Dockers[0].Dockerfile = "Dockerfile"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run the pipe.
|
|
func (Pipe) Run(ctx *context.Context) error {
|
|
if len(ctx.Config.Dockers) == 0 || len(ctx.Config.Dockers[0].ImageTemplates) == 0 {
|
|
return pipe.Skip("docker section is not configured")
|
|
}
|
|
_, err := exec.LookPath("docker")
|
|
if err != nil {
|
|
return ErrNoDocker
|
|
}
|
|
return doRun(ctx)
|
|
}
|
|
|
|
// Publish the docker images.
|
|
func (Pipe) Publish(ctx *context.Context) error {
|
|
if ctx.SkipPublish {
|
|
return pipe.ErrSkipPublishEnabled
|
|
}
|
|
var images = ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
|
|
for _, image := range images {
|
|
if err := dockerPush(ctx, image); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doRun(ctx *context.Context) error {
|
|
var g = semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
|
|
for _, docker := range ctx.Config.Dockers {
|
|
docker := docker
|
|
g.Go(func() error {
|
|
log.WithField("docker", docker).Debug("looking for binaries matching")
|
|
var binaryNames = make([]string, len(docker.Binaries))
|
|
for i := range docker.Binaries {
|
|
bin, err := tmpl.New(ctx).Apply(docker.Binaries[i])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute binary template '%s': %w", docker.Binaries[i], err)
|
|
}
|
|
binaryNames[i] = bin
|
|
}
|
|
var filters = []artifact.Filter{
|
|
artifact.ByGoos(docker.Goos),
|
|
artifact.ByGoarch(docker.Goarch),
|
|
artifact.ByGoarm(docker.Goarm),
|
|
artifact.ByType(artifact.Binary),
|
|
func(a *artifact.Artifact) bool {
|
|
for _, bin := range binaryNames {
|
|
if a.ExtraOr("Binary", "").(string) == bin {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
if len(docker.Builds) > 0 {
|
|
filters = append(filters, artifact.ByIDs(docker.Builds...))
|
|
}
|
|
var binaries = ctx.Artifacts.Filter(artifact.And(filters...)).List()
|
|
// TODO: not so good of a check, if one binary match multiple
|
|
// binaries and the other match none, this will still pass...
|
|
log.WithField("binaries", binaries).Debug("found binaries")
|
|
if len(binaries) != len(docker.Binaries) {
|
|
return fmt.Errorf(
|
|
"%d binaries match docker definition: %v: %s_%s_%s, should be %d",
|
|
len(binaries),
|
|
binaryNames, docker.Goos, docker.Goarch, docker.Goarm,
|
|
len(docker.Binaries),
|
|
)
|
|
}
|
|
return process(ctx, docker, binaries)
|
|
})
|
|
}
|
|
return g.Wait()
|
|
}
|
|
|
|
func process(ctx *context.Context, docker config.Docker, bins []*artifact.Artifact) error {
|
|
tmp, err := ioutil.TempDir(ctx.Config.Dist, "goreleaserdocker")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temporary dir: %w", err)
|
|
}
|
|
log.Debug("tempdir: " + tmp)
|
|
|
|
images, err := processImageTemplates(ctx, docker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Link(docker.Dockerfile, filepath.Join(tmp, "Dockerfile")); err != nil {
|
|
return fmt.Errorf("failed to link dockerfile: %w", err)
|
|
}
|
|
for _, file := range docker.Files {
|
|
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0755); err != nil {
|
|
return fmt.Errorf("failed to link extra file '%s': %w", file, err)
|
|
}
|
|
if err := link(file, filepath.Join(tmp, file)); err != nil {
|
|
return fmt.Errorf("failed to link extra file '%s': %w", file, err)
|
|
}
|
|
}
|
|
for _, bin := range bins {
|
|
if err := os.Link(bin.Path, filepath.Join(tmp, filepath.Base(bin.Path))); err != nil {
|
|
return fmt.Errorf("failed to link binary: %w", err)
|
|
}
|
|
}
|
|
|
|
buildFlags, err := processBuildFlagTemplates(ctx, docker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dockerBuild(ctx, tmp, images, buildFlags); err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.TrimSpace(docker.SkipPush) == "true" {
|
|
return pipe.Skip("docker.skip_push is set")
|
|
}
|
|
if ctx.SkipPublish {
|
|
return pipe.ErrSkipPublishEnabled
|
|
}
|
|
if ctx.Config.Release.Draft {
|
|
return pipe.Skip("release is marked as draft")
|
|
}
|
|
if strings.TrimSpace(docker.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
|
|
return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish")
|
|
}
|
|
for _, img := range images {
|
|
ctx.Artifacts.Add(&artifact.Artifact{
|
|
Type: artifact.PublishableDockerImage,
|
|
Name: img,
|
|
Path: img,
|
|
Goarch: docker.Goarch,
|
|
Goos: docker.Goos,
|
|
Goarm: docker.Goarm,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// walks the src, recreating dirs and hard-linking files.
|
|
func link(src, dest string) error {
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// We have the following:
|
|
// - src = "a/b"
|
|
// - dest = "dist/linuxamd64/b"
|
|
// - path = "a/b/c.txt"
|
|
// So we join "a/b" with "c.txt" and use it as the destination.
|
|
var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
|
|
log.WithFields(log.Fields{
|
|
"src": path,
|
|
"dst": dst,
|
|
}).Debug("extra file")
|
|
if info.IsDir() {
|
|
return os.MkdirAll(dst, info.Mode())
|
|
}
|
|
return os.Link(path, dst)
|
|
})
|
|
}
|
|
|
|
func dockerBuild(ctx *context.Context, root string, images, flags []string) error {
|
|
log.WithField("image", images[0]).Info("building docker image")
|
|
/* #nosec */
|
|
var cmd = exec.CommandContext(ctx, "docker", buildCommand(images, flags)...)
|
|
cmd.Dir = root
|
|
log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build docker image: %s: \n%s: %w", images[0], string(out), err)
|
|
}
|
|
log.Debugf("docker build output: \n%s", string(out))
|
|
return nil
|
|
}
|
|
|
|
func buildCommand(images, flags []string) []string {
|
|
base := []string{"build", "."}
|
|
for _, image := range images {
|
|
base = append(base, "-t", image)
|
|
}
|
|
base = append(base, flags...)
|
|
return base
|
|
}
|
|
|
|
func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
|
|
log.WithField("image", image.Name).Info("pushing docker image")
|
|
/* #nosec */
|
|
var cmd = exec.CommandContext(ctx, "docker", "push", image.Name)
|
|
log.WithField("cmd", cmd.Args).Debug("running")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to push docker image: \n%s: %w", string(out), err)
|
|
}
|
|
log.Debugf("docker push output: \n%s", string(out))
|
|
ctx.Artifacts.Add(&artifact.Artifact{
|
|
Type: artifact.DockerImage,
|
|
Name: image.Name,
|
|
Path: image.Path,
|
|
Goarch: image.Goarch,
|
|
Goos: image.Goos,
|
|
Goarm: image.Goarm,
|
|
})
|
|
return nil
|
|
}
|