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