github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/builder/builder-next/builder.go (about) 1 package buildkit 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/containerd/containerd/platforms" 14 "github.com/containerd/containerd/remotes/docker" 15 "github.com/docker/docker/api/types" 16 "github.com/docker/docker/api/types/backend" 17 timetypes "github.com/docker/docker/api/types/time" 18 "github.com/docker/docker/builder" 19 "github.com/docker/docker/builder/builder-next/exporter" 20 "github.com/docker/docker/builder/builder-next/exporter/mobyexporter" 21 "github.com/docker/docker/builder/builder-next/exporter/overrides" 22 "github.com/docker/docker/daemon/config" 23 "github.com/docker/docker/daemon/images" 24 "github.com/docker/docker/libnetwork" 25 "github.com/docker/docker/opts" 26 "github.com/docker/docker/pkg/idtools" 27 "github.com/docker/docker/pkg/streamformatter" 28 "github.com/docker/go-units" 29 controlapi "github.com/moby/buildkit/api/services/control" 30 "github.com/moby/buildkit/client" 31 "github.com/moby/buildkit/control" 32 "github.com/moby/buildkit/identity" 33 "github.com/moby/buildkit/session" 34 "github.com/moby/buildkit/util/entitlements" 35 "github.com/moby/buildkit/util/tracing" 36 "github.com/pkg/errors" 37 "golang.org/x/sync/errgroup" 38 "google.golang.org/grpc" 39 grpcmetadata "google.golang.org/grpc/metadata" 40 ) 41 42 type errMultipleFilterValues struct{} 43 44 func (errMultipleFilterValues) Error() string { return "filters expect only one value" } 45 46 func (errMultipleFilterValues) InvalidParameter() {} 47 48 type errConflictFilter struct { 49 a, b string 50 } 51 52 func (e errConflictFilter) Error() string { 53 return fmt.Sprintf("conflicting filters: %q and %q", e.a, e.b) 54 } 55 56 func (errConflictFilter) InvalidParameter() {} 57 58 type errInvalidFilterValue struct { 59 error 60 } 61 62 func (errInvalidFilterValue) InvalidParameter() {} 63 64 var cacheFields = map[string]bool{ 65 "id": true, 66 "parent": true, 67 "type": true, 68 "description": true, 69 "inuse": true, 70 "shared": true, 71 "private": true, 72 // fields from buildkit that are not exposed 73 "mutable": false, 74 "immutable": false, 75 } 76 77 // Opt is option struct required for creating the builder 78 type Opt struct { 79 SessionManager *session.Manager 80 Root string 81 EngineID string 82 Dist images.DistributionServices 83 ImageTagger mobyexporter.ImageTagger 84 NetworkController *libnetwork.Controller 85 DefaultCgroupParent string 86 RegistryHosts docker.RegistryHosts 87 BuilderConfig config.BuilderConfig 88 Rootless bool 89 IdentityMapping idtools.IdentityMapping 90 DNSConfig config.DNSConfig 91 ApparmorProfile string 92 UseSnapshotter bool 93 Snapshotter string 94 ContainerdAddress string 95 ContainerdNamespace string 96 } 97 98 // Builder can build using BuildKit backend 99 type Builder struct { 100 controller *control.Controller 101 dnsconfig config.DNSConfig 102 reqBodyHandler *reqBodyHandler 103 104 mu sync.Mutex 105 jobs map[string]*buildJob 106 useSnapshotter bool 107 } 108 109 // New creates a new builder 110 func New(ctx context.Context, opt Opt) (*Builder, error) { 111 reqHandler := newReqBodyHandler(tracing.DefaultTransport) 112 113 c, err := newController(ctx, reqHandler, opt) 114 if err != nil { 115 return nil, err 116 } 117 b := &Builder{ 118 controller: c, 119 dnsconfig: opt.DNSConfig, 120 reqBodyHandler: reqHandler, 121 jobs: map[string]*buildJob{}, 122 useSnapshotter: opt.UseSnapshotter, 123 } 124 return b, nil 125 } 126 127 func (b *Builder) Close() error { 128 return b.controller.Close() 129 } 130 131 // RegisterGRPC registers controller to the grpc server. 132 func (b *Builder) RegisterGRPC(s *grpc.Server) { 133 b.controller.Register(s) 134 } 135 136 // Cancel cancels a build using ID 137 func (b *Builder) Cancel(ctx context.Context, id string) error { 138 b.mu.Lock() 139 if j, ok := b.jobs[id]; ok && j.cancel != nil { 140 j.cancel() 141 } 142 b.mu.Unlock() 143 return nil 144 } 145 146 // DiskUsage returns a report about space used by build cache 147 func (b *Builder) DiskUsage(ctx context.Context) ([]*types.BuildCache, error) { 148 duResp, err := b.controller.DiskUsage(ctx, &controlapi.DiskUsageRequest{}) 149 if err != nil { 150 return nil, err 151 } 152 153 var items []*types.BuildCache 154 for _, r := range duResp.Record { 155 items = append(items, &types.BuildCache{ 156 ID: r.ID, 157 Parent: r.Parent, //nolint:staticcheck // ignore SA1019 (Parent field is deprecated) 158 Parents: r.Parents, 159 Type: r.RecordType, 160 Description: r.Description, 161 InUse: r.InUse, 162 Shared: r.Shared, 163 Size: r.Size_, 164 CreatedAt: r.CreatedAt, 165 LastUsedAt: r.LastUsedAt, 166 UsageCount: int(r.UsageCount), 167 }) 168 } 169 return items, nil 170 } 171 172 // Prune clears all reclaimable build cache 173 func (b *Builder) Prune(ctx context.Context, opts types.BuildCachePruneOptions) (int64, []string, error) { 174 ch := make(chan *controlapi.UsageRecord) 175 176 eg, ctx := errgroup.WithContext(ctx) 177 178 validFilters := make(map[string]bool, 1+len(cacheFields)) 179 validFilters["unused-for"] = true 180 validFilters["until"] = true 181 validFilters["label"] = true // TODO(tiborvass): handle label 182 validFilters["label!"] = true // TODO(tiborvass): handle label! 183 for k, v := range cacheFields { 184 validFilters[k] = v 185 } 186 if err := opts.Filters.Validate(validFilters); err != nil { 187 return 0, nil, err 188 } 189 190 pi, err := toBuildkitPruneInfo(opts) 191 if err != nil { 192 return 0, nil, err 193 } 194 195 eg.Go(func() error { 196 defer close(ch) 197 return b.controller.Prune(&controlapi.PruneRequest{ 198 All: pi.All, 199 KeepDuration: int64(pi.KeepDuration), 200 KeepBytes: pi.KeepBytes, 201 Filter: pi.Filter, 202 }, &pruneProxy{ 203 streamProxy: streamProxy{ctx: ctx}, 204 ch: ch, 205 }) 206 }) 207 208 var size int64 209 var cacheIDs []string 210 eg.Go(func() error { 211 for r := range ch { 212 size += r.Size_ 213 cacheIDs = append(cacheIDs, r.ID) 214 } 215 return nil 216 }) 217 218 if err := eg.Wait(); err != nil { 219 return 0, nil, err 220 } 221 222 return size, cacheIDs, nil 223 } 224 225 // Build executes a build request 226 func (b *Builder) Build(ctx context.Context, opt backend.BuildConfig) (*builder.Result, error) { 227 if len(opt.Options.Outputs) > 1 { 228 return nil, errors.Errorf("multiple outputs not supported") 229 } 230 231 rc := opt.Source 232 if buildID := opt.Options.BuildID; buildID != "" { 233 b.mu.Lock() 234 235 upload := false 236 if strings.HasPrefix(buildID, "upload-request:") { 237 upload = true 238 buildID = strings.TrimPrefix(buildID, "upload-request:") 239 } 240 241 if _, ok := b.jobs[buildID]; !ok { 242 b.jobs[buildID] = newBuildJob() 243 } 244 j := b.jobs[buildID] 245 var cancel func() 246 ctx, cancel = context.WithCancel(ctx) 247 j.cancel = cancel 248 b.mu.Unlock() 249 250 if upload { 251 ctx2, cancel := context.WithTimeout(ctx, 5*time.Second) 252 defer cancel() 253 err := j.SetUpload(ctx2, rc) 254 return nil, err 255 } 256 257 if remoteContext := opt.Options.RemoteContext; remoteContext == "upload-request" { 258 ctx2, cancel := context.WithTimeout(ctx, 5*time.Second) 259 defer cancel() 260 var err error 261 rc, err = j.WaitUpload(ctx2) 262 if err != nil { 263 return nil, err 264 } 265 opt.Options.RemoteContext = "" 266 } 267 268 defer func() { 269 b.mu.Lock() 270 delete(b.jobs, buildID) 271 b.mu.Unlock() 272 }() 273 } 274 275 var out builder.Result 276 277 id := identity.NewID() 278 279 frontendAttrs := map[string]string{} 280 281 if opt.Options.Target != "" { 282 frontendAttrs["target"] = opt.Options.Target 283 } 284 285 if opt.Options.Dockerfile != "" && opt.Options.Dockerfile != "." { 286 frontendAttrs["filename"] = opt.Options.Dockerfile 287 } 288 289 if opt.Options.RemoteContext != "" { 290 if opt.Options.RemoteContext != "client-session" { 291 frontendAttrs["context"] = opt.Options.RemoteContext 292 } 293 } else { 294 url, cancel := b.reqBodyHandler.newRequest(rc) 295 defer cancel() 296 frontendAttrs["context"] = url 297 } 298 299 cacheFrom := append([]string{}, opt.Options.CacheFrom...) 300 301 frontendAttrs["cache-from"] = strings.Join(cacheFrom, ",") 302 303 for k, v := range opt.Options.BuildArgs { 304 if v == nil { 305 continue 306 } 307 frontendAttrs["build-arg:"+k] = *v 308 } 309 310 for k, v := range opt.Options.Labels { 311 frontendAttrs["label:"+k] = v 312 } 313 314 if opt.Options.NoCache { 315 frontendAttrs["no-cache"] = "" 316 } 317 318 if opt.Options.PullParent { 319 frontendAttrs["image-resolve-mode"] = "pull" 320 } else { 321 frontendAttrs["image-resolve-mode"] = "default" 322 } 323 324 if opt.Options.Platform != "" { 325 // same as in newBuilder in builder/dockerfile.builder.go 326 // TODO: remove once opt.Options.Platform is of type specs.Platform 327 _, err := platforms.Parse(opt.Options.Platform) 328 if err != nil { 329 return nil, err 330 } 331 frontendAttrs["platform"] = opt.Options.Platform 332 } 333 334 switch opt.Options.NetworkMode { 335 case "host", "none": 336 frontendAttrs["force-network-mode"] = opt.Options.NetworkMode 337 case "", "default": 338 default: 339 return nil, errors.Errorf("network mode %q not supported by buildkit", opt.Options.NetworkMode) 340 } 341 342 extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts, b.dnsconfig.HostGatewayIP) 343 if err != nil { 344 return nil, err 345 } 346 frontendAttrs["add-hosts"] = extraHosts 347 348 if opt.Options.ShmSize > 0 { 349 frontendAttrs["shm-size"] = strconv.FormatInt(opt.Options.ShmSize, 10) 350 } 351 352 ulimits, err := toBuildkitUlimits(opt.Options.Ulimits) 353 if err != nil { 354 return nil, err 355 } else if len(ulimits) > 0 { 356 frontendAttrs["ulimit"] = ulimits 357 } 358 359 exporterName := "" 360 exporterAttrs := map[string]string{} 361 if len(opt.Options.Outputs) == 0 { 362 exporterName = exporter.Moby 363 } else { 364 // cacheonly is a special type for triggering skipping all exporters 365 if opt.Options.Outputs[0].Type != "cacheonly" { 366 exporterName = opt.Options.Outputs[0].Type 367 exporterAttrs = opt.Options.Outputs[0].Attrs 368 } 369 } 370 371 if (exporterName == client.ExporterImage || exporterName == exporter.Moby) && len(opt.Options.Tags) > 0 { 372 nameAttr, err := overrides.SanitizeRepoAndTags(opt.Options.Tags) 373 if err != nil { 374 return nil, err 375 } 376 if exporterAttrs == nil { 377 exporterAttrs = make(map[string]string) 378 } 379 exporterAttrs["name"] = strings.Join(nameAttr, ",") 380 } 381 382 cache := controlapi.CacheOptions{} 383 if inlineCache := opt.Options.BuildArgs["BUILDKIT_INLINE_CACHE"]; inlineCache != nil { 384 if b, err := strconv.ParseBool(*inlineCache); err == nil && b { 385 cache.Exports = append(cache.Exports, &controlapi.CacheOptionsEntry{ 386 Type: "inline", 387 }) 388 } 389 } 390 391 req := &controlapi.SolveRequest{ 392 Ref: id, 393 Exporters: []*controlapi.Exporter{ 394 &controlapi.Exporter{Type: exporterName, Attrs: exporterAttrs}, 395 }, 396 Frontend: "dockerfile.v0", 397 FrontendAttrs: frontendAttrs, 398 Session: opt.Options.SessionID, 399 Cache: cache, 400 } 401 402 if opt.Options.NetworkMode == "host" { 403 req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost) 404 } 405 406 aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output} 407 408 eg, ctx := errgroup.WithContext(ctx) 409 410 eg.Go(func() error { 411 resp, err := b.controller.Solve(ctx, req) 412 if err != nil { 413 return err 414 } 415 if exporterName != exporter.Moby && exporterName != client.ExporterImage { 416 return nil 417 } 418 id, ok := resp.ExporterResponse["containerimage.digest"] 419 if !ok { 420 return errors.Errorf("missing image id") 421 } 422 out.ImageID = id 423 return aux.Emit("moby.image.id", types.BuildResult{ID: id}) 424 }) 425 426 ch := make(chan *controlapi.StatusResponse) 427 428 eg.Go(func() error { 429 defer close(ch) 430 // streamProxy.ctx is not set to ctx because when request is cancelled, 431 // only the build request has to be cancelled, not the status request. 432 stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch} 433 return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream) 434 }) 435 436 eg.Go(func() error { 437 for sr := range ch { 438 dt, err := sr.Marshal() 439 if err != nil { 440 return err 441 } 442 if err := aux.Emit("moby.buildkit.trace", dt); err != nil { 443 return err 444 } 445 } 446 return nil 447 }) 448 449 if err := eg.Wait(); err != nil { 450 return nil, err 451 } 452 453 return &out, nil 454 } 455 456 type streamProxy struct { 457 ctx context.Context 458 } 459 460 func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error { 461 return nil 462 } 463 464 func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error { 465 return nil 466 } 467 468 func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) { 469 } 470 471 func (sp *streamProxy) Context() context.Context { 472 return sp.ctx 473 } 474 475 func (sp *streamProxy) RecvMsg(m interface{}) error { 476 return io.EOF 477 } 478 479 type statusProxy struct { 480 streamProxy 481 ch chan *controlapi.StatusResponse 482 } 483 484 func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error { 485 return sp.SendMsg(resp) 486 } 487 488 func (sp *statusProxy) SendMsg(m interface{}) error { 489 if sr, ok := m.(*controlapi.StatusResponse); ok { 490 sp.ch <- sr 491 } 492 return nil 493 } 494 495 type pruneProxy struct { 496 streamProxy 497 ch chan *controlapi.UsageRecord 498 } 499 500 func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error { 501 return sp.SendMsg(resp) 502 } 503 504 func (sp *pruneProxy) SendMsg(m interface{}) error { 505 if sr, ok := m.(*controlapi.UsageRecord); ok { 506 sp.ch <- sr 507 } 508 return nil 509 } 510 511 type wrapRC struct { 512 io.ReadCloser 513 once sync.Once 514 err error 515 waitCh chan struct{} 516 } 517 518 func (w *wrapRC) Read(b []byte) (int, error) { 519 n, err := w.ReadCloser.Read(b) 520 if err != nil { 521 e := err 522 if e == io.EOF { 523 e = nil 524 } 525 w.close(e) 526 } 527 return n, err 528 } 529 530 func (w *wrapRC) Close() error { 531 err := w.ReadCloser.Close() 532 w.close(err) 533 return err 534 } 535 536 func (w *wrapRC) close(err error) { 537 w.once.Do(func() { 538 w.err = err 539 close(w.waitCh) 540 }) 541 } 542 543 func (w *wrapRC) wait() error { 544 <-w.waitCh 545 return w.err 546 } 547 548 type buildJob struct { 549 cancel func() 550 waitCh chan func(io.ReadCloser) error 551 } 552 553 func newBuildJob() *buildJob { 554 return &buildJob{waitCh: make(chan func(io.ReadCloser) error)} 555 } 556 557 func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) { 558 done := make(chan struct{}) 559 560 var upload io.ReadCloser 561 fn := func(rc io.ReadCloser) error { 562 w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})} 563 upload = w 564 close(done) 565 return w.wait() 566 } 567 568 select { 569 case <-ctx.Done(): 570 return nil, ctx.Err() 571 case j.waitCh <- fn: 572 <-done 573 return upload, nil 574 } 575 } 576 577 func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error { 578 select { 579 case <-ctx.Done(): 580 return ctx.Err() 581 case fn := <-j.waitCh: 582 return fn(rc) 583 } 584 } 585 586 // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format 587 func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) { 588 if len(inp) == 0 { 589 return "", nil 590 } 591 hosts := make([]string, 0, len(inp)) 592 for _, h := range inp { 593 host, ip, ok := strings.Cut(h, ":") 594 if !ok || host == "" || ip == "" { 595 return "", errors.Errorf("invalid host %s", h) 596 } 597 // If the IP Address is a "host-gateway", replace this value with the 598 // IP address stored in the daemon level HostGatewayIP config variable. 599 if ip == opts.HostGatewayName { 600 gateway := hostGatewayIP.String() 601 if gateway == "" { 602 return "", fmt.Errorf("unable to derive the IP value for host-gateway") 603 } 604 ip = gateway 605 } else if net.ParseIP(ip) == nil { 606 return "", fmt.Errorf("invalid host %s", h) 607 } 608 hosts = append(hosts, host+"="+ip) 609 } 610 return strings.Join(hosts, ","), nil 611 } 612 613 // toBuildkitUlimits converts ulimits from docker type=soft:hard format to buildkit's csv format 614 func toBuildkitUlimits(inp []*units.Ulimit) (string, error) { 615 if len(inp) == 0 { 616 return "", nil 617 } 618 ulimits := make([]string, 0, len(inp)) 619 for _, ulimit := range inp { 620 ulimits = append(ulimits, ulimit.String()) 621 } 622 return strings.Join(ulimits, ","), nil 623 } 624 625 func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) { 626 var until time.Duration 627 untilValues := opts.Filters.Get("until") // canonical 628 unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter 629 630 if len(untilValues) > 0 && len(unusedForValues) > 0 { 631 return client.PruneInfo{}, errConflictFilter{"until", "unused-for"} 632 } 633 filterKey := "until" 634 if len(unusedForValues) > 0 { 635 filterKey = "unused-for" 636 } 637 untilValues = append(untilValues, unusedForValues...) 638 639 switch len(untilValues) { 640 case 0: 641 // nothing to do 642 case 1: 643 ts, err := timetypes.GetTimestamp(untilValues[0], time.Now()) 644 if err != nil { 645 return client.PruneInfo{}, errInvalidFilterValue{ 646 errors.Wrapf(err, "%q filter expects a duration (e.g., '24h') or a timestamp", filterKey), 647 } 648 } 649 seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) 650 if err != nil { 651 return client.PruneInfo{}, errInvalidFilterValue{ 652 errors.Wrapf(err, "failed to parse timestamp %q", ts), 653 } 654 } 655 656 until = time.Since(time.Unix(seconds, nanoseconds)) 657 default: 658 return client.PruneInfo{}, errMultipleFilterValues{} 659 } 660 661 bkFilter := make([]string, 0, opts.Filters.Len()) 662 for cacheField := range cacheFields { 663 if opts.Filters.Contains(cacheField) { 664 values := opts.Filters.Get(cacheField) 665 switch len(values) { 666 case 0: 667 bkFilter = append(bkFilter, cacheField) 668 case 1: 669 if cacheField == "id" { 670 bkFilter = append(bkFilter, cacheField+"~="+values[0]) 671 } else { 672 bkFilter = append(bkFilter, cacheField+"=="+values[0]) 673 } 674 default: 675 return client.PruneInfo{}, errMultipleFilterValues{} 676 } 677 } 678 } 679 return client.PruneInfo{ 680 All: opts.All, 681 KeepDuration: until, 682 KeepBytes: opts.KeepStorage, 683 Filter: []string{strings.Join(bkFilter, ",")}, 684 }, nil 685 }