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