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