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