github.1git.de/goreleaser/goreleaser@v0.92.0/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 "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, buildFlags); err != nil {
   155  		return err
   156  	}
   157  	if docker.SkipPush {
   158  		// TODO: this should also be better handled
   159  		log.Warn(pipe.Skip("skip_push is set").Error())
   160  		return nil
   161  	}
   162  	for _, img := range images {
   163  		ctx.Artifacts.Add(artifact.Artifact{
   164  			Type:   artifact.PublishableDockerImage,
   165  			Name:   img,
   166  			Path:   img,
   167  			Goarch: docker.Goarch,
   168  			Goos:   docker.Goos,
   169  			Goarm:  docker.Goarm,
   170  		})
   171  	}
   172  	return nil
   173  }
   174  
   175  func processImageTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) {
   176  	if len(docker.ImageTemplates) != 0 && docker.Image != "" {
   177  		return nil, errors.New("failed to process image, use either image_templates (preferred) or image, not both")
   178  	}
   179  
   180  	// nolint:prealloc
   181  	var images []string
   182  	for _, imageTemplate := range docker.ImageTemplates {
   183  		// TODO: add overrides support to config
   184  		image, err := tmpl.New(ctx).
   185  			WithArtifact(artifact, map[string]string{}).
   186  			Apply(imageTemplate)
   187  		if err != nil {
   188  			return nil, errors.Wrapf(err, "failed to execute image template '%s'", imageTemplate)
   189  		}
   190  
   191  		images = append(images, image)
   192  	}
   193  
   194  	for _, tagTemplate := range docker.TagTemplates {
   195  		imageTemplate := fmt.Sprintf("%s:%s", docker.Image, tagTemplate)
   196  		// TODO: add overrides support to config
   197  		image, err := tmpl.New(ctx).
   198  			WithArtifact(artifact, map[string]string{}).
   199  			Apply(imageTemplate)
   200  		if err != nil {
   201  			return nil, errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate)
   202  		}
   203  		images = append(images, image)
   204  	}
   205  
   206  	return images, nil
   207  }
   208  
   209  func processBuildFlagTemplates(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) ([]string, error) {
   210  	// nolint:prealloc
   211  	var buildFlags []string
   212  	for _, buildFlagTemplate := range docker.BuildFlagTemplates {
   213  		buildFlag, err := tmpl.New(ctx).
   214  			WithArtifact(artifact, map[string]string{}).
   215  			Apply(buildFlagTemplate)
   216  		if err != nil {
   217  			return nil, errors.Wrapf(err, "failed to process build flag template '%s'", buildFlagTemplate)
   218  		}
   219  		buildFlags = append(buildFlags, buildFlag)
   220  	}
   221  	return buildFlags, nil
   222  }
   223  
   224  // walks the src, recreating dirs and hard-linking files
   225  func link(src, dest string) error {
   226  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   227  		if err != nil {
   228  			return err
   229  		}
   230  		// We have the following:
   231  		// - src = "a/b"
   232  		// - dest = "dist/linuxamd64/b"
   233  		// - path = "a/b/c.txt"
   234  		// So we join "a/b" with "c.txt" and use it as the destination.
   235  		var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
   236  		log.WithFields(log.Fields{
   237  			"src": path,
   238  			"dst": dst,
   239  		}).Debug("extra file")
   240  		if info.IsDir() {
   241  			return os.MkdirAll(dst, info.Mode())
   242  		}
   243  		return os.Link(path, dst)
   244  	})
   245  }
   246  
   247  func dockerBuild(ctx *context.Context, root string, images, flags []string) error {
   248  	log.WithField("image", images[0]).Info("building docker image")
   249  	/* #nosec */
   250  	var cmd = exec.CommandContext(ctx, "docker", buildCommand(images, flags)...)
   251  	cmd.Dir = root
   252  	log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running")
   253  	out, err := cmd.CombinedOutput()
   254  	if err != nil {
   255  		return errors.Wrapf(err, "failed to build docker image: \n%s", string(out))
   256  	}
   257  	log.Debugf("docker build output: \n%s", string(out))
   258  	return nil
   259  }
   260  
   261  func buildCommand(images, flags []string) []string {
   262  	base := []string{"build", "."}
   263  	for _, image := range images {
   264  		base = append(base, "-t", image)
   265  	}
   266  	base = append(base, flags...)
   267  	return base
   268  }
   269  
   270  func dockerPush(ctx *context.Context, image artifact.Artifact) error {
   271  	log.WithField("image", image.Name).Info("pushing docker image")
   272  	/* #nosec */
   273  	var cmd = exec.CommandContext(ctx, "docker", "push", image.Name)
   274  	log.WithField("cmd", cmd.Args).Debug("running")
   275  	out, err := cmd.CombinedOutput()
   276  	if err != nil {
   277  		return errors.Wrapf(err, "failed to push docker image: \n%s", string(out))
   278  	}
   279  	log.Debugf("docker push output: \n%s", string(out))
   280  	image.Type = artifact.DockerImage
   281  	ctx.Artifacts.Add(image)
   282  	return nil
   283  }