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