github.com/amane3/goreleaser@v0.182.0/internal/pipe/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/amane3/goreleaser/internal/artifact"
    13  	"github.com/amane3/goreleaser/internal/pipe"
    14  	"github.com/amane3/goreleaser/internal/semerrgroup"
    15  	"github.com/amane3/goreleaser/internal/tmpl"
    16  	"github.com/amane3/goreleaser/pkg/config"
    17  	"github.com/amane3/goreleaser/pkg/context"
    18  	"github.com/apex/log"
    19  )
    20  
    21  // ErrNoDocker is shown when docker cannot be found in $PATH.
    22  var ErrNoDocker = errors.New("docker not present in $PATH")
    23  
    24  // Pipe for docker.
    25  type Pipe struct{}
    26  
    27  func (Pipe) String() string {
    28  	return "docker images"
    29  }
    30  
    31  // Default sets the pipe defaults.
    32  func (Pipe) Default(ctx *context.Context) error {
    33  	for i := range ctx.Config.Dockers {
    34  		var docker = &ctx.Config.Dockers[i]
    35  
    36  		if docker.Goos == "" {
    37  			docker.Goos = "linux"
    38  		}
    39  		if docker.Goarch == "" {
    40  			docker.Goarch = "amd64"
    41  		}
    42  		if docker.Dockerfile == "" {
    43  			docker.Dockerfile = "Dockerfile"
    44  		}
    45  		for _, f := range docker.Files {
    46  			if f == "." || strings.HasPrefix(f, ctx.Config.Dist) {
    47  				return fmt.Errorf("invalid docker.files: can't be . or inside dist folder: %s", f)
    48  			}
    49  		}
    50  	}
    51  	// only set defaults if there is exactly 1 docker setup in the config file.
    52  	if len(ctx.Config.Dockers) != 1 {
    53  		return nil
    54  	}
    55  	if len(ctx.Config.Dockers[0].Binaries) == 0 {
    56  		ctx.Config.Dockers[0].Binaries = []string{
    57  			ctx.Config.Builds[0].Binary,
    58  		}
    59  	}
    60  	return nil
    61  }
    62  
    63  // Run the pipe.
    64  func (Pipe) Run(ctx *context.Context) error {
    65  	if len(ctx.Config.Dockers) == 0 || len(ctx.Config.Dockers[0].ImageTemplates) == 0 {
    66  		return pipe.Skip("docker section is not configured")
    67  	}
    68  	_, err := exec.LookPath("docker")
    69  	if err != nil {
    70  		return ErrNoDocker
    71  	}
    72  	return doRun(ctx)
    73  }
    74  
    75  // Publish the docker images.
    76  func (Pipe) Publish(ctx *context.Context) error {
    77  	if ctx.SkipPublish {
    78  		return pipe.ErrSkipPublishEnabled
    79  	}
    80  	var images = ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
    81  	for _, image := range images {
    82  		if err := dockerPush(ctx, image); err != nil {
    83  			return err
    84  		}
    85  	}
    86  	return nil
    87  }
    88  
    89  func doRun(ctx *context.Context) error {
    90  	var g = semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
    91  	for _, docker := range ctx.Config.Dockers {
    92  		docker := docker
    93  		g.Go(func() error {
    94  			log.WithField("docker", docker).Debug("looking for binaries matching")
    95  			var binaryNames = make([]string, len(docker.Binaries))
    96  			for i := range docker.Binaries {
    97  				bin, err := tmpl.New(ctx).Apply(docker.Binaries[i])
    98  				if err != nil {
    99  					return fmt.Errorf("failed to execute binary template '%s': %w", docker.Binaries[i], err)
   100  				}
   101  				binaryNames[i] = bin
   102  			}
   103  			var filters = []artifact.Filter{
   104  				artifact.ByGoos(docker.Goos),
   105  				artifact.ByGoarch(docker.Goarch),
   106  				artifact.ByGoarm(docker.Goarm),
   107  				artifact.ByType(artifact.Binary),
   108  				func(a *artifact.Artifact) bool {
   109  					for _, bin := range binaryNames {
   110  						if a.ExtraOr("Binary", "").(string) == bin {
   111  							return true
   112  						}
   113  					}
   114  					return false
   115  				},
   116  			}
   117  			if len(docker.Builds) > 0 {
   118  				filters = append(filters, artifact.ByIDs(docker.Builds...))
   119  			}
   120  			var binaries = ctx.Artifacts.Filter(artifact.And(filters...)).List()
   121  			// TODO: not so good of a check, if one binary match multiple
   122  			// binaries and the other match none, this will still pass...
   123  			log.WithField("binaries", binaries).Debug("found binaries")
   124  			if len(binaries) != len(docker.Binaries) {
   125  				return fmt.Errorf(
   126  					"%d binaries match docker definition: %v: %s_%s_%s, should be %d",
   127  					len(binaries),
   128  					binaryNames, docker.Goos, docker.Goarch, docker.Goarm,
   129  					len(docker.Binaries),
   130  				)
   131  			}
   132  			return process(ctx, docker, binaries)
   133  		})
   134  	}
   135  	return g.Wait()
   136  }
   137  
   138  func process(ctx *context.Context, docker config.Docker, bins []*artifact.Artifact) error {
   139  	tmp, err := ioutil.TempDir(ctx.Config.Dist, "goreleaserdocker")
   140  	if err != nil {
   141  		return fmt.Errorf("failed to create temporary dir: %w", err)
   142  	}
   143  	log.Debug("tempdir: " + tmp)
   144  
   145  	images, err := processImageTemplates(ctx, docker)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	if err := os.Link(docker.Dockerfile, filepath.Join(tmp, "Dockerfile")); err != nil {
   151  		return fmt.Errorf("failed to link dockerfile: %w", err)
   152  	}
   153  	for _, file := range docker.Files {
   154  		if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0755); err != nil {
   155  			return fmt.Errorf("failed to link extra file '%s': %w", file, err)
   156  		}
   157  		if err := link(file, filepath.Join(tmp, file)); err != nil {
   158  			return fmt.Errorf("failed to link extra file '%s': %w", file, err)
   159  		}
   160  	}
   161  	for _, bin := range bins {
   162  		if err := os.Link(bin.Path, filepath.Join(tmp, filepath.Base(bin.Path))); err != nil {
   163  			return fmt.Errorf("failed to link binary: %w", err)
   164  		}
   165  	}
   166  
   167  	buildFlags, err := processBuildFlagTemplates(ctx, docker)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	if err := dockerBuild(ctx, tmp, images, buildFlags); err != nil {
   173  		return err
   174  	}
   175  
   176  	if strings.TrimSpace(docker.SkipPush) == "true" {
   177  		return pipe.Skip("docker.skip_push is set")
   178  	}
   179  	if ctx.SkipPublish {
   180  		return pipe.ErrSkipPublishEnabled
   181  	}
   182  	if ctx.Config.Release.Draft {
   183  		return pipe.Skip("release is marked as draft")
   184  	}
   185  	if strings.TrimSpace(docker.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
   186  		return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish")
   187  	}
   188  	for _, img := range images {
   189  		ctx.Artifacts.Add(&artifact.Artifact{
   190  			Type:   artifact.PublishableDockerImage,
   191  			Name:   img,
   192  			Path:   img,
   193  			Goarch: docker.Goarch,
   194  			Goos:   docker.Goos,
   195  			Goarm:  docker.Goarm,
   196  		})
   197  	}
   198  	return nil
   199  }
   200  
   201  func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string, error) {
   202  	// nolint:prealloc
   203  	var images []string
   204  	for _, imageTemplate := range docker.ImageTemplates {
   205  		image, err := tmpl.New(ctx).Apply(imageTemplate)
   206  		if err != nil {
   207  			return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err)
   208  		}
   209  
   210  		images = append(images, image)
   211  	}
   212  
   213  	return images, nil
   214  }
   215  
   216  func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]string, error) {
   217  	// nolint:prealloc
   218  	var buildFlags []string
   219  	for _, buildFlagTemplate := range docker.BuildFlagTemplates {
   220  		buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate)
   221  		if err != nil {
   222  			return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err)
   223  		}
   224  		buildFlags = append(buildFlags, buildFlag)
   225  	}
   226  	return buildFlags, nil
   227  }
   228  
   229  // walks the src, recreating dirs and hard-linking files.
   230  func link(src, dest string) error {
   231  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   232  		if err != nil {
   233  			return err
   234  		}
   235  		// We have the following:
   236  		// - src = "a/b"
   237  		// - dest = "dist/linuxamd64/b"
   238  		// - path = "a/b/c.txt"
   239  		// So we join "a/b" with "c.txt" and use it as the destination.
   240  		var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
   241  		log.WithFields(log.Fields{
   242  			"src": path,
   243  			"dst": dst,
   244  		}).Debug("extra file")
   245  		if info.IsDir() {
   246  			return os.MkdirAll(dst, info.Mode())
   247  		}
   248  		return os.Link(path, dst)
   249  	})
   250  }
   251  
   252  func dockerBuild(ctx *context.Context, root string, images, flags []string) error {
   253  	log.WithField("image", images[0]).Info("building docker image")
   254  	/* #nosec */
   255  	var cmd = exec.CommandContext(ctx, "docker", buildCommand(images, flags)...)
   256  	cmd.Dir = root
   257  	log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running")
   258  	out, err := cmd.CombinedOutput()
   259  	if err != nil {
   260  		return fmt.Errorf("failed to build docker image: %s: \n%s: %w", images[0], string(out), err)
   261  	}
   262  	log.Debugf("docker build output: \n%s", string(out))
   263  	return nil
   264  }
   265  
   266  func buildCommand(images, flags []string) []string {
   267  	base := []string{"build", "."}
   268  	for _, image := range images {
   269  		base = append(base, "-t", image)
   270  	}
   271  	base = append(base, flags...)
   272  	return base
   273  }
   274  
   275  func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
   276  	log.WithField("image", image.Name).Info("pushing docker image")
   277  	/* #nosec */
   278  	var cmd = exec.CommandContext(ctx, "docker", "push", image.Name)
   279  	log.WithField("cmd", cmd.Args).Debug("running")
   280  	out, err := cmd.CombinedOutput()
   281  	if err != nil {
   282  		return fmt.Errorf("failed to push docker image: \n%s: %w", string(out), err)
   283  	}
   284  	log.Debugf("docker push output: \n%s", string(out))
   285  	ctx.Artifacts.Add(&artifact.Artifact{
   286  		Type:   artifact.DockerImage,
   287  		Name:   image.Name,
   288  		Path:   image.Path,
   289  		Goarch: image.Goarch,
   290  		Goos:   image.Goos,
   291  		Goarm:  image.Goarm,
   292  	})
   293  	return nil
   294  }