github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/builder/builder-next/builder.go (about) 1 package buildkit 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/containerd/containerd/content" 13 "github.com/containerd/containerd/platforms" 14 "github.com/docker/docker/api/types" 15 "github.com/docker/docker/api/types/backend" 16 "github.com/docker/docker/builder" 17 "github.com/docker/docker/daemon/config" 18 "github.com/docker/docker/daemon/images" 19 "github.com/docker/docker/pkg/streamformatter" 20 "github.com/docker/docker/pkg/system" 21 "github.com/docker/libnetwork" 22 controlapi "github.com/moby/buildkit/api/services/control" 23 "github.com/moby/buildkit/client" 24 "github.com/moby/buildkit/control" 25 "github.com/moby/buildkit/identity" 26 "github.com/moby/buildkit/session" 27 "github.com/moby/buildkit/solver/llbsolver" 28 "github.com/moby/buildkit/util/entitlements" 29 "github.com/moby/buildkit/util/resolver" 30 "github.com/moby/buildkit/util/tracing" 31 "github.com/pkg/errors" 32 "golang.org/x/sync/errgroup" 33 grpcmetadata "google.golang.org/grpc/metadata" 34 ) 35 36 type errMultipleFilterValues struct{} 37 38 func (errMultipleFilterValues) Error() string { return "filters expect only one value" } 39 40 func (errMultipleFilterValues) InvalidParameter() {} 41 42 type errConflictFilter struct { 43 a, b string 44 } 45 46 func (e errConflictFilter) Error() string { 47 return fmt.Sprintf("conflicting filters: %q and %q", e.a, e.b) 48 } 49 50 func (errConflictFilter) InvalidParameter() {} 51 52 var cacheFields = map[string]bool{ 53 "id": true, 54 "parent": true, 55 "type": true, 56 "description": true, 57 "inuse": true, 58 "shared": true, 59 "private": true, 60 // fields from buildkit that are not exposed 61 "mutable": false, 62 "immutable": false, 63 } 64 65 func init() { 66 llbsolver.AllowNetworkHostUnstable = true 67 } 68 69 // Opt is option struct required for creating the builder 70 type Opt struct { 71 SessionManager *session.Manager 72 Root string 73 Dist images.DistributionServices 74 NetworkController libnetwork.NetworkController 75 DefaultCgroupParent string 76 ResolverOpt resolver.ResolveOptionsFunc 77 BuilderConfig config.BuilderConfig 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 c, err := newController(reqHandler, opt) 94 if err != nil { 95 return nil, err 96 } 97 b := &Builder{ 98 controller: c, 99 reqBodyHandler: reqHandler, 100 jobs: map[string]*buildJob{}, 101 } 102 return b, nil 103 } 104 105 // Cancel cancels a build using ID 106 func (b *Builder) Cancel(ctx context.Context, id string) error { 107 b.mu.Lock() 108 if j, ok := b.jobs[id]; ok && j.cancel != nil { 109 j.cancel() 110 } 111 b.mu.Unlock() 112 return nil 113 } 114 115 // DiskUsage returns a report about space used by build cache 116 func (b *Builder) DiskUsage(ctx context.Context) ([]*types.BuildCache, error) { 117 duResp, err := b.controller.DiskUsage(ctx, &controlapi.DiskUsageRequest{}) 118 if err != nil { 119 return nil, err 120 } 121 122 var items []*types.BuildCache 123 for _, r := range duResp.Record { 124 items = append(items, &types.BuildCache{ 125 ID: r.ID, 126 Parent: r.Parent, 127 Type: r.RecordType, 128 Description: r.Description, 129 InUse: r.InUse, 130 Shared: r.Shared, 131 Size: r.Size_, 132 CreatedAt: r.CreatedAt, 133 LastUsedAt: r.LastUsedAt, 134 UsageCount: int(r.UsageCount), 135 }) 136 } 137 return items, nil 138 } 139 140 // Prune clears all reclaimable build cache 141 func (b *Builder) Prune(ctx context.Context, opts types.BuildCachePruneOptions) (int64, []string, error) { 142 ch := make(chan *controlapi.UsageRecord) 143 144 eg, ctx := errgroup.WithContext(ctx) 145 146 validFilters := make(map[string]bool, 1+len(cacheFields)) 147 validFilters["unused-for"] = true 148 validFilters["until"] = true 149 validFilters["label"] = true // TODO(tiborvass): handle label 150 validFilters["label!"] = true // TODO(tiborvass): handle label! 151 for k, v := range cacheFields { 152 validFilters[k] = v 153 } 154 if err := opts.Filters.Validate(validFilters); err != nil { 155 return 0, nil, err 156 } 157 158 pi, err := toBuildkitPruneInfo(opts) 159 if err != nil { 160 return 0, nil, err 161 } 162 163 eg.Go(func() error { 164 defer close(ch) 165 return b.controller.Prune(&controlapi.PruneRequest{ 166 All: pi.All, 167 KeepDuration: int64(pi.KeepDuration), 168 KeepBytes: pi.KeepBytes, 169 Filter: pi.Filter, 170 }, &pruneProxy{ 171 streamProxy: streamProxy{ctx: ctx}, 172 ch: ch, 173 }) 174 }) 175 176 var size int64 177 var cacheIDs []string 178 eg.Go(func() error { 179 for r := range ch { 180 size += r.Size_ 181 cacheIDs = append(cacheIDs, r.ID) 182 } 183 return nil 184 }) 185 186 if err := eg.Wait(); err != nil { 187 return 0, nil, err 188 } 189 190 return size, cacheIDs, nil 191 } 192 193 // Build executes a build request 194 func (b *Builder) Build(ctx context.Context, opt backend.BuildConfig) (*builder.Result, error) { 195 var rc = opt.Source 196 197 if buildID := opt.Options.BuildID; buildID != "" { 198 b.mu.Lock() 199 200 upload := false 201 if strings.HasPrefix(buildID, "upload-request:") { 202 upload = true 203 buildID = strings.TrimPrefix(buildID, "upload-request:") 204 } 205 206 if _, ok := b.jobs[buildID]; !ok { 207 b.jobs[buildID] = newBuildJob() 208 } 209 j := b.jobs[buildID] 210 var cancel func() 211 ctx, cancel = context.WithCancel(ctx) 212 j.cancel = cancel 213 b.mu.Unlock() 214 215 if upload { 216 ctx2, cancel := context.WithTimeout(ctx, 5*time.Second) 217 defer cancel() 218 err := j.SetUpload(ctx2, rc) 219 return nil, err 220 } 221 222 if remoteContext := opt.Options.RemoteContext; remoteContext == "upload-request" { 223 ctx2, cancel := context.WithTimeout(ctx, 5*time.Second) 224 defer cancel() 225 var err error 226 rc, err = j.WaitUpload(ctx2) 227 if err != nil { 228 return nil, err 229 } 230 opt.Options.RemoteContext = "" 231 } 232 233 defer func() { 234 delete(b.jobs, buildID) 235 }() 236 } 237 238 var out builder.Result 239 240 id := identity.NewID() 241 242 frontendAttrs := map[string]string{ 243 "override-copy-image": "docker.io/docker/dockerfile-copy:v0.1.9@sha256:e8f159d3f00786604b93c675ee2783f8dc194bb565e61ca5788f6a6e9d304061", 244 } 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 exporterAttrs := map[string]string{} 317 318 if len(opt.Options.Tags) > 0 { 319 exporterAttrs["name"] = strings.Join(opt.Options.Tags, ",") 320 } 321 322 req := &controlapi.SolveRequest{ 323 Ref: id, 324 Exporter: "moby", 325 ExporterAttrs: exporterAttrs, 326 Frontend: "dockerfile.v0", 327 FrontendAttrs: frontendAttrs, 328 Session: opt.Options.SessionID, 329 } 330 331 if opt.Options.NetworkMode == "host" { 332 req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost) 333 } 334 335 aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output} 336 337 eg, ctx := errgroup.WithContext(ctx) 338 339 eg.Go(func() error { 340 resp, err := b.controller.Solve(ctx, req) 341 if err != nil { 342 return err 343 } 344 id, ok := resp.ExporterResponse["containerimage.digest"] 345 if !ok { 346 return errors.Errorf("missing image id") 347 } 348 out.ImageID = id 349 return aux.Emit("moby.image.id", types.BuildResult{ID: id}) 350 }) 351 352 ch := make(chan *controlapi.StatusResponse) 353 354 eg.Go(func() error { 355 defer close(ch) 356 // streamProxy.ctx is not set to ctx because when request is cancelled, 357 // only the build request has to be cancelled, not the status request. 358 stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch} 359 return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream) 360 }) 361 362 eg.Go(func() error { 363 for sr := range ch { 364 dt, err := sr.Marshal() 365 if err != nil { 366 return err 367 } 368 if err := aux.Emit("moby.buildkit.trace", dt); err != nil { 369 return err 370 } 371 } 372 return nil 373 }) 374 375 if err := eg.Wait(); err != nil { 376 return nil, err 377 } 378 379 return &out, nil 380 } 381 382 type streamProxy struct { 383 ctx context.Context 384 } 385 386 func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error { 387 return nil 388 } 389 390 func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error { 391 return nil 392 } 393 394 func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) { 395 } 396 397 func (sp *streamProxy) Context() context.Context { 398 return sp.ctx 399 } 400 func (sp *streamProxy) RecvMsg(m interface{}) error { 401 return io.EOF 402 } 403 404 type statusProxy struct { 405 streamProxy 406 ch chan *controlapi.StatusResponse 407 } 408 409 func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error { 410 return sp.SendMsg(resp) 411 } 412 func (sp *statusProxy) SendMsg(m interface{}) error { 413 if sr, ok := m.(*controlapi.StatusResponse); ok { 414 sp.ch <- sr 415 } 416 return nil 417 } 418 419 type pruneProxy struct { 420 streamProxy 421 ch chan *controlapi.UsageRecord 422 } 423 424 func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error { 425 return sp.SendMsg(resp) 426 } 427 func (sp *pruneProxy) SendMsg(m interface{}) error { 428 if sr, ok := m.(*controlapi.UsageRecord); ok { 429 sp.ch <- sr 430 } 431 return nil 432 } 433 434 type contentStoreNoLabels struct { 435 content.Store 436 } 437 438 func (c *contentStoreNoLabels) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) { 439 return content.Info{}, nil 440 } 441 442 type wrapRC struct { 443 io.ReadCloser 444 once sync.Once 445 err error 446 waitCh chan struct{} 447 } 448 449 func (w *wrapRC) Read(b []byte) (int, error) { 450 n, err := w.ReadCloser.Read(b) 451 if err != nil { 452 e := err 453 if e == io.EOF { 454 e = nil 455 } 456 w.close(e) 457 } 458 return n, err 459 } 460 461 func (w *wrapRC) Close() error { 462 err := w.ReadCloser.Close() 463 w.close(err) 464 return err 465 } 466 467 func (w *wrapRC) close(err error) { 468 w.once.Do(func() { 469 w.err = err 470 close(w.waitCh) 471 }) 472 } 473 474 func (w *wrapRC) wait() error { 475 <-w.waitCh 476 return w.err 477 } 478 479 type buildJob struct { 480 cancel func() 481 waitCh chan func(io.ReadCloser) error 482 } 483 484 func newBuildJob() *buildJob { 485 return &buildJob{waitCh: make(chan func(io.ReadCloser) error)} 486 } 487 488 func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) { 489 done := make(chan struct{}) 490 491 var upload io.ReadCloser 492 fn := func(rc io.ReadCloser) error { 493 w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})} 494 upload = w 495 close(done) 496 return w.wait() 497 } 498 499 select { 500 case <-ctx.Done(): 501 return nil, ctx.Err() 502 case j.waitCh <- fn: 503 <-done 504 return upload, nil 505 } 506 } 507 508 func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error { 509 select { 510 case <-ctx.Done(): 511 return ctx.Err() 512 case fn := <-j.waitCh: 513 return fn(rc) 514 } 515 } 516 517 // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format 518 func toBuildkitExtraHosts(inp []string) (string, error) { 519 if len(inp) == 0 { 520 return "", nil 521 } 522 hosts := make([]string, 0, len(inp)) 523 for _, h := range inp { 524 parts := strings.Split(h, ":") 525 526 if len(parts) != 2 || parts[0] == "" || net.ParseIP(parts[1]) == nil { 527 return "", errors.Errorf("invalid host %s", h) 528 } 529 hosts = append(hosts, parts[0]+"="+parts[1]) 530 } 531 return strings.Join(hosts, ","), nil 532 } 533 534 func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) { 535 var until time.Duration 536 untilValues := opts.Filters.Get("until") // canonical 537 unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter 538 539 if len(untilValues) > 0 && len(unusedForValues) > 0 { 540 return client.PruneInfo{}, errConflictFilter{"until", "unused-for"} 541 } 542 filterKey := "until" 543 if len(unusedForValues) > 0 { 544 filterKey = "unused-for" 545 } 546 untilValues = append(untilValues, unusedForValues...) 547 548 switch len(untilValues) { 549 case 0: 550 // nothing to do 551 case 1: 552 var err error 553 until, err = time.ParseDuration(untilValues[0]) 554 if err != nil { 555 return client.PruneInfo{}, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", filterKey) 556 } 557 default: 558 return client.PruneInfo{}, errMultipleFilterValues{} 559 } 560 561 bkFilter := make([]string, 0, opts.Filters.Len()) 562 for cacheField := range cacheFields { 563 if opts.Filters.Include(cacheField) { 564 values := opts.Filters.Get(cacheField) 565 switch len(values) { 566 case 0: 567 bkFilter = append(bkFilter, cacheField) 568 case 1: 569 if cacheField == "id" { 570 bkFilter = append(bkFilter, cacheField+"~="+values[0]) 571 } else { 572 bkFilter = append(bkFilter, cacheField+"=="+values[0]) 573 } 574 default: 575 return client.PruneInfo{}, errMultipleFilterValues{} 576 } 577 } 578 } 579 return client.PruneInfo{ 580 All: opts.All, 581 KeepDuration: until, 582 KeepBytes: opts.KeepStorage, 583 Filter: []string{strings.Join(bkFilter, ",")}, 584 }, nil 585 }