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