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 }