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