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