github.com/wozhu6104/docker@v20.10.10+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/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  
    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  	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.
   111  func (b *Builder) RegisterGRPC(s *grpc.Server) {
   112  	b.controller.Register(s)
   113  }
   114  
   115  // Cancel cancels a build using ID
   116  func (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
   126  func (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
   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  		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  
   421  type streamProxy struct {
   422  	ctx context.Context
   423  }
   424  
   425  func (sp *streamProxy) SetHeader(_ grpcmetadata.MD) error {
   426  	return nil
   427  }
   428  
   429  func (sp *streamProxy) SendHeader(_ grpcmetadata.MD) error {
   430  	return nil
   431  }
   432  
   433  func (sp *streamProxy) SetTrailer(_ grpcmetadata.MD) {
   434  }
   435  
   436  func (sp *streamProxy) Context() context.Context {
   437  	return sp.ctx
   438  }
   439  func (sp *streamProxy) RecvMsg(m interface{}) error {
   440  	return io.EOF
   441  }
   442  
   443  type statusProxy struct {
   444  	streamProxy
   445  	ch chan *controlapi.StatusResponse
   446  }
   447  
   448  func (sp *statusProxy) Send(resp *controlapi.StatusResponse) error {
   449  	return sp.SendMsg(resp)
   450  }
   451  func (sp *statusProxy) SendMsg(m interface{}) error {
   452  	if sr, ok := m.(*controlapi.StatusResponse); ok {
   453  		sp.ch <- sr
   454  	}
   455  	return nil
   456  }
   457  
   458  type pruneProxy struct {
   459  	streamProxy
   460  	ch chan *controlapi.UsageRecord
   461  }
   462  
   463  func (sp *pruneProxy) Send(resp *controlapi.UsageRecord) error {
   464  	return sp.SendMsg(resp)
   465  }
   466  func (sp *pruneProxy) SendMsg(m interface{}) error {
   467  	if sr, ok := m.(*controlapi.UsageRecord); ok {
   468  		sp.ch <- sr
   469  	}
   470  	return nil
   471  }
   472  
   473  type wrapRC struct {
   474  	io.ReadCloser
   475  	once   sync.Once
   476  	err    error
   477  	waitCh chan struct{}
   478  }
   479  
   480  func (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  
   492  func (w *wrapRC) Close() error {
   493  	err := w.ReadCloser.Close()
   494  	w.close(err)
   495  	return err
   496  }
   497  
   498  func (w *wrapRC) close(err error) {
   499  	w.once.Do(func() {
   500  		w.err = err
   501  		close(w.waitCh)
   502  	})
   503  }
   504  
   505  func (w *wrapRC) wait() error {
   506  	<-w.waitCh
   507  	return w.err
   508  }
   509  
   510  type buildJob struct {
   511  	cancel func()
   512  	waitCh chan func(io.ReadCloser) error
   513  }
   514  
   515  func newBuildJob() *buildJob {
   516  	return &buildJob{waitCh: make(chan func(io.ReadCloser) error)}
   517  }
   518  
   519  func (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  
   539  func (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
   549  func 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  
   565  func 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  }