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