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