github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/volume/list.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package volume
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"text/tabwriter"
    26  	"text/template"
    27  
    28  	"github.com/containerd/containerd/pkg/progress"
    29  	"github.com/containerd/log"
    30  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    31  	"github.com/containerd/nerdctl/v2/pkg/formatter"
    32  	"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
    33  )
    34  
    35  type volumePrintable struct {
    36  	Driver     string
    37  	Labels     string
    38  	Mountpoint string
    39  	Name       string
    40  	Scope      string
    41  	Size       string
    42  	// TODO: "Links"
    43  }
    44  
    45  func List(options types.VolumeListOptions) error {
    46  	if options.Quiet && options.Size {
    47  		log.L.Warn("cannot use --size and --quiet together, ignoring --size")
    48  		options.Size = false
    49  	}
    50  	sizeFilter := hasSizeFilter(options.Filters)
    51  	if sizeFilter && options.Quiet {
    52  		log.L.Warn("cannot use --filter=size and --quiet together, ignoring --filter=size")
    53  		options.Filters = removeSizeFilters(options.Filters)
    54  	}
    55  	if sizeFilter && !options.Size {
    56  		log.L.Warn("should use --filter=size and --size together")
    57  		options.Size = true
    58  	}
    59  
    60  	vols, err := Volumes(
    61  		options.GOptions.Namespace,
    62  		options.GOptions.DataRoot,
    63  		options.GOptions.Address,
    64  		options.Size,
    65  		options.Filters,
    66  	)
    67  	if err != nil {
    68  		return err
    69  	}
    70  	return lsPrintOutput(vols, options)
    71  }
    72  
    73  func hasSizeFilter(filters []string) bool {
    74  	for _, filter := range filters {
    75  		if strings.HasPrefix(filter, "size") {
    76  			return true
    77  		}
    78  	}
    79  	return false
    80  }
    81  
    82  func removeSizeFilters(filters []string) []string {
    83  	var res []string
    84  	for _, filter := range filters {
    85  		if !strings.HasPrefix(filter, "size") {
    86  			res = append(res, filter)
    87  		}
    88  	}
    89  	return res
    90  }
    91  
    92  func lsPrintOutput(vols map[string]native.Volume, options types.VolumeListOptions) error {
    93  	w := options.Stdout
    94  	var tmpl *template.Template
    95  	switch options.Format {
    96  	case "", "table", "wide":
    97  		w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
    98  		if !options.Quiet {
    99  			if options.Size {
   100  				fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY\tSIZE")
   101  			} else {
   102  				fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY")
   103  			}
   104  		}
   105  	case "raw":
   106  		return errors.New("unsupported format: \"raw\"")
   107  	default:
   108  		if options.Quiet {
   109  			return errors.New("format and quiet must not be specified together")
   110  		}
   111  		var err error
   112  		tmpl, err = formatter.ParseTemplate(options.Format)
   113  		if err != nil {
   114  			return err
   115  		}
   116  	}
   117  
   118  	for _, v := range vols {
   119  		p := volumePrintable{
   120  			Driver:     "local",
   121  			Labels:     "",
   122  			Mountpoint: v.Mountpoint,
   123  			Name:       v.Name,
   124  			Scope:      "local",
   125  		}
   126  		if v.Labels != nil {
   127  			p.Labels = formatter.FormatLabels(*v.Labels)
   128  		}
   129  		if options.Size {
   130  			p.Size = progress.Bytes(v.Size).String()
   131  		}
   132  		if tmpl != nil {
   133  			var b bytes.Buffer
   134  			if err := tmpl.Execute(&b, p); err != nil {
   135  				return err
   136  			}
   137  			if _, err := fmt.Fprintln(w, b.String()); err != nil {
   138  				return err
   139  			}
   140  		} else if options.Quiet {
   141  			fmt.Fprintln(w, p.Name)
   142  		} else if options.Size {
   143  			fmt.Fprintf(w, "%s\t%s\t%s\n", p.Name, p.Mountpoint, p.Size)
   144  		} else {
   145  			fmt.Fprintf(w, "%s\t%s\n", p.Name, p.Mountpoint)
   146  		}
   147  	}
   148  	if f, ok := w.(formatter.Flusher); ok {
   149  		return f.Flush()
   150  	}
   151  	return nil
   152  }
   153  
   154  // Volumes returns volumes that match the given filters.
   155  //
   156  // Supported filters:
   157  //   - label=<key>=<value>: Match volumes by label on both key and value.
   158  //     If value is left empty, match all volumes with key regardless of its value.
   159  //   - name=<value>: Match all volumes with a name containing the value string.
   160  //   - size=<value>: Match all volumes with a size meets the value.
   161  //     Size operand can be >=, <=, >, <, = and value must be an integer.
   162  //
   163  // Unsupported filters:
   164  //   - dangling=true: Filter volumes by dangling.
   165  //   - driver=local: Filter volumes by driver.
   166  func Volumes(ns string, dataRoot string, address string, volumeSize bool, filters []string) (map[string]native.Volume, error) {
   167  	volStore, err := Store(ns, dataRoot, address)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	vols, err := volStore.List(volumeSize)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, err := getVolumeFilterFuncs(filters)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	if !isFilter {
   181  		return vols, nil
   182  	}
   183  	for k, v := range vols {
   184  		if !volumeMatchesFilter(v, labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs) {
   185  			delete(vols, k)
   186  		}
   187  	}
   188  	return vols, nil
   189  }
   190  
   191  func getVolumeFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, []func(int64) bool, bool, error) {
   192  	isFilter := len(filters) > 0
   193  	labelFilterFuncs := make([]func(*map[string]string) bool, 0)
   194  	nameFilterFuncs := make([]func(string) bool, 0)
   195  	sizeFilterFuncs := make([]func(int64) bool, 0)
   196  	sizeOperators := []struct {
   197  		Operand string
   198  		Compare func(int64, int64) bool
   199  	}{
   200  		{">=", func(size, volumeSize int64) bool {
   201  			return volumeSize >= size
   202  		}},
   203  		{"<=", func(size, volumeSize int64) bool {
   204  			return volumeSize <= size
   205  		}},
   206  		{">", func(size, volumeSize int64) bool {
   207  			return volumeSize > size
   208  		}},
   209  		{"<", func(size, volumeSize int64) bool {
   210  			return volumeSize < size
   211  		}},
   212  		{"=", func(size, volumeSize int64) bool {
   213  			return volumeSize == size
   214  		}},
   215  	}
   216  	for _, filter := range filters {
   217  		if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") {
   218  			subs := strings.SplitN(filter, "=", 2)
   219  			if len(subs) < 2 {
   220  				continue
   221  			}
   222  			switch subs[0] {
   223  			case "name":
   224  				nameFilterFuncs = append(nameFilterFuncs, func(name string) bool {
   225  					return strings.Contains(name, subs[1])
   226  				})
   227  			case "label":
   228  				v, k, hasValue := "", subs[1], false
   229  				if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 {
   230  					hasValue = true
   231  					k, v = subs[0], subs[1]
   232  				}
   233  				labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool {
   234  					if labels == nil {
   235  						return false
   236  					}
   237  					val, ok := (*labels)[k]
   238  					if !ok || (hasValue && val != v) {
   239  						return false
   240  					}
   241  					return true
   242  				})
   243  			}
   244  			continue
   245  		}
   246  		if strings.HasPrefix(filter, "size") {
   247  			for _, sizeOperator := range sizeOperators {
   248  				if subs := strings.SplitN(filter, sizeOperator.Operand, 2); len(subs) == 2 {
   249  					v, err := strconv.Atoi(subs[1])
   250  					if err != nil {
   251  						return nil, nil, nil, false, err
   252  					}
   253  					sizeFilterFuncs = append(sizeFilterFuncs, func(size int64) bool {
   254  						return sizeOperator.Compare(int64(v), size)
   255  					})
   256  					break
   257  				}
   258  			}
   259  			continue
   260  		}
   261  	}
   262  	return labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, nil
   263  }
   264  
   265  func volumeMatchesFilter(vol native.Volume, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool, sizeFilterFuncs []func(int64) bool) bool {
   266  	for _, labelFilterFunc := range labelFilterFuncs {
   267  		if !labelFilterFunc(vol.Labels) {
   268  			return false
   269  		}
   270  	}
   271  	for _, nameFilterFunc := range nameFilterFuncs {
   272  		if !nameFilterFunc(vol.Name) {
   273  			return false
   274  		}
   275  	}
   276  	for _, sizeFilterFunc := range sizeFilterFuncs {
   277  		if !sizeFilterFunc(vol.Size) {
   278  			return false
   279  		}
   280  	}
   281  	return true
   282  }