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