github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/internal/pipe/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"errors"
     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  )
    21  
    22  // ErrNoDocker is shown when docker cannot be found in $PATH.
    23  var ErrNoDocker = errors.New("docker not present in $PATH")
    24  
    25  // Pipe for docker.
    26  type Pipe struct{}
    27  
    28  func (Pipe) String() string {
    29  	return "docker images"
    30  }
    31  
    32  // Default sets the pipe defaults.
    33  func (Pipe) Default(ctx *context.Context) error {
    34  	for i := range ctx.Config.Dockers {
    35  		var docker = &ctx.Config.Dockers[i]
    36  
    37  		if docker.Goos == "" {
    38  			docker.Goos = "linux"
    39  		}
    40  		if docker.Goarch == "" {
    41  			docker.Goarch = "amd64"
    42  		}
    43  		if docker.Dockerfile == "" {
    44  			docker.Dockerfile = "Dockerfile"
    45  		}
    46  		if len(docker.Binaries) > 0 {
    47  			deprecate.Notice(ctx, "docker.binaries")
    48  		}
    49  		if len(docker.Builds) > 0 {
    50  			deprecate.Notice(ctx, "docker.builds")
    51  			docker.IDs = append(docker.IDs, docker.Builds...)
    52  		}
    53  		for _, f := range docker.Files {
    54  			if f == "." || strings.HasPrefix(f, ctx.Config.Dist) {
    55  				return fmt.Errorf("invalid docker.files: can't be . or inside dist folder: %s", f)
    56  			}
    57  		}
    58  	}
    59  	return nil
    60  }
    61  
    62  // Run the pipe.
    63  func (Pipe) Run(ctx *context.Context) error {
    64  	if len(ctx.Config.Dockers) == 0 || len(ctx.Config.Dockers[0].ImageTemplates) == 0 {
    65  		return pipe.Skip("docker section is not configured")
    66  	}
    67  	_, err := exec.LookPath("docker")
    68  	if err != nil {
    69  		return ErrNoDocker
    70  	}
    71  	return doRun(ctx)
    72  }
    73  
    74  // Publish the docker images.
    75  func (Pipe) Publish(ctx *context.Context) error {
    76  	if ctx.SkipPublish {
    77  		return pipe.ErrSkipPublishEnabled
    78  	}
    79  	var images = ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
    80  	for _, image := range images {
    81  		if err := dockerPush(ctx, image); err != nil {
    82  			return err
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  func doRun(ctx *context.Context) error {
    89  	var g = semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
    90  	for _, docker := range ctx.Config.Dockers {
    91  		docker := docker
    92  		g.Go(func() error {
    93  			log.WithField("docker", docker).Debug("looking for artifacts matching")
    94  			var filters = []artifact.Filter{
    95  				artifact.ByGoos(docker.Goos),
    96  				artifact.ByGoarch(docker.Goarch),
    97  				artifact.ByGoarm(docker.Goarm),
    98  				artifact.Or(
    99  					artifact.ByType(artifact.Binary),
   100  					artifact.ByType(artifact.LinuxPackage),
   101  				),
   102  			}
   103  			if len(docker.IDs) > 0 {
   104  				filters = append(filters, artifact.ByIDs(docker.IDs...))
   105  			}
   106  			var artifacts = ctx.Artifacts.Filter(artifact.And(filters...))
   107  			log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts")
   108  			return process(ctx, docker, artifacts.List())
   109  		})
   110  	}
   111  	return g.Wait()
   112  }
   113  
   114  func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.Artifact) error {
   115  	tmp, err := ioutil.TempDir(ctx.Config.Dist, "goreleaserdocker")
   116  	if err != nil {
   117  		return fmt.Errorf("failed to create temporary dir: %w", err)
   118  	}
   119  	log.Debug("tempdir: " + tmp)
   120  
   121  	images, err := processImageTemplates(ctx, docker)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	if err := os.Link(docker.Dockerfile, filepath.Join(tmp, "Dockerfile")); err != nil {
   127  		return fmt.Errorf("failed to link dockerfile: %w", err)
   128  	}
   129  	for _, file := range docker.Files {
   130  		if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0755); err != nil {
   131  			return fmt.Errorf("failed to link extra file '%s': %w", file, err)
   132  		}
   133  		if err := link(file, filepath.Join(tmp, file)); err != nil {
   134  			return fmt.Errorf("failed to link extra file '%s': %w", file, err)
   135  		}
   136  	}
   137  	for _, art := range artifacts {
   138  		if err := os.Link(art.Path, filepath.Join(tmp, filepath.Base(art.Path))); err != nil {
   139  			return fmt.Errorf("failed to link artifact: %w", err)
   140  		}
   141  	}
   142  
   143  	buildFlags, err := processBuildFlagTemplates(ctx, docker)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	if err := dockerBuild(ctx, tmp, images, buildFlags, docker.Buildx); err != nil {
   149  		return err
   150  	}
   151  
   152  	if strings.TrimSpace(docker.SkipPush) == "true" {
   153  		return pipe.Skip("docker.skip_push is set")
   154  	}
   155  	if ctx.SkipPublish {
   156  		return pipe.ErrSkipPublishEnabled
   157  	}
   158  	if ctx.Config.Release.Draft {
   159  		return pipe.Skip("release is marked as draft")
   160  	}
   161  	if strings.TrimSpace(docker.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
   162  		return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish")
   163  	}
   164  	for _, img := range images {
   165  		ctx.Artifacts.Add(&artifact.Artifact{
   166  			Type:   artifact.PublishableDockerImage,
   167  			Name:   img,
   168  			Path:   img,
   169  			Goarch: docker.Goarch,
   170  			Goos:   docker.Goos,
   171  			Goarm:  docker.Goarm,
   172  		})
   173  	}
   174  	return nil
   175  }
   176  
   177  func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string, error) {
   178  	// nolint:prealloc
   179  	var images []string
   180  	for _, imageTemplate := range docker.ImageTemplates {
   181  		image, err := tmpl.New(ctx).Apply(imageTemplate)
   182  		if err != nil {
   183  			return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err)
   184  		}
   185  		if image == "" {
   186  			continue
   187  		}
   188  
   189  		images = append(images, image)
   190  	}
   191  
   192  	if len(images) == 0 {
   193  		return images, errors.New("no image templates found")
   194  	}
   195  
   196  	return images, nil
   197  }
   198  
   199  func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]string, error) {
   200  	// nolint:prealloc
   201  	var buildFlags []string
   202  	for _, buildFlagTemplate := range docker.BuildFlagTemplates {
   203  		buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate)
   204  		if err != nil {
   205  			return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err)
   206  		}
   207  		buildFlags = append(buildFlags, buildFlag)
   208  	}
   209  	return buildFlags, nil
   210  }
   211  
   212  // walks the src, recreating dirs and hard-linking files.
   213  func link(src, dest string) error {
   214  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
   215  		if err != nil {
   216  			return err
   217  		}
   218  		// We have the following:
   219  		// - src = "a/b"
   220  		// - dest = "dist/linuxamd64/b"
   221  		// - path = "a/b/c.txt"
   222  		// So we join "a/b" with "c.txt" and use it as the destination.
   223  		var dst = filepath.Join(dest, strings.Replace(path, src, "", 1))
   224  		log.WithFields(log.Fields{
   225  			"src": path,
   226  			"dst": dst,
   227  		}).Debug("extra file")
   228  		if info.IsDir() {
   229  			return os.MkdirAll(dst, info.Mode())
   230  		}
   231  		return os.Link(path, dst)
   232  	})
   233  }
   234  
   235  func dockerBuild(ctx *context.Context, root string, images, flags []string, buildx bool) error {
   236  	log.WithField("image", images[0]).WithField("buildx", buildx).Info("building docker image")
   237  	/* #nosec */
   238  	var cmd = exec.CommandContext(ctx, "docker", buildCommand(buildx, images, flags)...)
   239  	cmd.Dir = root
   240  	log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running")
   241  	out, err := cmd.CombinedOutput()
   242  	if err != nil {
   243  		return fmt.Errorf("failed to build docker image: %s: \n%s: %w", images[0], string(out), err)
   244  	}
   245  	log.Debugf("docker build output: \n%s", string(out))
   246  	return nil
   247  }
   248  
   249  func buildCommand(buildx bool, images, flags []string) []string {
   250  	base := []string{"build", "."}
   251  	if buildx {
   252  		base = []string{"buildx", "build", ".", "--load"}
   253  	}
   254  	for _, image := range images {
   255  		base = append(base, "-t", image)
   256  	}
   257  	base = append(base, flags...)
   258  	return base
   259  }
   260  
   261  func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
   262  	log.WithField("image", image.Name).Info("pushing docker image")
   263  	/* #nosec */
   264  	var cmd = exec.CommandContext(ctx, "docker", "push", image.Name)
   265  	log.WithField("cmd", cmd.Args).Debug("running")
   266  	out, err := cmd.CombinedOutput()
   267  	if err != nil {
   268  		return fmt.Errorf("failed to push docker image: \n%s: %w", string(out), err)
   269  	}
   270  	log.Debugf("docker push output: \n%s", string(out))
   271  	ctx.Artifacts.Add(&artifact.Artifact{
   272  		Type:   artifact.DockerImage,
   273  		Name:   image.Name,
   274  		Path:   image.Path,
   275  		Goarch: image.Goarch,
   276  		Goos:   image.Goos,
   277  		Goarm:  image.Goarm,
   278  	})
   279  	return nil
   280  }