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