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