github.com/droot/goreleaser@v0.66.2-0.20180420030140-c2db5fb17157/pipeline/docker/docker.go (about)

     1  // Package docker provides a Pipe that creates and pushes a Docker image
     2  package docker
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"text/template"
    12  
    13  	"github.com/apex/log"
    14  	"github.com/masterminds/semver"
    15  	"github.com/pkg/errors"
    16  	"golang.org/x/sync/errgroup"
    17  
    18  	"github.com/goreleaser/goreleaser/config"
    19  	"github.com/goreleaser/goreleaser/context"
    20  	"github.com/goreleaser/goreleaser/internal/artifact"
    21  	"github.com/goreleaser/goreleaser/internal/deprecate"
    22  	"github.com/goreleaser/goreleaser/pipeline"
    23  )
    24  
    25  // ErrNoDocker is shown when docker cannot be found in $PATH
    26  var ErrNoDocker = errors.New("docker not present in $PATH")
    27  
    28  // Pipe for docker
    29  type Pipe struct{}
    30  
    31  func (Pipe) String() string {
    32  	return "creating Docker images"
    33  }
    34  
    35  // Default sets the pipe defaults
    36  func (Pipe) Default(ctx *context.Context) error {
    37  	for i := range ctx.Config.Dockers {
    38  		var docker = &ctx.Config.Dockers[i]
    39  		if docker.OldTagTemplate != "" {
    40  			deprecate.Notice("docker.tag_template")
    41  			docker.TagTemplates = append(docker.TagTemplates, docker.OldTagTemplate)
    42  		}
    43  		if len(docker.TagTemplates) == 0 {
    44  			docker.TagTemplates = append(docker.TagTemplates, "{{ .Version }}")
    45  		}
    46  		if docker.Goos == "" {
    47  			docker.Goos = "linux"
    48  		}
    49  		if docker.Goarch == "" {
    50  			docker.Goarch = "amd64"
    51  		}
    52  		if docker.Latest {
    53  			deprecate.Notice("docker.latest")
    54  			docker.TagTemplates = append(docker.TagTemplates, "latest")
    55  		}
    56  	}
    57  	// only set defaults if there is exacly 1 docker setup in the config file.
    58  	if len(ctx.Config.Dockers) != 1 {
    59  		return nil
    60  	}
    61  	if ctx.Config.Dockers[0].Binary == "" {
    62  		ctx.Config.Dockers[0].Binary = ctx.Config.Builds[0].Binary
    63  	}
    64  	if ctx.Config.Dockers[0].Dockerfile == "" {
    65  		ctx.Config.Dockers[0].Dockerfile = "Dockerfile"
    66  	}
    67  	return nil
    68  }
    69  
    70  // Run the pipe
    71  func (Pipe) Run(ctx *context.Context) error {
    72  	if len(ctx.Config.Dockers) == 0 || ctx.Config.Dockers[0].Image == "" {
    73  		return pipeline.Skip("docker section is not configured")
    74  	}
    75  	_, err := exec.LookPath("docker")
    76  	if err != nil {
    77  		return ErrNoDocker
    78  	}
    79  	return doRun(ctx)
    80  }
    81  
    82  func doRun(ctx *context.Context) error {
    83  	var g errgroup.Group
    84  	sem := make(chan bool, ctx.Parallelism)
    85  	for _, docker := range ctx.Config.Dockers {
    86  		docker := docker
    87  		sem <- true
    88  		g.Go(func() error {
    89  			defer func() {
    90  				<-sem
    91  			}()
    92  			log.WithField("docker", docker).Debug("looking for binaries matching")
    93  			var binaries = ctx.Artifacts.Filter(
    94  				artifact.And(
    95  					artifact.ByGoos(docker.Goos),
    96  					artifact.ByGoarch(docker.Goarch),
    97  					artifact.ByGoarm(docker.Goarm),
    98  					artifact.ByType(artifact.Binary),
    99  					func(a artifact.Artifact) bool {
   100  						return a.Extra["Binary"] == docker.Binary
   101  					},
   102  				),
   103  			).List()
   104  			if len(binaries) == 0 {
   105  				log.Warnf("no binaries found for %s", docker.Binary)
   106  			}
   107  			for _, binary := range binaries {
   108  				if err := process(ctx, docker, binary); err != nil {
   109  					return err
   110  				}
   111  			}
   112  			return nil
   113  		})
   114  	}
   115  	return g.Wait()
   116  }
   117  
   118  func tagName(ctx *context.Context, tagTemplate string) (string, error) {
   119  	var out bytes.Buffer
   120  	t, err := template.New("tag").Option("missingkey=error").Parse(tagTemplate)
   121  	if err != nil {
   122  		return "", err
   123  	}
   124  	sv, err := semver.NewVersion(ctx.Git.CurrentTag)
   125  	if err != nil {
   126  		return "", err
   127  	}
   128  	data := struct {
   129  		Version, Tag        string
   130  		Major, Minor, Patch int64
   131  		Env                 map[string]string
   132  	}{
   133  		Version: ctx.Version,
   134  		Tag:     ctx.Git.CurrentTag,
   135  		Env:     ctx.Env,
   136  		Major:   sv.Major(),
   137  		Minor:   sv.Minor(),
   138  		Patch:   sv.Patch(),
   139  	}
   140  	err = t.Execute(&out, data)
   141  	return out.String(), err
   142  }
   143  
   144  func process(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) error {
   145  	var root = filepath.Dir(artifact.Path)
   146  	var dockerfile = filepath.Join(root, filepath.Base(docker.Dockerfile))
   147  	var images []string
   148  	for _, tagTemplate := range docker.TagTemplates {
   149  		tag, err := tagName(ctx, tagTemplate)
   150  		if err != nil {
   151  			return errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate)
   152  		}
   153  		images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag))
   154  	}
   155  	if err := os.Link(docker.Dockerfile, dockerfile); err != nil {
   156  		return errors.Wrap(err, "failed to link dockerfile")
   157  	}
   158  	for _, file := range docker.Files {
   159  		if err := link(file, filepath.Join(root, filepath.Base(file))); err != nil {
   160  			return errors.Wrapf(err, "failed to link extra file '%s'", file)
   161  		}
   162  	}
   163  	if err := dockerBuild(ctx, root, dockerfile, images[0]); err != nil {
   164  		return err
   165  	}
   166  	for _, img := range images[1:] {
   167  		if err := dockerTag(ctx, images[0], img); err != nil {
   168  			return err
   169  		}
   170  	}
   171  	return publish(ctx, docker, images)
   172  }
   173  
   174  // walks the src, recreating dirs and hard-linking files
   175  func link(src, dest string) error {
   176  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   177  		if err != nil {
   178  			return err
   179  		}
   180  		// We have the following:
   181  		// - src = "a/b"
   182  		// - dest = "dist/linuxamd64/b"
   183  		// - path = "a/b/c.txt"
   184  		// So we join "a/b" with "c.txt" and use it as the destination.
   185  		var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
   186  		log.WithFields(log.Fields{
   187  			"src": path,
   188  			"dst": dst,
   189  		}).Info("extra file")
   190  		if info.IsDir() {
   191  			return os.MkdirAll(dst, info.Mode())
   192  		}
   193  		return os.Link(path, dst)
   194  	})
   195  }
   196  
   197  func publish(ctx *context.Context, docker config.Docker, images []string) error {
   198  	if ctx.SkipPublish {
   199  		// TODO: this should be better handled
   200  		log.Warn(pipeline.ErrSkipPublishEnabled.Error())
   201  		return nil
   202  	}
   203  	if docker.SkipPush {
   204  		// TODO: this should also be better handled
   205  		log.Warn(pipeline.Skip("skip_push is set").Error())
   206  		return nil
   207  	}
   208  	for _, image := range images {
   209  		if err := dockerPush(ctx, docker, image); err != nil {
   210  			return err
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  func dockerBuild(ctx *context.Context, root, dockerfile, image string) error {
   217  	log.WithField("image", image).Info("building docker image")
   218  	/* #nosec */
   219  	var cmd = exec.CommandContext(ctx, "docker", "build", "-f", dockerfile, "-t", image, root)
   220  	log.WithField("cmd", cmd.Args).Debug("running")
   221  	out, err := cmd.CombinedOutput()
   222  	if err != nil {
   223  		return errors.Wrapf(err, "failed to build docker image: \n%s", string(out))
   224  	}
   225  	log.Debugf("docker build output: \n%s", string(out))
   226  	return nil
   227  }
   228  
   229  func dockerTag(ctx *context.Context, image, tag string) error {
   230  	log.WithField("image", image).WithField("tag", tag).Info("tagging docker image")
   231  	/* #nosec */
   232  	var cmd = exec.CommandContext(ctx, "docker", "tag", image, tag)
   233  	log.WithField("cmd", cmd.Args).Debug("running")
   234  	out, err := cmd.CombinedOutput()
   235  	if err != nil {
   236  		return errors.Wrapf(err, "failed to tag docker image: \n%s", string(out))
   237  	}
   238  	log.Debugf("docker tag output: \n%s", string(out))
   239  	return nil
   240  }
   241  
   242  func dockerPush(ctx *context.Context, docker config.Docker, image string) error {
   243  	log.WithField("image", image).Info("pushing docker image")
   244  	/* #nosec */
   245  	var cmd = exec.CommandContext(ctx, "docker", "push", image)
   246  	log.WithField("cmd", cmd.Args).Debug("running")
   247  	out, err := cmd.CombinedOutput()
   248  	if err != nil {
   249  		return errors.Wrapf(err, "failed to push docker image: \n%s", string(out))
   250  	}
   251  	log.Debugf("docker push output: \n%s", string(out))
   252  	ctx.Artifacts.Add(artifact.Artifact{
   253  		Type:   artifact.DockerImage,
   254  		Name:   image,
   255  		Path:   image,
   256  		Goarch: docker.Goarch,
   257  		Goos:   docker.Goos,
   258  		Goarm:  docker.Goarm,
   259  	})
   260  	return nil
   261  }