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