github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/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/Prakhar-Agarwal-byte/moby/api/types" 16 "github.com/Prakhar-Agarwal-byte/moby/api/types/backend" 17 timetypes "github.com/Prakhar-Agarwal-byte/moby/api/types/time" 18 "github.com/Prakhar-Agarwal-byte/moby/builder" 19 "github.com/Prakhar-Agarwal-byte/moby/builder/builder-next/exporter" 20 "github.com/Prakhar-Agarwal-byte/moby/builder/builder-next/exporter/mobyexporter" 21 "github.com/Prakhar-Agarwal-byte/moby/builder/builder-next/exporter/overrides" 22 "github.com/Prakhar-Agarwal-byte/moby/daemon/config" 23 "github.com/Prakhar-Agarwal-byte/moby/daemon/images" 24 "github.com/Prakhar-Agarwal-byte/moby/libnetwork" 25 "github.com/Prakhar-Agarwal-byte/moby/opts" 26 "github.com/Prakhar-Agarwal-byte/moby/pkg/idtools" 27 "github.com/Prakhar-Agarwal-byte/moby/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 Exporter: exporterName, 394 ExporterAttrs: exporterAttrs, 395 Frontend: "dockerfile.v0", 396 FrontendAttrs: frontendAttrs, 397 Session: opt.Options.SessionID, 398 Cache: cache, 399 } 400 401 if opt.Options.NetworkMode == "host" { 402 req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost) 403 } 404 405 aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output} 406 407 eg, ctx := errgroup.WithContext(ctx) 408 409 eg.Go(func() error { 410 resp, err := b.controller.Solve(ctx, req) 411 if err != nil { 412 return err 413 } 414 if exporterName != exporter.Moby && exporterName != client.ExporterImage { 415 return nil 416 } 417 id, ok := resp.ExporterResponse["containerimage.digest"] 418 if !ok { 419 return errors.Errorf("missing image id") 420 } 421 out.ImageID = id 422 return aux.Emit("moby.image.id", types.BuildResult{ID: id}) 423 }) 424 425 ch := make(chan *controlapi.StatusResponse) 426 427 eg.Go(func() error { 428 defer close(ch) 429 // streamProxy.ctx is not set to ctx because when request is cancelled, 430 // only the build request has to be cancelled, not the status request. 431 stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch} 432 return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream) 433 }) 434 435 eg.Go(func() error { 436 for sr := range ch { 437 dt, err := sr.Marshal() 438 if err != nil { 439 return err 440 } 441 if err := aux.Emit("moby.buildkit.trace", dt); err != nil { 442 return err 443 } 444 } 445 return nil 446 }) 447 448 if err := eg.Wait(); err != nil { 449 return nil, err 450 } 451 452 return &out, nil 453 } 454 455 type streamProxy struct { 456 ctx context.Context 457 } 458 459 func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error { 460 return nil 461 } 462 463 func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error { 464 return nil 465 } 466 467 func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) { 468 } 469 470 func (sp *streamProxy) Context() context.Context { 471 return sp.ctx 472 } 473 474 func (sp *streamProxy) RecvMsg(m interface{}) error { 475 return io.EOF 476 } 477 478 type statusProxy struct { 479 streamProxy 480 ch chan *controlapi.StatusResponse 481 } 482 483 func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error { 484 return sp.SendMsg(resp) 485 } 486 487 func (sp *statusProxy) SendMsg(m interface{}) error { 488 if sr, ok := m.(*controlapi.StatusResponse); ok { 489 sp.ch <- sr 490 } 491 return nil 492 } 493 494 type pruneProxy struct { 495 streamProxy 496 ch chan *controlapi.UsageRecord 497 } 498 499 func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error { 500 return sp.SendMsg(resp) 501 } 502 503 func (sp *pruneProxy) SendMsg(m interface{}) error { 504 if sr, ok := m.(*controlapi.UsageRecord); ok { 505 sp.ch <- sr 506 } 507 return nil 508 } 509 510 type wrapRC struct { 511 io.ReadCloser 512 once sync.Once 513 err error 514 waitCh chan struct{} 515 } 516 517 func (w *wrapRC) Read(b []byte) (int, error) { 518 n, err := w.ReadCloser.Read(b) 519 if err != nil { 520 e := err 521 if e == io.EOF { 522 e = nil 523 } 524 w.close(e) 525 } 526 return n, err 527 } 528 529 func (w *wrapRC) Close() error { 530 err := w.ReadCloser.Close() 531 w.close(err) 532 return err 533 } 534 535 func (w *wrapRC) close(err error) { 536 w.once.Do(func() { 537 w.err = err 538 close(w.waitCh) 539 }) 540 } 541 542 func (w *wrapRC) wait() error { 543 <-w.waitCh 544 return w.err 545 } 546 547 type buildJob struct { 548 cancel func() 549 waitCh chan func(io.ReadCloser) error 550 } 551 552 func newBuildJob() *buildJob { 553 return &buildJob{waitCh: make(chan func(io.ReadCloser) error)} 554 } 555 556 func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) { 557 done := make(chan struct{}) 558 559 var upload io.ReadCloser 560 fn := func(rc io.ReadCloser) error { 561 w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})} 562 upload = w 563 close(done) 564 return w.wait() 565 } 566 567 select { 568 case <-ctx.Done(): 569 return nil, ctx.Err() 570 case j.waitCh <- fn: 571 <-done 572 return upload, nil 573 } 574 } 575 576 func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error { 577 select { 578 case <-ctx.Done(): 579 return ctx.Err() 580 case fn := <-j.waitCh: 581 return fn(rc) 582 } 583 } 584 585 // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format 586 func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) { 587 if len(inp) == 0 { 588 return "", nil 589 } 590 hosts := make([]string, 0, len(inp)) 591 for _, h := range inp { 592 host, ip, ok := strings.Cut(h, ":") 593 if !ok || host == "" || ip == "" { 594 return "", errors.Errorf("invalid host %s", h) 595 } 596 // If the IP Address is a "host-gateway", replace this value with the 597 // IP address stored in the daemon level HostGatewayIP config variable. 598 if ip == opts.HostGatewayName { 599 gateway := hostGatewayIP.String() 600 if gateway == "" { 601 return "", fmt.Errorf("unable to derive the IP value for host-gateway") 602 } 603 ip = gateway 604 } else if net.ParseIP(ip) == nil { 605 return "", fmt.Errorf("invalid host %s", h) 606 } 607 hosts = append(hosts, host+"="+ip) 608 } 609 return strings.Join(hosts, ","), nil 610 } 611 612 // toBuildkitUlimits converts ulimits from docker type=soft:hard format to buildkit's csv format 613 func toBuildkitUlimits(inp []*units.Ulimit) (string, error) { 614 if len(inp) == 0 { 615 return "", nil 616 } 617 ulimits := make([]string, 0, len(inp)) 618 for _, ulimit := range inp { 619 ulimits = append(ulimits, ulimit.String()) 620 } 621 return strings.Join(ulimits, ","), nil 622 } 623 624 func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) { 625 var until time.Duration 626 untilValues := opts.Filters.Get("until") // canonical 627 unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter 628 629 if len(untilValues) > 0 && len(unusedForValues) > 0 { 630 return client.PruneInfo{}, errConflictFilter{"until", "unused-for"} 631 } 632 filterKey := "until" 633 if len(unusedForValues) > 0 { 634 filterKey = "unused-for" 635 } 636 untilValues = append(untilValues, unusedForValues...) 637 638 switch len(untilValues) { 639 case 0: 640 // nothing to do 641 case 1: 642 ts, err := timetypes.GetTimestamp(untilValues[0], time.Now()) 643 if err != nil { 644 return client.PruneInfo{}, errInvalidFilterValue{ 645 errors.Wrapf(err, "%q filter expects a duration (e.g., '24h') or a timestamp", filterKey), 646 } 647 } 648 seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) 649 if err != nil { 650 return client.PruneInfo{}, errInvalidFilterValue{ 651 errors.Wrapf(err, "failed to parse timestamp %q", ts), 652 } 653 } 654 655 until = time.Since(time.Unix(seconds, nanoseconds)) 656 default: 657 return client.PruneInfo{}, errMultipleFilterValues{} 658 } 659 660 bkFilter := make([]string, 0, opts.Filters.Len()) 661 for cacheField := range cacheFields { 662 if opts.Filters.Contains(cacheField) { 663 values := opts.Filters.Get(cacheField) 664 switch len(values) { 665 case 0: 666 bkFilter = append(bkFilter, cacheField) 667 case 1: 668 if cacheField == "id" { 669 bkFilter = append(bkFilter, cacheField+"~="+values[0]) 670 } else { 671 bkFilter = append(bkFilter, cacheField+"=="+values[0]) 672 } 673 default: 674 return client.PruneInfo{}, errMultipleFilterValues{} 675 } 676 } 677 } 678 return client.PruneInfo{ 679 All: opts.All, 680 KeepDuration: until, 681 KeepBytes: opts.KeepStorage, 682 Filter: []string{strings.Join(bkFilter, ",")}, 683 }, nil 684 }