github.com/windmeup/goreleaser@v1.21.95/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/windmeup/goreleaser/internal/artifact" 15 "github.com/windmeup/goreleaser/internal/gio" 16 "github.com/windmeup/goreleaser/internal/ids" 17 "github.com/windmeup/goreleaser/internal/pipe" 18 "github.com/windmeup/goreleaser/internal/semerrgroup" 19 "github.com/windmeup/goreleaser/internal/skips" 20 "github.com/windmeup/goreleaser/internal/tmpl" 21 "github.com/windmeup/goreleaser/pkg/config" 22 "github.com/windmeup/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 log.WithField("artifacts", artifacts.Paths()).Debug("found artifacts") 146 return process(ctx, docker, artifacts.List()) 147 }) 148 } 149 if err := g.Wait(); err != nil { 150 if pipe.IsSkip(err) { 151 return err 152 } 153 return fmt.Errorf("docker build failed: %w\nLearn more at https://goreleaser.com/errors/docker-build\n", err) // nolint:revive 154 } 155 return nil 156 } 157 158 func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.Artifact) error { 159 if len(artifacts) == 0 { 160 log.Warn("no binaries or packages found for the given platform - COPY/ADD may not work") 161 } 162 tmp, err := os.MkdirTemp("", "goreleaserdocker") 163 if err != nil { 164 return fmt.Errorf("failed to create temporary dir: %w", err) 165 } 166 defer os.RemoveAll(tmp) 167 168 images, err := processImageTemplates(ctx, docker) 169 if err != nil { 170 return err 171 } 172 173 if len(images) == 0 { 174 return pipe.Skip("no image templates found") 175 } 176 177 log := log.WithField("image", images[0]) 178 log.Debug("tempdir: " + tmp) 179 180 if err := tmpl.New(ctx).ApplyAll( 181 &docker.Dockerfile, 182 ); err != nil { 183 return err 184 } 185 if err := gio.Copy( 186 docker.Dockerfile, 187 filepath.Join(tmp, "Dockerfile"), 188 ); err != nil { 189 return fmt.Errorf("failed to copy dockerfile: %w", err) 190 } 191 192 for _, file := range docker.Files { 193 if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(file)), 0o755); err != nil { 194 return fmt.Errorf("failed to copy extra file '%s': %w", file, err) 195 } 196 if err := gio.Copy(file, filepath.Join(tmp, file)); err != nil { 197 return fmt.Errorf("failed to copy extra file '%s': %w", file, err) 198 } 199 } 200 for _, art := range artifacts { 201 target := filepath.Join(tmp, art.Name) 202 if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { 203 return fmt.Errorf("failed to make dir for artifact: %w", err) 204 } 205 206 if err := gio.Copy(art.Path, target); err != nil { 207 return fmt.Errorf("failed to copy artifact: %w", err) 208 } 209 } 210 211 buildFlags, err := processBuildFlagTemplates(ctx, docker) 212 if err != nil { 213 return err 214 } 215 216 log.Info("building docker image") 217 if err := imagers[docker.Use].Build(ctx, tmp, images, buildFlags); err != nil { 218 if isFileNotFoundError(err.Error()) { 219 var files []string 220 _ = filepath.Walk(tmp, func(_ string, info fs.FileInfo, _ error) error { 221 if info.IsDir() { 222 return nil 223 } 224 files = append(files, info.Name()) 225 return nil 226 }) 227 return fmt.Errorf(`seems like you tried to copy a file that is not available in the build context. 228 229 Here's more information about the build context: 230 231 dir: %q 232 files in that dir: 233 %s 234 235 Previous error: 236 %w`, tmp, strings.Join(files, "\n "), err) 237 } 238 if isBuildxContextError(err.Error()) { 239 return fmt.Errorf("docker buildx is not set to default context - please switch with 'docker context use default'") 240 } 241 return err 242 } 243 244 for _, img := range images { 245 ctx.Artifacts.Add(&artifact.Artifact{ 246 Type: artifact.PublishableDockerImage, 247 Name: img, 248 Path: img, 249 Goarch: docker.Goarch, 250 Goos: docker.Goos, 251 Goarm: docker.Goarm, 252 Extra: map[string]interface{}{ 253 dockerConfigExtra: docker, 254 }, 255 }) 256 } 257 return nil 258 } 259 260 func isFileNotFoundError(out string) bool { 261 if strings.Contains(out, `executable file not found in $PATH`) { 262 return false 263 } 264 return strings.Contains(out, "file not found") || 265 strings.Contains(out, ": not found") 266 } 267 268 func isBuildxContextError(out string) bool { 269 return strings.Contains(out, "to switch to context") 270 } 271 272 func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { 273 // nolint:prealloc 274 var images []string 275 for _, imageTemplate := range docker.ImageTemplates { 276 image, err := tmpl.New(ctx).Apply(imageTemplate) 277 if err != nil { 278 return nil, fmt.Errorf("failed to execute image template '%s': %w", imageTemplate, err) 279 } 280 if image == "" { 281 continue 282 } 283 284 images = append(images, image) 285 } 286 287 return images, nil 288 } 289 290 func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]string, error) { 291 // nolint:prealloc 292 var buildFlags []string 293 for _, buildFlagTemplate := range docker.BuildFlagTemplates { 294 buildFlag, err := tmpl.New(ctx).Apply(buildFlagTemplate) 295 if err != nil { 296 return nil, fmt.Errorf("failed to process build flag template '%s': %w", buildFlagTemplate, err) 297 } 298 buildFlags = append(buildFlags, buildFlag) 299 } 300 return buildFlags, nil 301 } 302 303 func dockerPush(ctx *context.Context, image *artifact.Artifact) error { 304 log.WithField("image", image.Name).Info("pushing") 305 306 docker, err := artifact.Extra[config.Docker](*image, dockerConfigExtra) 307 if err != nil { 308 return err 309 } 310 311 skip, err := tmpl.New(ctx).Apply(docker.SkipPush) 312 if err != nil { 313 return err 314 } 315 if strings.TrimSpace(skip) == "true" { 316 return pipe.Skip("docker.skip_push is set: " + image.Name) 317 } 318 if strings.TrimSpace(skip) == "auto" && ctx.Semver.Prerelease != "" { 319 return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish: " + image.Name) 320 } 321 322 digest, err := doPush(ctx, imagers[docker.Use], image.Name, docker.PushFlags) 323 if err != nil { 324 return err 325 } 326 327 art := &artifact.Artifact{ 328 Type: artifact.DockerImage, 329 Name: image.Name, 330 Path: image.Path, 331 Goarch: image.Goarch, 332 Goos: image.Goos, 333 Goarm: image.Goarm, 334 Extra: map[string]interface{}{}, 335 } 336 if docker.ID != "" { 337 art.Extra[artifact.ExtraID] = docker.ID 338 } 339 art.Extra[artifact.ExtraDigest] = digest 340 341 ctx.Artifacts.Add(art) 342 return nil 343 } 344 345 func doPush(ctx *context.Context, img imager, name string, flags []string) (string, error) { 346 var try int 347 for try < 10 { 348 digest, err := img.Push(ctx, name, flags) 349 if err == nil { 350 return digest, nil 351 } 352 if isRetryable(err) { 353 log.WithField("try", try). 354 WithField("image", name). 355 WithError(err). 356 Warnf("failed to push image, will retry") 357 time.Sleep(time.Duration(try*10) * time.Second) 358 try++ 359 continue 360 } 361 return "", fmt.Errorf("failed to push %s after %d tries: %w", name, try, err) 362 } 363 return "", nil // will never happen 364 } 365 366 func isRetryable(err error) bool { 367 for _, code := range []int{ 368 http.StatusInternalServerError, 369 // http.StatusNotImplemented, 370 http.StatusBadGateway, 371 http.StatusServiceUnavailable, 372 http.StatusGatewayTimeout, 373 // http.StatusHTTPVersionNotSupported, 374 http.StatusVariantAlsoNegotiates, 375 // http.StatusInsufficientStorage, 376 // http.StatusLoopDetected, 377 http.StatusNotExtended, 378 // http.StatusNetworkAuthenticationRequired, 379 } { 380 if strings.Contains( 381 err.Error(), 382 fmt.Sprintf( 383 "received unexpected HTTP status: %d %s", 384 code, 385 http.StatusText(code), 386 ), 387 ) { 388 return true 389 } 390 } 391 return false 392 }