1package buildkit 2 3import ( 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/pkg/idtools" 21 "github.com/docker/docker/pkg/streamformatter" 22 "github.com/docker/docker/pkg/system" 23 "github.com/docker/libnetwork" 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 37type errMultipleFilterValues struct{} 38 39func (errMultipleFilterValues) Error() string { return "filters expect only one value" } 40 41func (errMultipleFilterValues) InvalidParameter() {} 42 43type errConflictFilter struct { 44 a, b string 45} 46 47func (e errConflictFilter) Error() string { 48 return fmt.Sprintf("conflicting filters: %q and %q", e.a, e.b) 49} 50 51func (errConflictFilter) InvalidParameter() {} 52 53var 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 67type 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 82type 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 91func New(opt Opt) (*Builder, error) { 92 reqHandler := newReqBodyHandler(tracing.DefaultTransport) 93 94 if opt.IdentityMapping != nil && opt.IdentityMapping.Empty() { 95 opt.IdentityMapping = nil 96 } 97 98 c, err := newController(reqHandler, opt) 99 if err != nil { 100 return nil, err 101 } 102 b := &Builder{ 103 controller: c, 104 reqBodyHandler: reqHandler, 105 jobs: map[string]*buildJob{}, 106 } 107 return b, nil 108} 109 110// RegisterGRPC registers controller to the grpc server. 111func (b *Builder) RegisterGRPC(s *grpc.Server) { 112 b.controller.Register(s) 113} 114 115// Cancel cancels a build using ID 116func (b *Builder) Cancel(ctx context.Context, id string) error { 117 b.mu.Lock() 118 if j, ok := b.jobs[id]; ok && j.cancel != nil { 119 j.cancel() 120 } 121 b.mu.Unlock() 122 return nil 123} 124 125// DiskUsage returns a report about space used by build cache 126func (b *Builder) DiskUsage(ctx context.Context) ([]*types.BuildCache, error) { 127 duResp, err := b.controller.DiskUsage(ctx, &controlapi.DiskUsageRequest{}) 128 if err != nil { 129 return nil, err 130 } 131 132 var items []*types.BuildCache 133 for _, r := range duResp.Record { 134 items = append(items, &types.BuildCache{ 135 ID: r.ID, 136 Parent: r.Parent, 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 151func (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 204func (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 sp, err := platforms.Parse(opt.Options.Platform) 303 if err != nil { 304 return nil, err 305 } 306 if err := system.ValidatePlatform(sp); err != nil { 307 return nil, err 308 } 309 frontendAttrs["platform"] = opt.Options.Platform 310 } 311 312 switch opt.Options.NetworkMode { 313 case "host", "none": 314 frontendAttrs["force-network-mode"] = opt.Options.NetworkMode 315 case "", "default": 316 default: 317 return nil, errors.Errorf("network mode %q not supported by buildkit", opt.Options.NetworkMode) 318 } 319 320 extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts) 321 if err != nil { 322 return nil, err 323 } 324 frontendAttrs["add-hosts"] = extraHosts 325 326 exporterName := "" 327 exporterAttrs := map[string]string{} 328 329 if len(opt.Options.Outputs) > 1 { 330 return nil, errors.Errorf("multiple outputs not supported") 331 } else if len(opt.Options.Outputs) == 0 { 332 exporterName = "moby" 333 } else { 334 // cacheonly is a special type for triggering skipping all exporters 335 if opt.Options.Outputs[0].Type != "cacheonly" { 336 exporterName = opt.Options.Outputs[0].Type 337 exporterAttrs = opt.Options.Outputs[0].Attrs 338 } 339 } 340 341 if exporterName == "moby" { 342 if len(opt.Options.Tags) > 0 { 343 exporterAttrs["name"] = strings.Join(opt.Options.Tags, ",") 344 } 345 } 346 347 cache := controlapi.CacheOptions{} 348 349 if inlineCache := opt.Options.BuildArgs["BUILDKIT_INLINE_CACHE"]; inlineCache != nil { 350 if b, err := strconv.ParseBool(*inlineCache); err == nil && b { 351 cache.Exports = append(cache.Exports, &controlapi.CacheOptionsEntry{ 352 Type: "inline", 353 }) 354 } 355 } 356 357 req := &controlapi.SolveRequest{ 358 Ref: id, 359 Exporter: exporterName, 360 ExporterAttrs: exporterAttrs, 361 Frontend: "dockerfile.v0", 362 FrontendAttrs: frontendAttrs, 363 Session: opt.Options.SessionID, 364 Cache: cache, 365 } 366 367 if opt.Options.NetworkMode == "host" { 368 req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost) 369 } 370 371 aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output} 372 373 eg, ctx := errgroup.WithContext(ctx) 374 375 eg.Go(func() error { 376 resp, err := b.controller.Solve(ctx, req) 377 if err != nil { 378 return err 379 } 380 if exporterName != "moby" { 381 return nil 382 } 383 id, ok := resp.ExporterResponse["containerimage.digest"] 384 if !ok { 385 return errors.Errorf("missing image id") 386 } 387 out.ImageID = id 388 return aux.Emit("moby.image.id", types.BuildResult{ID: id}) 389 }) 390 391 ch := make(chan *controlapi.StatusResponse) 392 393 eg.Go(func() error { 394 defer close(ch) 395 // streamProxy.ctx is not set to ctx because when request is cancelled, 396 // only the build request has to be cancelled, not the status request. 397 stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch} 398 return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream) 399 }) 400 401 eg.Go(func() error { 402 for sr := range ch { 403 dt, err := sr.Marshal() 404 if err != nil { 405 return err 406 } 407 if err := aux.Emit("moby.buildkit.trace", dt); err != nil { 408 return err 409 } 410 } 411 return nil 412 }) 413 414 if err := eg.Wait(); err != nil { 415 return nil, err 416 } 417 418 return &out, nil 419} 420 421type streamProxy struct { 422 ctx context.Context 423} 424 425func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error { 426 return nil 427} 428 429func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error { 430 return nil 431} 432 433func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) { 434} 435 436func (sp *streamProxy) Context() context.Context { 437 return sp.ctx 438} 439func (sp *streamProxy) RecvMsg(m interface{}) error { 440 return io.EOF 441} 442 443type statusProxy struct { 444 streamProxy 445 ch chan *controlapi.StatusResponse 446} 447 448func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error { 449 return sp.SendMsg(resp) 450} 451func (sp *statusProxy) SendMsg(m interface{}) error { 452 if sr, ok := m.(*controlapi.StatusResponse); ok { 453 sp.ch <- sr 454 } 455 return nil 456} 457 458type pruneProxy struct { 459 streamProxy 460 ch chan *controlapi.UsageRecord 461} 462 463func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error { 464 return sp.SendMsg(resp) 465} 466func (sp *pruneProxy) SendMsg(m interface{}) error { 467 if sr, ok := m.(*controlapi.UsageRecord); ok { 468 sp.ch <- sr 469 } 470 return nil 471} 472 473type wrapRC struct { 474 io.ReadCloser 475 once sync.Once 476 err error 477 waitCh chan struct{} 478} 479 480func (w *wrapRC) Read(b []byte) (int, error) { 481 n, err := w.ReadCloser.Read(b) 482 if err != nil { 483 e := err 484 if e == io.EOF { 485 e = nil 486 } 487 w.close(e) 488 } 489 return n, err 490} 491 492func (w *wrapRC) Close() error { 493 err := w.ReadCloser.Close() 494 w.close(err) 495 return err 496} 497 498func (w *wrapRC) close(err error) { 499 w.once.Do(func() { 500 w.err = err 501 close(w.waitCh) 502 }) 503} 504 505func (w *wrapRC) wait() error { 506 <-w.waitCh 507 return w.err 508} 509 510type buildJob struct { 511 cancel func() 512 waitCh chan func(io.ReadCloser) error 513} 514 515func newBuildJob() *buildJob { 516 return &buildJob{waitCh: make(chan func(io.ReadCloser) error)} 517} 518 519func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) { 520 done := make(chan struct{}) 521 522 var upload io.ReadCloser 523 fn := func(rc io.ReadCloser) error { 524 w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})} 525 upload = w 526 close(done) 527 return w.wait() 528 } 529 530 select { 531 case <-ctx.Done(): 532 return nil, ctx.Err() 533 case j.waitCh <- fn: 534 <-done 535 return upload, nil 536 } 537} 538 539func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error { 540 select { 541 case <-ctx.Done(): 542 return ctx.Err() 543 case fn := <-j.waitCh: 544 return fn(rc) 545 } 546} 547 548// toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format 549func toBuildkitExtraHosts(inp []string) (string, error) { 550 if len(inp) == 0 { 551 return "", nil 552 } 553 hosts := make([]string, 0, len(inp)) 554 for _, h := range inp { 555 parts := strings.Split(h, ":") 556 557 if len(parts) != 2 || parts[0] == "" || net.ParseIP(parts[1]) == nil { 558 return "", errors.Errorf("invalid host %s", h) 559 } 560 hosts = append(hosts, parts[0]+"="+parts[1]) 561 } 562 return strings.Join(hosts, ","), nil 563} 564 565func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) { 566 var until time.Duration 567 untilValues := opts.Filters.Get("until") // canonical 568 unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter 569 570 if len(untilValues) > 0 && len(unusedForValues) > 0 { 571 return client.PruneInfo{}, errConflictFilter{"until", "unused-for"} 572 } 573 filterKey := "until" 574 if len(unusedForValues) > 0 { 575 filterKey = "unused-for" 576 } 577 untilValues = append(untilValues, unusedForValues...) 578 579 switch len(untilValues) { 580 case 0: 581 // nothing to do 582 case 1: 583 var err error 584 until, err = time.ParseDuration(untilValues[0]) 585 if err != nil { 586 return client.PruneInfo{}, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", filterKey) 587 } 588 default: 589 return client.PruneInfo{}, errMultipleFilterValues{} 590 } 591 592 bkFilter := make([]string, 0, opts.Filters.Len()) 593 for cacheField := range cacheFields { 594 if opts.Filters.Contains(cacheField) { 595 values := opts.Filters.Get(cacheField) 596 switch len(values) { 597 case 0: 598 bkFilter = append(bkFilter, cacheField) 599 case 1: 600 if cacheField == "id" { 601 bkFilter = append(bkFilter, cacheField+"~="+values[0]) 602 } else { 603 bkFilter = append(bkFilter, cacheField+"=="+values[0]) 604 } 605 default: 606 return client.PruneInfo{}, errMultipleFilterValues{} 607 } 608 } 609 } 610 return client.PruneInfo{ 611 All: opts.All, 612 KeepDuration: until, 613 KeepBytes: opts.KeepStorage, 614 Filter: []string{strings.Join(bkFilter, ",")}, 615 }, nil 616} 617