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