github.com/jonathanlloyd/goreleaser@v0.91.1/internal/pipe/docker/docker.go (about)

     1  // Package docker provides a Pipe that creates and pushes a Docker image
     2  package docker
     3  
     4  import (
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/apex/log"
    13  	"github.com/goreleaser/goreleaser/internal/artifact"
    14  	"github.com/goreleaser/goreleaser/internal/deprecate"
    15  	"github.com/goreleaser/goreleaser/internal/pipe"
    16  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    17  	"github.com/goreleaser/goreleaser/internal/tmpl"
    18  	"github.com/goreleaser/goreleaser/pkg/config"
    19  	"github.com/goreleaser/goreleaser/pkg/context"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  // ErrNoDocker is shown when docker cannot be found in $PATH
    24  var ErrNoDocker = errors.New("docker not present in $PATH")
    25  
    26  // Pipe for docker
    27  type Pipe struct{}
    28  
    29  func (Pipe) String() string {
    30  	return "creating Docker images"
    31  }
    32  
    33  // Default sets the pipe defaults
    34  func (Pipe) Default(ctx *context.Context) error {
    35  	for i := range ctx.Config.Dockers {
    36  		var docker = &ctx.Config.Dockers[i]
    37  
    38  		if docker.Image != "" {
    39  			deprecate.Notice("docker.image")
    40  
    41  			if len(docker.TagTemplates) != 0 {
    42  				deprecate.Notice("docker.tag_templates")
    43  			} else {
    44  				docker.TagTemplates = []string{"{{ .Version }}"}
    45  			}
    46  		}
    47  
    48  		if docker.Goos == "" {
    49  			docker.Goos = "linux"
    50  		}
    51  		if docker.Goarch == "" {
    52  			docker.Goarch = "amd64"
    53  		}
    54  	}
    55  	// only set defaults if there is exactly 1 docker setup in the config file.
    56  	if len(ctx.Config.Dockers) != 1 {
    57  		return nil
    58  	}
    59  	if ctx.Config.Dockers[0].Binary == "" {
    60  		ctx.Config.Dockers[0].Binary = ctx.Config.Builds[0].Binary
    61  	}
    62  	if ctx.Config.Dockers[0].Dockerfile == "" {
    63  		ctx.Config.Dockers[0].Dockerfile = "Dockerfile"
    64  	}
    65  	return nil
    66  }
    67  
    68  // Run the pipe
    69  func (Pipe) Run(ctx *context.Context) error {
    70  	if len(ctx.Config.Dockers) == 0 || missingImage(ctx) {
    71  		return pipe.Skip("docker section is not configured")
    72  	}
    73  	_, err := exec.LookPath("docker")
    74  	if err != nil {
    75  		return ErrNoDocker
    76  	}
    77  	return doRun(ctx)
    78  }
    79  
    80  // Publish the docker images
    81  func (Pipe) Publish(ctx *context.Context) error {
    82  	var images = ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
    83  	for _, image := range images {
    84  		if err := dockerPush(ctx, image); err != nil {
    85  			return err
    86  		}
    87  	}
    88  	return nil
    89  }
    90  
    91  func missingImage(ctx *context.Context) bool {
    92  	return ctx.Config.Dockers[0].Image == "" && len(ctx.Config.Dockers[0].ImageTemplates) == 0
    93  }
    94  
    95  func doRun(ctx *context.Context) error {
    96  	var g = semerrgroup.New(ctx.Parallelism)
    97  	for _, docker := range ctx.Config.Dockers {
    98  		docker := docker
    99  		g.Go(func() error {
   100  			log.WithField("docker", docker).Debug("looking for binaries matching")
   101  			var binaries = ctx.Artifacts.Filter(
   102  				artifact.And(
   103  					artifact.ByGoos(docker.Goos),
   104  					artifact.ByGoarch(docker.Goarch),
   105  					artifact.ByGoarm(docker.Goarm),
   106  					artifact.ByType(artifact.Binary),
   107  					func(a artifact.Artifact) bool {
   108  						return a.Extra["Binary"] == docker.Binary
   109  					},
   110  				),
   111  			).List()
   112  			if len(binaries) != 1 {
   113  				return fmt.Errorf(
   114  					"%d binaries match docker definition: %s: %s_%s_%s",
   115  					len(binaries),
   116  					docker.Binary, docker.Goos, docker.Goarch, docker.Goarm,
   117  				)
   118  			}
   119  			return process(ctx, docker, binaries[0])
   120  		})
   121  	}
   122  	return g.Wait()
   123  }
   124  
   125  func process(ctx *context.Context, docker config.Docker, bin artifact.Artifact) error {
   126  	tmp, err := ioutil.TempDir(ctx.Config.Dist, "goreleaserdocker")
   127  	if err != nil {
   128  		return errors.Wrap(err, "failed to create temporary dir")
   129  	}
   130  	log.Debug("tempdir: " + tmp)
   131  
   132  	images, err := processImageTemplates(ctx, docker, bin)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	if err := os.Link(docker.Dockerfile, filepath.Join(tmp, "Dockerfile")); err != nil {
   138  		return errors.Wrap(err, "failed to link dockerfile")
   139  	}
   140  	for _, file := range docker.Files {
   141  		if err := link(file, filepath.Join(tmp, filepath.Base(file))); err != nil {
   142  			return errors.Wrapf(err, "failed to link extra file '%s'", file)
   143  		}
   144  	}
   145  	if err := os.Link(bin.Path, filepath.Join(tmp, filepath.Base(bin.Path))); err != nil {
   146  		return errors.Wrap(err, "failed to link binary")
   147  	}
   148  
   149  	buildFlags, err := processBuildFlagTemplates(ctx, docker, bin)
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	if err := dockerBuild(ctx, tmp, images[0], buildFlags); err != nil {
   155  		return err
   156  	}
   157  	for _, img := range images[1:] {
   158  		if err := dockerTag(ctx, images[0], img); err != nil {
   159  			return err
   160  		}
   161  	}
   162  	if docker.SkipPush {
   163  		// TODO: this should also be better handled
   164  		log.Warn(pipe.Skip("skip_push is set").Error())
   165  		return nil
   166  	}
   167  	for _, img := range images {
   168  		ctx.Artifacts.Add(artifact.Artifact{
   169  			Type:   artifact.PublishableDockerImage,
   170  			Name:   img,
   171  			Path:   img,
   172  			Goarch: docker.Goarch,
   173  			Goos:   docker.Goos,
   174  			Goarm:  docker.Goarm,
   175  		})
   176  	}
   177  	return nil
   178  }
   179  
   180  func processImageTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) {
   181  	if len(docker.ImageTemplates) != 0 && docker.Image != "" {
   182  		return nil, errors.New("failed to process image, use either image_templates (preferred) or image, not both")
   183  	}
   184  
   185  	// nolint:prealloc
   186  	var images []string
   187  	for _, imageTemplate := range docker.ImageTemplates {
   188  		// TODO: add overrides support to config
   189  		image, err := tmpl.New(ctx).
   190  			WithArtifact(artifact, map[string]string{}).
   191  			Apply(imageTemplate)
   192  		if err != nil {
   193  			return nil, errors.Wrapf(err, "failed to execute image template '%s'", imageTemplate)
   194  		}
   195  
   196  		images = append(images, image)
   197  	}
   198  
   199  	for _, tagTemplate := range docker.TagTemplates {
   200  		imageTemplate := fmt.Sprintf("%s:%s", docker.Image, tagTemplate)
   201  		// TODO: add overrides support to config
   202  		image, err := tmpl.New(ctx).
   203  			WithArtifact(artifact, map[string]string{}).
   204  			Apply(imageTemplate)
   205  		if err != nil {
   206  			return nil, errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate)
   207  		}
   208  		images = append(images, image)
   209  	}
   210  
   211  	return images, nil
   212  }
   213  
   214  func processBuildFlagTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) {
   215  	// nolint:prealloc
   216  	var buildFlags []string
   217  	for _, buildFlagTemplate := range docker.BuildFlagTemplates {
   218  		buildFlag, err := tmpl.New(ctx).
   219  			WithArtifact(artifact, map[string]string{}).
   220  			Apply(buildFlagTemplate)
   221  		if err != nil {
   222  			return nil, errors.Wrapf(err, "failed to process build flag template '%s'", buildFlagTemplate)
   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, image string, flags []string) error {
   253  	log.WithField("image", image).Info("building docker image")
   254  	/* #nosec */
   255  	var cmd = exec.CommandContext(ctx, "docker", buildCommand(image, 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 errors.Wrapf(err, "failed to build docker image: \n%s", string(out))
   261  	}
   262  	log.Debugf("docker build output: \n%s", string(out))
   263  	return nil
   264  }
   265  
   266  func buildCommand(image string, flags []string) []string {
   267  	base := []string{"build", "-t", image, "."}
   268  	base = append(base, flags...)
   269  	return base
   270  }
   271  
   272  func dockerTag(ctx *context.Context, image, tag string) error {
   273  	log.WithField("image", image).WithField("tag", tag).Info("tagging docker image")
   274  	/* #nosec */
   275  	var cmd = exec.CommandContext(ctx, "docker", "tag", image, tag)
   276  	log.WithField("cmd", cmd.Args).Debug("running")
   277  	out, err := cmd.CombinedOutput()
   278  	if err != nil {
   279  		return errors.Wrapf(err, "failed to tag docker image: \n%s", string(out))
   280  	}
   281  	log.Debugf("docker tag output: \n%s", string(out))
   282  	return nil
   283  }
   284  
   285  func dockerPush(ctx *context.Context, image artifact.Artifact) error {
   286  	log.WithField("image", image.Name).Info("pushing docker image")
   287  	/* #nosec */
   288  	var cmd = exec.CommandContext(ctx, "docker", "push", image.Name)
   289  	log.WithField("cmd", cmd.Args).Debug("running")
   290  	out, err := cmd.CombinedOutput()
   291  	if err != nil {
   292  		return errors.Wrapf(err, "failed to push docker image: \n%s", string(out))
   293  	}
   294  	log.Debugf("docker push output: \n%s", string(out))
   295  	image.Type = artifact.DockerImage
   296  	ctx.Artifacts.Add(image)
   297  	return nil
   298  }