github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/docker/docker.go (about) 1 package docker 2 3 import ( 4 "fmt" 5 "io/fs" 6 "net/http" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/caarlos0/log" 14 "github.com/goreleaser/goreleaser/internal/artifact" 15 "github.com/goreleaser/goreleaser/internal/gio" 16 "github.com/goreleaser/goreleaser/internal/ids" 17 "github.com/goreleaser/goreleaser/internal/pipe" 18 "github.com/goreleaser/goreleaser/internal/semerrgroup" 19 "github.com/goreleaser/goreleaser/internal/skips" 20 "github.com/goreleaser/goreleaser/internal/tmpl" 21 "github.com/goreleaser/goreleaser/pkg/config" 22 "github.com/goreleaser/goreleaser/pkg/context" 23 ) 24 25 const ( 26 dockerConfigExtra = "DockerConfig" 27 28 useBuildx = "buildx" 29 useDocker = "docker" 30 ) 31 32 // Pipe for docker. 33 type Pipe struct{} 34 35 func (Pipe) String() string { return "docker images" } 36 37 func (Pipe) Skip(ctx *context.Context) bool { 38 return len(ctx.Config.Dockers) == 0 || skips.Any(ctx, skips.Docker) 39 } 40 41 func (Pipe) Dependencies(ctx *context.Context) []string { 42 var cmds []string 43 for _, s := range ctx.Config.Dockers { 44 switch s.Use { 45 case useDocker, useBuildx: 46 cmds = append(cmds, "docker") 47 // TODO: how to check if buildx is installed 48 } 49 } 50 return cmds 51 } 52 53 // Default sets the pipe defaults. 54 func (Pipe) Default(ctx *context.Context) error { 55 ids := ids.New("dockers") 56 for i := range ctx.Config.Dockers { 57 docker := &ctx.Config.Dockers[i] 58 59 if docker.ID != "" { 60 ids.Inc(docker.ID) 61 } 62 if docker.Goos == "" { 63 docker.Goos = "linux" 64 } 65 if docker.Goarch == "" { 66 docker.Goarch = "amd64" 67 } 68 if docker.Goarm == "" { 69 docker.Goarm = "6" 70 } 71 if docker.Goamd64 == "" { 72 docker.Goamd64 = "v1" 73 } 74 if docker.Dockerfile == "" { 75 docker.Dockerfile = "Dockerfile" 76 } 77 if docker.Use == "" { 78 docker.Use = useDocker 79 } 80 if err := validateImager(docker.Use); err != nil { 81 return err 82 } 83 } 84 return ids.Validate() 85 } 86 87 func validateImager(use string) error { 88 valid := make([]string, 0, len(imagers)) 89 for k := range imagers { 90 valid = append(valid, k) 91 } 92 for _, s := range valid { 93 if s == use { 94 return nil 95 } 96 } 97 sort.Strings(valid) 98 return fmt.Errorf("docker: invalid use: %s, valid options are %v", use, valid) 99 } 100 101 // Publish the docker images. 102 func (Pipe) Publish(ctx *context.Context) error { 103 skips := pipe.SkipMemento{} 104 images := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List() 105 for _, image := range images { 106 if err := dockerPush(ctx, image); err != nil { 107 if pipe.IsSkip(err) { 108 skips.Remember(err) 109 continue 110 } 111 return err 112 } 113 } 114 return skips.Evaluate() 115 } 116 117 // Run the pipe. 118 func (Pipe) Run(ctx *context.Context) error { 119 g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism)) 120 for i, docker := range ctx.Config.Dockers { 121 i := i 122 docker := docker 123 g.Go(func() error { 124 log := log.WithField("index", i) 125 log.Debug("looking for artifacts matching") 126 filters := []artifact.Filter{ 127 artifact.ByGoos(docker.Goos), 128 artifact.ByGoarch(docker.Goarch), 129 artifact.Or( 130 artifact.ByType(artifact.Binary), 131 artifact.ByType(artifact.LinuxPackage), 132 ), 133 } 134 // TODO: properly test this 135 switch docker.Goarch { 136 case "amd64": 137 filters = append(filters, artifact.ByGoamd64(docker.Goamd64)) 138 case "arm": 139 filters = append(filters, artifact.ByGoarm(docker.Goarm)) 140 } 141 if len(docker.IDs) > 0 { 142 filters = append(filters, artifact.ByIDs(docker.IDs...)) 143 } 144 artifacts := ctx.Artifacts.Filter(artifact.And(filters...)) 145 if d := len(docker.IDs); d > 0 && len(artifacts.GroupByID()) != d { 146 return pipe.Skipf("expected to find %d artifacts for ids %v, found %d\nLearn more at https://goreleaser.com/errors/docker-build\n", d, docker.IDs, len(artifacts.List())) 147 } 148 log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts") 149 return process(ctx, docker, artifacts.List()) 150 }) 151 } 152 if err := g.Wait(); err != nil { 153 if pipe.IsSkip(err) { 154 return err 155 } 156 return fmt.Errorf("docker build failed: %w\nLearn more at https://goreleaser.com/errors/docker-build\n", err) // nolint:revive 157 } 158 return nil 159 } 160 161 func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.Artifact) error { 162 if len(artifacts) == 0 { 163 log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work") 164 } 165 tmp, err := os.MkdirTemp("", "goreleaserdocker") 166 if err != nil { 167 return fmt.Errorf("failed to create temporary dir: %w", err) 168 } 169 defer os.RemoveAll(tmp) 170 171 images, err := processImageTemplates(ctx, docker) 172 if err != nil { 173 return err 174 } 175 176 if len(images) == 0 { 177 return pipe.Skip("no image templates found") 178 } 179 180 log := log.WithField("image", images[0]) 181 log.Debug("tempdir: " + tmp) 182 183 if err := tmpl.New(ctx).ApplyAll( 184 &docker.Dockerfile, 185 ); err != nil { 186 return err 187 } 188 if err := gio.Copy( 189 docker.Dockerfile, 190 filepath.Join(tmp, "Dockerfile"), 191 ); err != nil { 192 return fmt.Errorf("failed to copy dockerfile: %w", err) 193 } 194 195 for _, file := range docker.Files { 196 if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil { 197 return fmt.Errorf("failed to copy extra file '%s': %w", file, err) 198 } 199 if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil { 200 return fmt.Errorf("failed to copy extra file '%s': %w", file, err) 201 } 202 } 203 for _, art := range artifacts { 204 target := filepath.Join(tmp, art.Name) 205 if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { 206 return fmt.Errorf("failed to make dir for artifact: %w", err) 207 } 208 209 if err := gio.Copy(art.Path, target); err != nil { 210 return fmt.Errorf("failed to copy artifact: %w", err) 211 } 212 } 213 214 buildFlags, err := processBuildFlagTemplates(ctx, docker) 215 if err != nil { 216 return err 217 } 218 219 log.Info("building docker image") 220 if err := imagers[docker.Use].Build(ctx, tmp, images, buildFlags); err != nil { 221 if isFileNotFoundError(err.Error()) { 222 var files []string 223 _ = filepath.Walk(tmp, func(_ string, info fs.FileInfo, _ error) error { 224 if info.IsDir() { 225 return nil 226 } 227 files = append(files, info.Name()) 228 return nil 229 }) 230 return fmt.Errorf(`seems like you tried to copy a file that is not available in the build context. 231 232 Here's more information about the build context: 233 234 dir: %q 235 files in that dir: 236 %s 237 238 Previous error: 239 %w`, tmp, strings.Join(files, "\n "), err) 240 } 241 if isBuildxContextError(err.Error()) { 242 return fmt.Errorf("docker buildx is not set to default context - please switch with 'docker context use default'") 243 } 244 return err 245 } 246 247 for _, img := range images { 248 ctx.Artifacts.Add(&artifact.Artifact{ 249 Type: artifact.PublishableDockerImage, 250 Name: img, 251 Path: img, 252 Goarch: docker.Goarch, 253 Goos: docker.Goos, 254 Goarm: docker.Goarm, 255 Extra: map[string]interface{}{ 256 dockerConfigExtra: docker, 257 }, 258 }) 259 } 260 return nil 261 } 262 263 func isFileNotFoundError(out string) bool { 264 if strings.Contains(out, `executable file not found in $PATH`) { 265 return false 266 } 267 return strings.Contains(out, "file not found") || 268 strings.Contains(out, ": not found") 269 } 270 271 func isBuildxContextError(out string) bool { 272 return strings.Contains(out, "to switch to context") 273 } 274 275 func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { 276 // nolint:prealloc 277 var images []string 278 for _, imageTemplate := range docker.ImageTemplates { 279 image, err := tmpl.New(ctx).Apply(imageTemplate) 280 if err != nil { 281 return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err) 282 } 283 if image == "" { 284 continue 285 } 286 287 images = append(images, image) 288 } 289 290 return images, nil 291 } 292 293 func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { 294 // nolint:prealloc 295 var buildFlags []string 296 for _, buildFlagTemplate := range docker.BuildFlagTemplates { 297 buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate) 298 if err != nil { 299 return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err) 300 } 301 buildFlags = append(buildFlags, buildFlag) 302 } 303 return buildFlags, nil 304 } 305 306 func dockerPush(ctx *context.Context, image *artifact.Artifact) error { 307 log.WithField("image", image.Name).Info("pushing") 308 309 docker, err := artifact.Extra[config.Docker](*image, dockerConfigExtra) 310 if err != nil { 311 return err 312 } 313 314 skip, err := tmpl.New(ctx).Apply(docker.SkipPush) 315 if err != nil { 316 return err 317 } 318 if strings.TrimSpace(skip) == "true" { 319 return pipe.Skip("docker.skip_push is set: " + image.Name) 320 } 321 if strings.TrimSpace(skip) == "auto" && ctx.Semver.Prerelease != "" { 322 return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish: " + image.Name) 323 } 324 325 digest, err := doPush(ctx, imagers[docker.Use], image.Name, docker.PushFlags) 326 if err != nil { 327 return err 328 } 329 330 art := &artifact.Artifact{ 331 Type: artifact.DockerImage, 332 Name: image.Name, 333 Path: image.Path, 334 Goarch: image.Goarch, 335 Goos: image.Goos, 336 Goarm: image.Goarm, 337 Extra: map[string]interface{}{}, 338 } 339 if docker.ID != "" { 340 art.Extra[artifact.ExtraID] = docker.ID 341 } 342 art.Extra[artifact.ExtraDigest] = digest 343 344 ctx.Artifacts.Add(art) 345 return nil 346 } 347 348 func doPush(ctx *context.Context, img imager, name string, flags []string) (string, error) { 349 var try int 350 for try < 10 { 351 digest, err := img.Push(ctx, name, flags) 352 if err == nil { 353 return digest, nil 354 } 355 if isRetryable(err) { 356 log.WithField("try", try). 357 WithField("image", name). 358 WithError(err). 359 Warnf("failed to push image, will retry") 360 time.Sleep(time.Duration(try*10) * time.Second) 361 try++ 362 continue 363 } 364 return "", fmt.Errorf("failed to push %s after %d tries: %w", name, try, err) 365 } 366 return "", nil // will never happen 367 } 368 369 func isRetryable(err error) bool { 370 for _, code := range []int{ 371 http.StatusInternalServerError, 372 // http.StatusNotImplemented, 373 http.StatusBadGateway, 374 http.StatusServiceUnavailable, 375 http.StatusGatewayTimeout, 376 // http.StatusHTTPVersionNotSupported, 377 http.StatusVariantAlsoNegotiates, 378 // http.StatusInsufficientStorage, 379 // http.StatusLoopDetected, 380 http.StatusNotExtended, 381 // http.StatusNetworkAuthenticationRequired, 382 } { 383 if strings.Contains( 384 err.Error(), 385 fmt.Sprintf( 386 "received unexpected HTTP status: %d %s", 387 code, 388 http.StatusText(code), 389 ), 390 ) { 391 return true 392 } 393 } 394 return false 395 }