github.com/rumpl/bof@v23.0.0-rc.2+incompatible/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/opts"
    22  	"github.com/docker/docker/pkg/idtools"
    23  	"github.com/docker/docker/pkg/streamformatter"
    24  	"github.com/docker/go-units"
    25  	controlapi "github.com/moby/buildkit/api/services/control"
    26  	"github.com/moby/buildkit/client"
    27  	"github.com/moby/buildkit/control"
    28  	"github.com/moby/buildkit/identity"
    29  	"github.com/moby/buildkit/session"
    30  	"github.com/moby/buildkit/util/entitlements"
    31  	"github.com/moby/buildkit/util/tracing"
    32  	"github.com/pkg/errors"
    33  	"golang.org/x/sync/errgroup"
    34  	"google.golang.org/grpc"
    35  	grpcmetadata "google.golang.org/grpc/metadata"
    36  )
    37  
    38  type errMultipleFilterValues struct{}
    39  
    40  func (errMultipleFilterValues) Error() string { return "filters expect only one value" }
    41  
    42  func (errMultipleFilterValues) InvalidParameter() {}
    43  
    44  type errConflictFilter struct {
    45  	a, b string
    46  }
    47  
    48  func (e errConflictFilter) Error() string {
    49  	return fmt.Sprintf("conflicting filters: %q and %q", e.a, e.b)
    50  }
    51  
    52  func (errConflictFilter) InvalidParameter() {}
    53  
    54  var cacheFields = map[string]bool{
    55  	"id":          true,
    56  	"parent":      true,
    57  	"type":        true,
    58  	"description": true,
    59  	"inuse":       true,
    60  	"shared":      true,
    61  	"private":     true,
    62  	// fields from buildkit that are not exposed
    63  	"mutable":   false,
    64  	"immutable": false,
    65  }
    66  
    67  // Opt is option struct required for creating the builder
    68  type Opt struct {
    69  	SessionManager      *session.Manager
    70  	Root                string
    71  	Dist                images.DistributionServices
    72  	NetworkController   libnetwork.NetworkController
    73  	DefaultCgroupParent string
    74  	RegistryHosts       docker.RegistryHosts
    75  	BuilderConfig       config.BuilderConfig
    76  	Rootless            bool
    77  	IdentityMapping     idtools.IdentityMapping
    78  	DNSConfig           config.DNSConfig
    79  	ApparmorProfile     string
    80  }
    81  
    82  // Builder can build using BuildKit backend
    83  type Builder struct {
    84  	controller     *control.Controller
    85  	dnsconfig      config.DNSConfig
    86  	reqBodyHandler *reqBodyHandler
    87  
    88  	mu   sync.Mutex
    89  	jobs map[string]*buildJob
    90  }
    91  
    92  // New creates a new builder
    93  func New(opt Opt) (*Builder, error) {
    94  	reqHandler := newReqBodyHandler(tracing.DefaultTransport)
    95  
    96  	c, err := newController(reqHandler, opt)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	b := &Builder{
   101  		controller:     c,
   102  		dnsconfig:      opt.DNSConfig,
   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, //nolint:staticcheck // ignore SA1019 (Parent field is deprecated)
   136  			Parents:     r.Parents,
   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
   151  func (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
   204  func (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  		_, err := platforms.Parse(opt.Options.Platform)
   303  		if err != nil {
   304  			return nil, err
   305  		}
   306  		frontendAttrs["platform"] = opt.Options.Platform
   307  	}
   308  
   309  	switch opt.Options.NetworkMode {
   310  	case "host", "none":
   311  		frontendAttrs["force-network-mode"] = opt.Options.NetworkMode
   312  	case "", "default":
   313  	default:
   314  		return nil, errors.Errorf("network mode %q not supported by buildkit", opt.Options.NetworkMode)
   315  	}
   316  
   317  	extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts, b.dnsconfig.HostGatewayIP)
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  	frontendAttrs["add-hosts"] = extraHosts
   322  
   323  	if opt.Options.ShmSize > 0 {
   324  		frontendAttrs["shm-size"] = strconv.FormatInt(opt.Options.ShmSize, 10)
   325  	}
   326  
   327  	ulimits, err := toBuildkitUlimits(opt.Options.Ulimits)
   328  	if err != nil {
   329  		return nil, err
   330  	} else if len(ulimits) > 0 {
   331  		frontendAttrs["ulimit"] = ulimits
   332  	}
   333  
   334  	exporterName := ""
   335  	exporterAttrs := map[string]string{}
   336  
   337  	if len(opt.Options.Outputs) > 1 {
   338  		return nil, errors.Errorf("multiple outputs not supported")
   339  	} else if len(opt.Options.Outputs) == 0 {
   340  		exporterName = "moby"
   341  	} else {
   342  		// cacheonly is a special type for triggering skipping all exporters
   343  		if opt.Options.Outputs[0].Type != "cacheonly" {
   344  			exporterName = opt.Options.Outputs[0].Type
   345  			exporterAttrs = opt.Options.Outputs[0].Attrs
   346  		}
   347  	}
   348  
   349  	if exporterName == "moby" {
   350  		if len(opt.Options.Tags) > 0 {
   351  			exporterAttrs["name"] = strings.Join(opt.Options.Tags, ",")
   352  		}
   353  	}
   354  
   355  	cache := controlapi.CacheOptions{}
   356  
   357  	if inlineCache := opt.Options.BuildArgs["BUILDKIT_INLINE_CACHE"]; inlineCache != nil {
   358  		if b, err := strconv.ParseBool(*inlineCache); err == nil && b {
   359  			cache.Exports = append(cache.Exports, &controlapi.CacheOptionsEntry{
   360  				Type: "inline",
   361  			})
   362  		}
   363  	}
   364  
   365  	req := &controlapi.SolveRequest{
   366  		Ref:           id,
   367  		Exporter:      exporterName,
   368  		ExporterAttrs: exporterAttrs,
   369  		Frontend:      "dockerfile.v0",
   370  		FrontendAttrs: frontendAttrs,
   371  		Session:       opt.Options.SessionID,
   372  		Cache:         cache,
   373  	}
   374  
   375  	if opt.Options.NetworkMode == "host" {
   376  		req.Entitlements = append(req.Entitlements, entitlements.EntitlementNetworkHost)
   377  	}
   378  
   379  	aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output}
   380  
   381  	eg, ctx := errgroup.WithContext(ctx)
   382  
   383  	eg.Go(func() error {
   384  		resp, err := b.controller.Solve(ctx, req)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		if exporterName != "moby" {
   389  			return nil
   390  		}
   391  		id, ok := resp.ExporterResponse["containerimage.digest"]
   392  		if !ok {
   393  			return errors.Errorf("missing image id")
   394  		}
   395  		out.ImageID = id
   396  		return aux.Emit("moby.image.id", types.BuildResult{ID: id})
   397  	})
   398  
   399  	ch := make(chan *controlapi.StatusResponse)
   400  
   401  	eg.Go(func() error {
   402  		defer close(ch)
   403  		// streamProxy.ctx is not set to ctx because when request is cancelled,
   404  		// only the build request has to be cancelled, not the status request.
   405  		stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch}
   406  		return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream)
   407  	})
   408  
   409  	eg.Go(func() error {
   410  		for sr := range ch {
   411  			dt, err := sr.Marshal()
   412  			if err != nil {
   413  				return err
   414  			}
   415  			if err := aux.Emit("moby.buildkit.trace", dt); err != nil {
   416  				return err
   417  			}
   418  		}
   419  		return nil
   420  	})
   421  
   422  	if err := eg.Wait(); err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	return &out, nil
   427  }
   428  
   429  type streamProxy struct {
   430  	ctx context.Context
   431  }
   432  
   433  func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error {
   434  	return nil
   435  }
   436  
   437  func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error {
   438  	return nil
   439  }
   440  
   441  func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) {
   442  }
   443  
   444  func (sp *streamProxy) Context() context.Context {
   445  	return sp.ctx
   446  }
   447  func (sp *streamProxy) RecvMsg(m interface{}) error {
   448  	return io.EOF
   449  }
   450  
   451  type statusProxy struct {
   452  	streamProxy
   453  	ch chan *controlapi.StatusResponse
   454  }
   455  
   456  func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error {
   457  	return sp.SendMsg(resp)
   458  }
   459  func (sp *statusProxy) SendMsg(m interface{}) error {
   460  	if sr, ok := m.(*controlapi.StatusResponse); ok {
   461  		sp.ch <- sr
   462  	}
   463  	return nil
   464  }
   465  
   466  type pruneProxy struct {
   467  	streamProxy
   468  	ch chan *controlapi.UsageRecord
   469  }
   470  
   471  func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error {
   472  	return sp.SendMsg(resp)
   473  }
   474  func (sp *pruneProxy) SendMsg(m interface{}) error {
   475  	if sr, ok := m.(*controlapi.UsageRecord); ok {
   476  		sp.ch <- sr
   477  	}
   478  	return nil
   479  }
   480  
   481  type wrapRC struct {
   482  	io.ReadCloser
   483  	once   sync.Once
   484  	err    error
   485  	waitCh chan struct{}
   486  }
   487  
   488  func (w *wrapRC) Read(b []byte) (int, error) {
   489  	n, err := w.ReadCloser.Read(b)
   490  	if err != nil {
   491  		e := err
   492  		if e == io.EOF {
   493  			e = nil
   494  		}
   495  		w.close(e)
   496  	}
   497  	return n, err
   498  }
   499  
   500  func (w *wrapRC) Close() error {
   501  	err := w.ReadCloser.Close()
   502  	w.close(err)
   503  	return err
   504  }
   505  
   506  func (w *wrapRC) close(err error) {
   507  	w.once.Do(func() {
   508  		w.err = err
   509  		close(w.waitCh)
   510  	})
   511  }
   512  
   513  func (w *wrapRC) wait() error {
   514  	<-w.waitCh
   515  	return w.err
   516  }
   517  
   518  type buildJob struct {
   519  	cancel func()
   520  	waitCh chan func(io.ReadCloser) error
   521  }
   522  
   523  func newBuildJob() *buildJob {
   524  	return &buildJob{waitCh: make(chan func(io.ReadCloser) error)}
   525  }
   526  
   527  func (j *buildJob) WaitUpload(ctx context.Context) (io.ReadCloser, error) {
   528  	done := make(chan struct{})
   529  
   530  	var upload io.ReadCloser
   531  	fn := func(rc io.ReadCloser) error {
   532  		w := &wrapRC{ReadCloser: rc, waitCh: make(chan struct{})}
   533  		upload = w
   534  		close(done)
   535  		return w.wait()
   536  	}
   537  
   538  	select {
   539  	case <-ctx.Done():
   540  		return nil, ctx.Err()
   541  	case j.waitCh <- fn:
   542  		<-done
   543  		return upload, nil
   544  	}
   545  }
   546  
   547  func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error {
   548  	select {
   549  	case <-ctx.Done():
   550  		return ctx.Err()
   551  	case fn := <-j.waitCh:
   552  		return fn(rc)
   553  	}
   554  }
   555  
   556  // toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format
   557  func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) {
   558  	if len(inp) == 0 {
   559  		return "", nil
   560  	}
   561  	hosts := make([]string, 0, len(inp))
   562  	for _, h := range inp {
   563  		host, ip, ok := strings.Cut(h, ":")
   564  		if !ok || host == "" || ip == "" {
   565  			return "", errors.Errorf("invalid host %s", h)
   566  		}
   567  		// If the IP Address is a "host-gateway", replace this value with the
   568  		// IP address stored in the daemon level HostGatewayIP config variable.
   569  		if ip == opts.HostGatewayName {
   570  			gateway := hostGatewayIP.String()
   571  			if gateway == "" {
   572  				return "", fmt.Errorf("unable to derive the IP value for host-gateway")
   573  			}
   574  			ip = gateway
   575  		} else if net.ParseIP(ip) == nil {
   576  			return "", fmt.Errorf("invalid host %s", h)
   577  		}
   578  		hosts = append(hosts, host+"="+ip)
   579  	}
   580  	return strings.Join(hosts, ","), nil
   581  }
   582  
   583  // toBuildkitUlimits converts ulimits from docker type=soft:hard format to buildkit's csv format
   584  func toBuildkitUlimits(inp []*units.Ulimit) (string, error) {
   585  	if len(inp) == 0 {
   586  		return "", nil
   587  	}
   588  	ulimits := make([]string, 0, len(inp))
   589  	for _, ulimit := range inp {
   590  		ulimits = append(ulimits, ulimit.String())
   591  	}
   592  	return strings.Join(ulimits, ","), nil
   593  }
   594  
   595  func toBuildkitPruneInfo(opts types.BuildCachePruneOptions) (client.PruneInfo, error) {
   596  	var until time.Duration
   597  	untilValues := opts.Filters.Get("until")          // canonical
   598  	unusedForValues := opts.Filters.Get("unused-for") // deprecated synonym for "until" filter
   599  
   600  	if len(untilValues) > 0 && len(unusedForValues) > 0 {
   601  		return client.PruneInfo{}, errConflictFilter{"until", "unused-for"}
   602  	}
   603  	filterKey := "until"
   604  	if len(unusedForValues) > 0 {
   605  		filterKey = "unused-for"
   606  	}
   607  	untilValues = append(untilValues, unusedForValues...)
   608  
   609  	switch len(untilValues) {
   610  	case 0:
   611  		// nothing to do
   612  	case 1:
   613  		var err error
   614  		until, err = time.ParseDuration(untilValues[0])
   615  		if err != nil {
   616  			return client.PruneInfo{}, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", filterKey)
   617  		}
   618  	default:
   619  		return client.PruneInfo{}, errMultipleFilterValues{}
   620  	}
   621  
   622  	bkFilter := make([]string, 0, opts.Filters.Len())
   623  	for cacheField := range cacheFields {
   624  		if opts.Filters.Contains(cacheField) {
   625  			values := opts.Filters.Get(cacheField)
   626  			switch len(values) {
   627  			case 0:
   628  				bkFilter = append(bkFilter, cacheField)
   629  			case 1:
   630  				if cacheField == "id" {
   631  					bkFilter = append(bkFilter, cacheField+"~="+values[0])
   632  				} else {
   633  					bkFilter = append(bkFilter, cacheField+"=="+values[0])
   634  				}
   635  			default:
   636  				return client.PruneInfo{}, errMultipleFilterValues{}
   637  			}
   638  		}
   639  	}
   640  	return client.PruneInfo{
   641  		All:          opts.All,
   642  		KeepDuration: until,
   643  		KeepBytes:    opts.KeepStorage,
   644  		Filter:       []string{strings.Join(bkFilter, ",")},
   645  	}, nil
   646  }