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