github.com/devdivbcp/moby@v17.12.0-ce-rc1.0.20200726071732-2d4bfdc789ad+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/content" 14 "github.com/containerd/containerd/platforms" 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/resolver" 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 ResolverOpt resolver.ResolveOptionsFunc 75 BuilderConfig config.BuilderConfig 76 Rootless bool 77 IdentityMapping *idtools.IdentityMapping 78 DNSConfig config.DNSConfig 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 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. 111 func (b *Builder) RegisterGRPC(s *grpc.Server) { 112 b.controller.Register(s) 113 } 114 115 // Cancel cancels a build using ID 116 func (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 126 func (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 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 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 421 type streamProxy struct { 422 ctx context.Context 423 } 424 425 func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error { 426 return nil 427 } 428 429 func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error { 430 return nil 431 } 432 433 func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) { 434 } 435 436 func (sp *streamProxy) Context() context.Context { 437 return sp.ctx 438 } 439 func (sp *streamProxy) RecvMsg(m interface{}) error { 440 return io.EOF 441 } 442 443 type statusProxy struct { 444 streamProxy 445 ch chan *controlapi.StatusResponse 446 } 447 448 func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error { 449 return sp.SendMsg(resp) 450 } 451 func (sp *statusProxy) SendMsg(m interface{}) error { 452 if sr, ok := m.(*controlapi.StatusResponse); ok { 453 sp.ch <- sr 454 } 455 return nil 456 } 457 458 type pruneProxy struct { 459 streamProxy 460 ch chan *controlapi.UsageRecord 461 } 462 463 func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error { 464 return sp.SendMsg(resp) 465 } 466 func (sp *pruneProxy) SendMsg(m interface{}) error { 467 if sr, ok := m.(*controlapi.UsageRecord); ok { 468 sp.ch <- sr 469 } 470 return nil 471 } 472 473 type contentStoreNoLabels struct { 474 content.Store 475 } 476 477 func (c *contentStoreNoLabels) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) { 478 return content.Info{}, 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) (string, error) { 558 if len(inp) == 0 { 559 return "", nil 560 } 561 hosts := make([]string, 0, len(inp)) 562 for _, h := range inp { 563 parts := strings.Split(h, ":") 564 565 if len(parts) != 2 || parts[0] == "" || net.ParseIP(parts[1]) == nil { 566 return "", errors.Errorf("invalid host %s", h) 567 } 568 hosts = append(hosts, parts[0]+"="+parts[1]) 569 } 570 return strings.Join(hosts, ","), nil 571 } 572 573 func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) { 574 var until time.Duration 575 untilValues := opts.Filters.Get("until") // canonical 576 unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter 577 578 if len(untilValues) > 0 && len(unusedForValues) > 0 { 579 return client.PruneInfo{}, errConflictFilter{"until", "unused-for"} 580 } 581 filterKey := "until" 582 if len(unusedForValues) > 0 { 583 filterKey = "unused-for" 584 } 585 untilValues = append(untilValues, unusedForValues...) 586 587 switch len(untilValues) { 588 case 0: 589 // nothing to do 590 case 1: 591 var err error 592 until, err = time.ParseDuration(untilValues[0]) 593 if err != nil { 594 return client.PruneInfo{}, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", filterKey) 595 } 596 default: 597 return client.PruneInfo{}, errMultipleFilterValues{} 598 } 599 600 bkFilter := make([]string, 0, opts.Filters.Len()) 601 for cacheField := range cacheFields { 602 if opts.Filters.Contains(cacheField) { 603 values := opts.Filters.Get(cacheField) 604 switch len(values) { 605 case 0: 606 bkFilter = append(bkFilter, cacheField) 607 case 1: 608 if cacheField == "id" { 609 bkFilter = append(bkFilter, cacheField+"~="+values[0]) 610 } else { 611 bkFilter = append(bkFilter, cacheField+"=="+values[0]) 612 } 613 default: 614 return client.PruneInfo{}, errMultipleFilterValues{} 615 } 616 } 617 } 618 return client.PruneInfo{ 619 All: opts.All, 620 KeepDuration: until, 621 KeepBytes: opts.KeepStorage, 622 Filter: []string{strings.Join(bkFilter, ",")}, 623 }, nil 624 }