From c79d835077e5242751e848e46cc353fe16dc666f Mon Sep 17 00:00:00 2001 From: Miles Liu Date: Mon, 2 Jan 2023 17:06:09 +0800 Subject: [PATCH] Refactor the volume ls command flagging process Signed-off-by: Miles Liu --- cmd/nerdctl/volume.go | 8 +- cmd/nerdctl/volume_ls.go | 222 +++++--------------------------- pkg/api/types/volume_types.go | 38 ++++++ pkg/cmd/volume/ls.go | 230 ++++++++++++++++++++++++++++++++++ pkg/cmd/volume/volume.go | 32 +++++ 5 files changed, 330 insertions(+), 200 deletions(-) create mode 100644 pkg/api/types/volume_types.go create mode 100644 pkg/cmd/volume/ls.go create mode 100644 pkg/cmd/volume/volume.go diff --git a/cmd/nerdctl/volume.go b/cmd/nerdctl/volume.go index c866b2aedda..0f9fe6e8d18 100644 --- a/cmd/nerdctl/volume.go +++ b/cmd/nerdctl/volume.go @@ -17,7 +17,7 @@ package main import ( - "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/containerd/nerdctl/pkg/mountutil/volumestore" "github.com/spf13/cobra" ) @@ -56,9 +56,5 @@ func getVolumeStore(cmd *cobra.Command) (volumestore.VolumeStore, error) { if err != nil { return nil, err } - dataStore, err := clientutil.DataStore(dataRoot, address) - if err != nil { - return nil, err - } - return volumestore.New(dataStore, ns) + return volume.Store(ns, dataRoot, address) } diff --git a/cmd/nerdctl/volume_ls.go b/cmd/nerdctl/volume_ls.go index 4488e98dc0b..a310c2e74f6 100644 --- a/cmd/nerdctl/volume_ls.go +++ b/cmd/nerdctl/volume_ls.go @@ -17,18 +17,9 @@ package main import ( - "bytes" - "errors" - "fmt" - "strconv" - "strings" - "text/tabwriter" - "text/template" - - "github.com/containerd/containerd/pkg/progress" - "github.com/containerd/nerdctl/pkg/formatter" + types "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/containerd/nerdctl/pkg/inspecttypes/native" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -54,220 +45,63 @@ func newVolumeLsCommand() *cobra.Command { return volumeLsCommand } -type volumePrintable struct { - Driver string - Labels string - Mountpoint string - Name string - Scope string - Size string - // TODO: "Links" -} - func volumeLsAction(cmd *cobra.Command, args []string) error { + options := &types.VolumeLsCommandOptions{} + options.Writer = cmd.OutOrStdout() quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } - volumeSize, err := cmd.Flags().GetBool("size") + options.Quiet = quiet + format, err := cmd.Flags().GetString("format") if err != nil { return err } - if quiet && volumeSize { - logrus.Warn("cannot use --size and --quiet together, ignoring --size") - volumeSize = false + options.Format = format + size, err := cmd.Flags().GetBool("size") + if err != nil { + return err } + options.Size = size filters, err := cmd.Flags().GetStringSlice("filter") if err != nil { return err } - labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, err := getVolumeFilterFuncs(filters) + options.Filters = filters + ns, err := cmd.Flags().GetString("namespace") if err != nil { return err } - if len(sizeFilterFuncs) > 0 && quiet { - logrus.Warn("cannot use --filter=size and --quiet together, ignoring --filter=size") - sizeFilterFuncs = nil - } - if len(sizeFilterFuncs) > 0 && !volumeSize { - logrus.Warn("should use --filter=size and --size together") - cmd.Flags().Set("size", "true") - volumeSize = true - } - w := cmd.OutOrStdout() - var tmpl *template.Template - format, err := cmd.Flags().GetString("format") + options.Namespace = ns + dataRoot, err := cmd.Flags().GetString("data-root") if err != nil { return err } - switch format { - case "", "table", "wide": - w = tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) - if !quiet { - if volumeSize { - fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY\tSIZE") - } else { - fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY") - } - } - case "raw": - return errors.New("unsupported format: \"raw\"") - default: - if quiet { - return errors.New("format and quiet must not be specified together") - } - var err error - tmpl, err = formatter.ParseTemplate(format) - if err != nil { - return err - } - } - - vols, err := getVolumes(cmd) + options.DataRoot = dataRoot + address, err := cmd.Flags().GetString("address") if err != nil { return err } - - for _, v := range vols { - if isFilter && !volumeMatchesFilter(v, labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs) { - continue - } - p := volumePrintable{ - Driver: "local", - Labels: "", - Mountpoint: v.Mountpoint, - Name: v.Name, - Scope: "local", - } - if v.Labels != nil { - p.Labels = formatter.FormatLabels(*v.Labels) - } - if volumeSize { - p.Size = progress.Bytes(v.Size).String() - } - if tmpl != nil { - var b bytes.Buffer - if err := tmpl.Execute(&b, p); err != nil { - return err - } - if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil { - return err - } - } else if quiet { - fmt.Fprintln(w, p.Name) - } else if volumeSize { - fmt.Fprintf(w, "%s\t%s\t%s\n", p.Name, p.Mountpoint, p.Size) - } else { - fmt.Fprintf(w, "%s\t%s\n", p.Name, p.Mountpoint) - } - } - if f, ok := w.(formatter.Flusher); ok { - return f.Flush() - } - return nil + options.Address = address + return volume.Ls(options) } func getVolumes(cmd *cobra.Command) (map[string]native.Volume, error) { - volStore, err := getVolumeStore(cmd) + ns, err := cmd.Flags().GetString("namespace") if err != nil { return nil, err } - volumeSize, err := cmd.Flags().GetBool("size") + dataRoot, err := cmd.Flags().GetString("data-root") if err != nil { return nil, err } - return volStore.List(volumeSize) -} - -func getVolumeFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, []func(int64) bool, bool, error) { - isFilter := len(filters) > 0 - labelFilterFuncs := make([]func(*map[string]string) bool, 0) - nameFilterFuncs := make([]func(string) bool, 0) - sizeFilterFuncs := make([]func(int64) bool, 0) - sizeOperators := []struct { - Operand string - Compare func(int64, int64) bool - }{ - {">=", func(size, volumeSize int64) bool { - return volumeSize >= size - }}, - {"<=", func(size, volumeSize int64) bool { - return volumeSize <= size - }}, - {">", func(size, volumeSize int64) bool { - return volumeSize > size - }}, - {"<", func(size, volumeSize int64) bool { - return volumeSize < size - }}, - {"=", func(size, volumeSize int64) bool { - return volumeSize == size - }}, - } - for _, filter := range filters { - if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") { - subs := strings.SplitN(filter, "=", 2) - if len(subs) < 2 { - continue - } - switch subs[0] { - case "name": - nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { - return strings.Contains(name, subs[1]) - }) - case "label": - v, k, hasValue := "", subs[1], false - if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 { - hasValue = true - k, v = subs[0], subs[1] - } - labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { - if labels == nil { - return false - } - val, ok := (*labels)[k] - if !ok || (hasValue && val != v) { - return false - } - return true - }) - } - continue - } - if strings.HasPrefix(filter, "size") { - for _, sizeOperator := range sizeOperators { - if subs := strings.SplitN(filter, sizeOperator.Operand, 2); len(subs) == 2 { - v, err := strconv.Atoi(subs[1]) - if err != nil { - return nil, nil, nil, false, err - } - sizeFilterFuncs = append(sizeFilterFuncs, func(size int64) bool { - return sizeOperator.Compare(int64(v), size) - }) - break - } - } - continue - } - } - return labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, nil -} - -func volumeMatchesFilter(vol native.Volume, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool, sizeFilterFuncs []func(int64) bool) bool { - for _, labelFilterFunc := range labelFilterFuncs { - if !labelFilterFunc(vol.Labels) { - return false - } - } - for _, nameFilterFunc := range nameFilterFuncs { - if !nameFilterFunc(vol.Name) { - return false - } + address, err := cmd.Flags().GetString("address") + if err != nil { + return nil, err } - for _, sizeFilterFunc := range sizeFilterFuncs { - if !sizeFilterFunc(vol.Size) { - return false - } + volumeSize, err := cmd.Flags().GetBool("size") + if err != nil { + return nil, err } - return true + return volume.Volumes(ns, dataRoot, address, volumeSize) } diff --git a/pkg/api/types/volume_types.go b/pkg/api/types/volume_types.go new file mode 100644 index 00000000000..43ad24ae4b2 --- /dev/null +++ b/pkg/api/types/volume_types.go @@ -0,0 +1,38 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package types + +import "io" + +type VolumeLsCommandOptions struct { + // Writer is the output writer + Writer io.Writer + // Only display volume names + Quiet bool + // Format the output using the given go template + Format string + // Display the disk usage of volumes. Can be slow with volumes having loads of directories. + Size bool + // Filter matches volumes based on given conditions + Filters []string + // containerd namespace + Namespace string + // Root directory of persistent nerdctl state + DataRoot string + // containerd address + Address string +} diff --git a/pkg/cmd/volume/ls.go b/pkg/cmd/volume/ls.go new file mode 100644 index 00000000000..9a6c8cbc654 --- /dev/null +++ b/pkg/cmd/volume/ls.go @@ -0,0 +1,230 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package volume + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "strings" + "text/tabwriter" + "text/template" + + "github.com/containerd/containerd/pkg/progress" + types "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/formatter" + "github.com/containerd/nerdctl/pkg/inspecttypes/native" + "github.com/sirupsen/logrus" +) + +type volumePrintable struct { + Driver string + Labels string + Mountpoint string + Name string + Scope string + Size string + // TODO: "Links" +} + +func Ls(options *types.VolumeLsCommandOptions) error { + if options.Quiet && options.Size { + logrus.Warn("cannot use --size and --quiet together, ignoring --size") + options.Size = false + } + labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, err := getVolumeFilterFuncs(options.Filters) + if err != nil { + return err + } + if len(sizeFilterFuncs) > 0 && options.Quiet { + logrus.Warn("cannot use --filter=size and --quiet together, ignoring --filter=size") + sizeFilterFuncs = nil + } + if len(sizeFilterFuncs) > 0 && !options.Size { + logrus.Warn("should use --filter=size and --size together") + options.Size = true + } + w := options.Writer + var tmpl *template.Template + switch options.Format { + case "", "table", "wide": + w = tabwriter.NewWriter(options.Writer, 4, 8, 4, ' ', 0) + if !options.Quiet { + if options.Size { + fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY\tSIZE") + } else { + fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY") + } + } + case "raw": + return errors.New("unsupported format: \"raw\"") + default: + if options.Quiet { + return errors.New("format and quiet must not be specified together") + } + var err error + tmpl, err = formatter.ParseTemplate(options.Format) + if err != nil { + return err + } + } + + vols, err := Volumes(options.Namespace, options.DataRoot, options.Address, options.Size) + if err != nil { + return err + } + + for _, v := range vols { + if isFilter && !volumeMatchesFilter(v, labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs) { + continue + } + p := volumePrintable{ + Driver: "local", + Labels: "", + Mountpoint: v.Mountpoint, + Name: v.Name, + Scope: "local", + } + if v.Labels != nil { + p.Labels = formatter.FormatLabels(*v.Labels) + } + if options.Size { + p.Size = progress.Bytes(v.Size).String() + } + if tmpl != nil { + var b bytes.Buffer + if err := tmpl.Execute(&b, p); err != nil { + return err + } + if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil { + return err + } + } else if options.Quiet { + fmt.Fprintln(w, p.Name) + } else if options.Size { + fmt.Fprintf(w, "%s\t%s\t%s\n", p.Name, p.Mountpoint, p.Size) + } else { + fmt.Fprintf(w, "%s\t%s\n", p.Name, p.Mountpoint) + } + } + if f, ok := w.(formatter.Flusher); ok { + return f.Flush() + } + return nil +} + +func Volumes(ns string, dataRoot string, address string, volumeSize bool) (map[string]native.Volume, error) { + volStore, err := Store(ns, dataRoot, address) + if err != nil { + return nil, err + } + return volStore.List(volumeSize) +} + +func getVolumeFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, []func(int64) bool, bool, error) { + isFilter := len(filters) > 0 + labelFilterFuncs := make([]func(*map[string]string) bool, 0) + nameFilterFuncs := make([]func(string) bool, 0) + sizeFilterFuncs := make([]func(int64) bool, 0) + sizeOperators := []struct { + Operand string + Compare func(int64, int64) bool + }{ + {">=", func(size, volumeSize int64) bool { + return volumeSize >= size + }}, + {"<=", func(size, volumeSize int64) bool { + return volumeSize <= size + }}, + {">", func(size, volumeSize int64) bool { + return volumeSize > size + }}, + {"<", func(size, volumeSize int64) bool { + return volumeSize < size + }}, + {"=", func(size, volumeSize int64) bool { + return volumeSize == size + }}, + } + for _, filter := range filters { + if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") { + subs := strings.SplitN(filter, "=", 2) + if len(subs) < 2 { + continue + } + switch subs[0] { + case "name": + nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { + return strings.Contains(name, subs[1]) + }) + case "label": + v, k, hasValue := "", subs[1], false + if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 { + hasValue = true + k, v = subs[0], subs[1] + } + labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { + if labels == nil { + return false + } + val, ok := (*labels)[k] + if !ok || (hasValue && val != v) { + return false + } + return true + }) + } + continue + } + if strings.HasPrefix(filter, "size") { + for _, sizeOperator := range sizeOperators { + if subs := strings.SplitN(filter, sizeOperator.Operand, 2); len(subs) == 2 { + v, err := strconv.Atoi(subs[1]) + if err != nil { + return nil, nil, nil, false, err + } + sizeFilterFuncs = append(sizeFilterFuncs, func(size int64) bool { + return sizeOperator.Compare(int64(v), size) + }) + break + } + } + continue + } + } + return labelFilterFuncs, nameFilterFuncs, sizeFilterFuncs, isFilter, nil +} + +func volumeMatchesFilter(vol native.Volume, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool, sizeFilterFuncs []func(int64) bool) bool { + for _, labelFilterFunc := range labelFilterFuncs { + if !labelFilterFunc(vol.Labels) { + return false + } + } + for _, nameFilterFunc := range nameFilterFuncs { + if !nameFilterFunc(vol.Name) { + return false + } + } + for _, sizeFilterFunc := range sizeFilterFuncs { + if !sizeFilterFunc(vol.Size) { + return false + } + } + return true +} diff --git a/pkg/cmd/volume/volume.go b/pkg/cmd/volume/volume.go new file mode 100644 index 00000000000..6efb1a7b46b --- /dev/null +++ b/pkg/cmd/volume/volume.go @@ -0,0 +1,32 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package volume + +import ( + "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/mountutil/volumestore" +) + +// Store returns a volume store +// that corresponds to a directory like `/var/lib/nerdctl/1935db59/volumes/default` +func Store(ns string, dataRoot string, address string) (volumestore.VolumeStore, error) { + dataStore, err := clientutil.DataStore(dataRoot, address) + if err != nil { + return nil, err + } + return volumestore.New(dataStore, ns) +}