github.com/szyn/goreleaser@v0.76.1-0.20180517112710-333da09a1297/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 string
   130  		Tag     string
   131  		Commit  string
   132  		Major   int64
   133  		Minor   int64
   134  		Patch   int64
   135  		Env     map[string]string
   136  	}{
   137  		Version: ctx.Version,
   138  		Commit:  ctx.Git.Commit,
   139  		Tag:     ctx.Git.CurrentTag,
   140  		Env:     ctx.Env,
   141  		Major:   sv.Major(),
   142  		Minor:   sv.Minor(),
   143  		Patch:   sv.Patch(),
   144  	}
   145  	err = t.Execute(&out, data)
   146  	return out.String(), err
   147  }
   148  
   149  func process(ctx *context.Context, docker config.Docker, artifact artifact.Artifact) error {
   150  	var root = filepath.Dir(artifact.Path)
   151  	var dockerfile = filepath.Join(root, filepath.Base(docker.Dockerfile))
   152  	var images []string
   153  	for _, tagTemplate := range docker.TagTemplates {
   154  		tag, err := tagName(ctx, tagTemplate)
   155  		if err != nil {
   156  			return errors.Wrapf(err, "failed to execute tag template '%s'", tagTemplate)
   157  		}
   158  		images = append(images, fmt.Sprintf("%s:%s", docker.Image, tag))
   159  	}
   160  	if err := os.Link(docker.Dockerfile, dockerfile); err != nil {
   161  		return errors.Wrap(err, "failed to link dockerfile")
   162  	}
   163  	for _, file := range docker.Files {
   164  		if err := link(file, filepath.Join(root, filepath.Base(file))); err != nil {
   165  			return errors.Wrapf(err, "failed to link extra file '%s'", file)
   166  		}
   167  	}
   168  	if err := dockerBuild(ctx, root, dockerfile, images[0]); err != nil {
   169  		return err
   170  	}
   171  	for _, img := range images[1:] {
   172  		if err := dockerTag(ctx, images[0], img); err != nil {
   173  			return err
   174  		}
   175  	}
   176  	return publish(ctx, docker, images)
   177  }
   178  
   179  // walks the src, recreating dirs and hard-linking files
   180  func link(src, dest string) error {
   181  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   182  		if err != nil {
   183  			return err
   184  		}
   185  		// We have the following:
   186  		// - src = "a/b"
   187  		// - dest = "dist/linuxamd64/b"
   188  		// - path = "a/b/c.txt"
   189  		// So we join "a/b" with "c.txt" and use it as the destination.
   190  		var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
   191  		log.WithFields(log.Fields{
   192  			"src": path,
   193  			"dst": dst,
   194  		}).Info("extra file")
   195  		if info.IsDir() {
   196  			return os.MkdirAll(dst, info.Mode())
   197  		}
   198  		return os.Link(path, dst)
   199  	})
   200  }
   201  
   202  func publish(ctx *context.Context, docker config.Docker, images []string) error {
   203  	if ctx.SkipPublish {
   204  		// TODO: this should be better handled
   205  		log.Warn(pipeline.ErrSkipPublishEnabled.Error())
   206  		return nil
   207  	}
   208  	if docker.SkipPush {
   209  		// TODO: this should also be better handled
   210  		log.Warn(pipeline.Skip("skip_push is set").Error())
   211  		return nil
   212  	}
   213  	for _, image := range images {
   214  		if err := dockerPush(ctx, docker, image); err != nil {
   215  			return err
   216  		}
   217  	}
   218  	return nil
   219  }
   220  
   221  func dockerBuild(ctx *context.Context, root, dockerfile, image string) error {
   222  	log.WithField("image", image).Info("building docker image")
   223  	/* #nosec */
   224  	var cmd = exec.CommandContext(ctx, "docker", "build", "-f", dockerfile, "-t", image, root)
   225  	log.WithField("cmd", cmd.Args).Debug("running")
   226  	out, err := cmd.CombinedOutput()
   227  	if err != nil {
   228  		return errors.Wrapf(err, "failed to build docker image: \n%s", string(out))
   229  	}
   230  	log.Debugf("docker build output: \n%s", string(out))
   231  	return nil
   232  }
   233  
   234  func dockerTag(ctx *context.Context, image, tag string) error {
   235  	log.WithField("image", image).WithField("tag", tag).Info("tagging docker image")
   236  	/* #nosec */
   237  	var cmd = exec.CommandContext(ctx, "docker", "tag", image, tag)
   238  	log.WithField("cmd", cmd.Args).Debug("running")
   239  	out, err := cmd.CombinedOutput()
   240  	if err != nil {
   241  		return errors.Wrapf(err, "failed to tag docker image: \n%s", string(out))
   242  	}
   243  	log.Debugf("docker tag output: \n%s", string(out))
   244  	return nil
   245  }
   246  
   247  func dockerPush(ctx *context.Context, docker config.Docker, image string) error {
   248  	log.WithField("image", image).Info("pushing docker image")
   249  	/* #nosec */
   250  	var cmd = exec.CommandContext(ctx, "docker", "push", image)
   251  	log.WithField("cmd", cmd.Args).Debug("running")
   252  	out, err := cmd.CombinedOutput()
   253  	if err != nil {
   254  		return errors.Wrapf(err, "failed to push docker image: \n%s", string(out))
   255  	}
   256  	log.Debugf("docker push output: \n%s", string(out))
   257  	ctx.Artifacts.Add(artifact.Artifact{
   258  		Type:   artifact.DockerImage,
   259  		Name:   image,
   260  		Path:   image,
   261  		Goarch: docker.Goarch,
   262  		Goos:   docker.Goos,
   263  		Goarm:  docker.Goarm,
   264  	})
   265  	return nil
   266  }