diff --git a/cmd/nerdctl/system_prune.go b/cmd/nerdctl/system_prune.go index 03dd28ca473..13e158f3016 100644 --- a/cmd/nerdctl/system_prune.go +++ b/cmd/nerdctl/system_prune.go @@ -21,7 +21,9 @@ import ( "fmt" "strings" + "github.com/containerd/nerdctl/pkg/api/types" "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/cmd/volume" buildkitclient "github.com/moby/buildkit/client" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -102,7 +104,10 @@ func systemPruneAction(cmd *cobra.Command, args []string) error { return err } if vFlag { - if err := volumePrune(ctx, cmd, client, globalOptions); err != nil { + if err := volume.Prune(ctx, &types.VolumePruneCommandOptions{ + GOptions: globalOptions, + Force: true, + }, cmd.InOrStdin(), cmd.OutOrStdout()); err != nil { return err } } diff --git a/cmd/nerdctl/volume_create.go b/cmd/nerdctl/volume_create.go index 2e16f12b53b..88b6bb5fdba 100644 --- a/cmd/nerdctl/volume_create.go +++ b/cmd/nerdctl/volume_create.go @@ -17,10 +17,8 @@ package main import ( - "fmt" - - "github.com/containerd/containerd/identifiers" - "github.com/containerd/nerdctl/pkg/strutil" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/spf13/cobra" ) @@ -43,23 +41,13 @@ func volumeCreateAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - name := args[0] - if err := identifiers.Validate(name); err != nil { - return fmt.Errorf("malformed name %s: %w", name, err) - } - - volStore, err := getVolumeStore(globalOptions) - if err != nil { - return err - } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return err } - labels = strutil.DedupeStrSlice(labels) - if _, err := volStore.Create(name, labels); err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "%s\n", name) - return nil + return volume.Create(&types.VolumeCreateCommandOptions{ + GOptions: globalOptions, + Name: args[0], + Labels: labels, + }, cmd.OutOrStdout()) } diff --git a/cmd/nerdctl/volume_inspect.go b/cmd/nerdctl/volume_inspect.go index fe35b132773..49fb7a3a6f8 100644 --- a/cmd/nerdctl/volume_inspect.go +++ b/cmd/nerdctl/volume_inspect.go @@ -17,7 +17,8 @@ package main import ( - "github.com/containerd/nerdctl/pkg/formatter" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/spf13/cobra" ) @@ -48,26 +49,16 @@ func volumeInspectAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - - volStore, err := getVolumeStore(globalOptions) - if err != nil { - return err - } - result := make([]interface{}, len(args)) - - for i, name := range args { - var vol, err = volStore.Get(name, volumeSize) - if err != nil { - return err - } - result[i] = vol - } format, err := cmd.Flags().GetString("format") if err != nil { return err } - - return formatter.FormatSlice(format, cmd.OutOrStdout(), result) + return volume.Inspect(&types.VolumeInspectCommandOptions{ + GOptions: globalOptions, + Format: format, + Size: volumeSize, + Volumes: args, + }, cmd.OutOrStdout()) } func volumeInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/volume_ls.go b/cmd/nerdctl/volume_ls.go index 7aaca28a900..44550ec6933 100644 --- a/cmd/nerdctl/volume_ls.go +++ b/cmd/nerdctl/volume_ls.go @@ -50,33 +50,29 @@ func volumeLsAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - - options := &types.VolumeLsCommandOptions{} - options.Writer = cmd.OutOrStdout() quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } - options.Quiet = quiet format, err := cmd.Flags().GetString("format") if err != nil { return err } - 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 } - options.Filters = filters - options.Namespace = globalOptions.Namespace - options.DataRoot = globalOptions.DataRoot - options.Address = globalOptions.Address - return volume.Ls(options) + return volume.Ls(&types.VolumeLsCommandOptions{ + GOptions: globalOptions, + Quiet: quiet, + Format: format, + Size: size, + Filters: filters, + }, cmd.OutOrStdout()) } func getVolumes(cmd *cobra.Command, globalOptions *types.GlobalCommandOptions) (map[string]native.Volume, error) { diff --git a/cmd/nerdctl/volume_prune.go b/cmd/nerdctl/volume_prune.go index b1779f1e981..91470d6ead0 100644 --- a/cmd/nerdctl/volume_prune.go +++ b/cmd/nerdctl/volume_prune.go @@ -17,13 +17,8 @@ package main import ( - "context" - "fmt" - "strings" - - "github.com/containerd/containerd" "github.com/containerd/nerdctl/pkg/api/types" - "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/spf13/cobra" ) @@ -49,61 +44,8 @@ func volumePruneAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } - - if !force { - var confirm string - msg := "This will remove all local volumes not used by at least one container." - msg += "\nAre you sure you want to continue? [y/N] " - fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg) - fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm) - - if strings.ToLower(confirm) != "y" { - return nil - } - } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if err != nil { - return err - } - defer cancel() - - return volumePrune(ctx, cmd, client, globalOptions) -} - -func volumePrune(ctx context.Context, cmd *cobra.Command, client *containerd.Client, globalOptions *types.GlobalCommandOptions) error { - volStore, err := getVolumeStore(globalOptions) - if err != nil { - return err - } - volumes, err := volStore.List(false) - if err != nil { - return err - } - containers, err := client.Containers(ctx) - if err != nil { - return err - } - usedVolumes, err := usedVolumes(ctx, containers) - if err != nil { - return err - } - var removeNames []string // nolint: prealloc - for _, volume := range volumes { - if _, ok := usedVolumes[volume.Name]; ok { - continue - } - removeNames = append(removeNames, volume.Name) - } - removedNames, err := volStore.Remove(removeNames) - if err != nil { - return err - } - if len(removedNames) > 0 { - fmt.Fprintln(cmd.OutOrStdout(), "Deleted Volumes:") - for _, name := range removedNames { - fmt.Fprintln(cmd.OutOrStdout(), name) - } - fmt.Fprintln(cmd.OutOrStdout(), "") - } - return nil + return volume.Prune(cmd.Context(), &types.VolumePruneCommandOptions{ + GOptions: globalOptions, + Force: force, + }, cmd.InOrStdin(), cmd.OutOrStdout()) } diff --git a/cmd/nerdctl/volume_rm.go b/cmd/nerdctl/volume_rm.go index 7102bcfad42..490426282a7 100644 --- a/cmd/nerdctl/volume_rm.go +++ b/cmd/nerdctl/volume_rm.go @@ -17,15 +17,8 @@ package main import ( - "context" - "encoding/json" - "fmt" - - "github.com/containerd/containerd" - "github.com/containerd/nerdctl/pkg/clientutil" - "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/pkg/labels" - "github.com/containerd/nerdctl/pkg/mountutil" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/cmd/volume" "github.com/spf13/cobra" ) @@ -50,73 +43,13 @@ func volumeRmAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if err != nil { - return err - } - defer cancel() - containers, err := client.Containers(ctx) - if err != nil { - return err - } - volStore, err := getVolumeStore(globalOptions) - if err != nil { - return err - } - names := args - usedVolumes, err := usedVolumes(ctx, containers) - if err != nil { - return err - } - - var volumenames []string // nolint: prealloc - for _, name := range names { - volume, err := volStore.Get(name, false) - if err != nil { - return err - } - if _, ok := usedVolumes[volume.Name]; ok { - return fmt.Errorf("volume %q is in use", name) - } - volumenames = append(volumenames, name) - } - removedNames, err := volStore.Remove(volumenames) - if err != nil { - return err - } - for _, name := range removedNames { - fmt.Fprintln(cmd.OutOrStdout(), name) - } - return err + return volume.Rm(cmd.Context(), &types.VolumeRmCommandOptions{ + GOptions: globalOptions, + Volumes: args, + }, cmd.OutOrStdout()) } func volumeRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show volume names return shellCompleteVolumeNames(cmd) } - -func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]struct{}, error) { - usedVolumes := make(map[string]struct{}) - for _, c := range containers { - l, err := c.Labels(ctx) - if err != nil { - return nil, err - } - mountsJSON, ok := l[labels.Mounts] - if !ok { - continue - } - - var mounts []dockercompat.MountPoint - err = json.Unmarshal([]byte(mountsJSON), &mounts) - if err != nil { - return nil, err - } - for _, m := range mounts { - if m.Type == mountutil.Volume { - usedVolumes[m.Name] = struct{}{} - } - } - } - return usedVolumes, nil -} diff --git a/pkg/api/types/volume_types.go b/pkg/api/types/volume_types.go index 43ad24ae4b2..ba26af5a586 100644 --- a/pkg/api/types/volume_types.go +++ b/pkg/api/types/volume_types.go @@ -16,11 +16,26 @@ package types -import "io" +type VolumeCreateCommandOptions struct { + GOptions *GlobalCommandOptions + // Name is the volume name + Name string + // Labels are the volume labels + Labels []string +} + +type VolumeInspectCommandOptions struct { + GOptions *GlobalCommandOptions + // 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 + // Volumes are the volumes to be inspected + Volumes []string +} type VolumeLsCommandOptions struct { - // Writer is the output writer - Writer io.Writer + GOptions *GlobalCommandOptions // Only display volume names Quiet bool // Format the output using the given go template @@ -29,10 +44,16 @@ type VolumeLsCommandOptions struct { 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 +} + +type VolumePruneCommandOptions struct { + GOptions *GlobalCommandOptions + // Do not prompt for confirmation + Force bool +} + +type VolumeRmCommandOptions struct { + GOptions *GlobalCommandOptions + // Volumes are the volumes to be removed + Volumes []string } diff --git a/pkg/cmd/volume/create.go b/pkg/cmd/volume/create.go new file mode 100644 index 00000000000..ea9d1162b71 --- /dev/null +++ b/pkg/cmd/volume/create.go @@ -0,0 +1,42 @@ +/* + 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 ( + "fmt" + "io" + + "github.com/containerd/containerd/identifiers" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/strutil" +) + +func Create(options *types.VolumeCreateCommandOptions, stdout io.Writer) error { + if err := identifiers.Validate(options.Name); err != nil { + return fmt.Errorf("malformed name %s: %w", options.Name, err) + } + volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + labels := strutil.DedupeStrSlice(options.Labels) + if _, err := volStore.Create(options.Name, labels); err != nil { + return err + } + fmt.Fprintf(stdout, "%s\n", options.Name) + return nil +} diff --git a/pkg/cmd/volume/inspect.go b/pkg/cmd/volume/inspect.go new file mode 100644 index 00000000000..ab3f2b3e608 --- /dev/null +++ b/pkg/cmd/volume/inspect.go @@ -0,0 +1,41 @@ +/* + 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 ( + "io" + + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/formatter" +) + +func Inspect(options *types.VolumeInspectCommandOptions, stdout io.Writer) error { + volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + result := make([]interface{}, len(options.Volumes)) + + for i, name := range options.Volumes { + var vol, err = volStore.Get(name, options.Size) + if err != nil { + return err + } + result[i] = vol + } + return formatter.FormatSlice(options.Format, stdout, result) +} diff --git a/pkg/cmd/volume/ls.go b/pkg/cmd/volume/ls.go index 9a6c8cbc654..e0c712fded2 100644 --- a/pkg/cmd/volume/ls.go +++ b/pkg/cmd/volume/ls.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "io" "strconv" "strings" "text/tabwriter" @@ -42,7 +43,7 @@ type volumePrintable struct { // TODO: "Links" } -func Ls(options *types.VolumeLsCommandOptions) error { +func Ls(options *types.VolumeLsCommandOptions, stdout io.Writer) error { if options.Quiet && options.Size { logrus.Warn("cannot use --size and --quiet together, ignoring --size") options.Size = false @@ -59,11 +60,11 @@ func Ls(options *types.VolumeLsCommandOptions) error { logrus.Warn("should use --filter=size and --size together") options.Size = true } - w := options.Writer + w := stdout var tmpl *template.Template switch options.Format { case "", "table", "wide": - w = tabwriter.NewWriter(options.Writer, 4, 8, 4, ' ', 0) + w = tabwriter.NewWriter(stdout, 4, 8, 4, ' ', 0) if !options.Quiet { if options.Size { fmt.Fprintln(w, "VOLUME NAME\tDIRECTORY\tSIZE") @@ -84,7 +85,7 @@ func Ls(options *types.VolumeLsCommandOptions) error { } } - vols, err := Volumes(options.Namespace, options.DataRoot, options.Address, options.Size) + vols, err := Volumes(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address, options.Size) if err != nil { return err } diff --git a/pkg/cmd/volume/prune.go b/pkg/cmd/volume/prune.go new file mode 100644 index 00000000000..1afe0e68ab7 --- /dev/null +++ b/pkg/cmd/volume/prune.go @@ -0,0 +1,82 @@ +/* + 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 ( + "context" + "fmt" + "io" + "strings" + + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/clientutil" +) + +func Prune(ctx context.Context, options *types.VolumePruneCommandOptions, stdin io.Reader, stdout io.Writer) error { + if !options.Force { + var confirm string + msg := "This will remove all local volumes not used by at least one container." + msg += "\nAre you sure you want to continue? [y/N] " + fmt.Fprintf(stdout, "WARNING! %s", msg) + fmt.Fscanf(stdin, "%s", &confirm) + + if strings.ToLower(confirm) != "y" { + return nil + } + } + volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + volumes, err := volStore.List(false) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(ctx, options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + containers, err := client.Containers(ctx) + if err != nil { + return err + } + usedVolumes, err := usedVolumes(ctx, containers) + if err != nil { + return err + } + var removeNames []string // nolint: prealloc + for _, volume := range volumes { + if _, ok := usedVolumes[volume.Name]; ok { + continue + } + removeNames = append(removeNames, volume.Name) + } + removedNames, err := volStore.Remove(removeNames) + if err != nil { + return err + } + if len(removedNames) > 0 { + fmt.Fprintln(stdout, "Deleted Volumes:") + for _, name := range removedNames { + fmt.Fprintln(stdout, name) + } + fmt.Fprintln(stdout, "") + } + return nil +} diff --git a/pkg/cmd/volume/rm.go b/pkg/cmd/volume/rm.go new file mode 100644 index 00000000000..01ae1b5c33a --- /dev/null +++ b/pkg/cmd/volume/rm.go @@ -0,0 +1,98 @@ +/* + 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 ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/containerd/containerd" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/mountutil" +) + +func Rm(ctx context.Context, options *types.VolumeRmCommandOptions, stdout io.Writer) error { + client, ctx, cancel, err := clientutil.NewClient(ctx, options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + containers, err := client.Containers(ctx) + if err != nil { + return err + } + volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + usedVolumes, err := usedVolumes(ctx, containers) + if err != nil { + return err + } + + var volumenames []string // nolint: prealloc + for _, name := range options.Volumes { + volume, err := volStore.Get(name, false) + if err != nil { + return err + } + if _, ok := usedVolumes[volume.Name]; ok { + return fmt.Errorf("volume %q is in use", name) + } + volumenames = append(volumenames, name) + } + removedNames, err := volStore.Remove(volumenames) + if err != nil { + return err + } + for _, name := range removedNames { + fmt.Fprintln(stdout, name) + } + return err +} + +func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]struct{}, error) { + usedVolumes := make(map[string]struct{}) + for _, c := range containers { + l, err := c.Labels(ctx) + if err != nil { + return nil, err + } + mountsJSON, ok := l[labels.Mounts] + if !ok { + continue + } + + var mounts []dockercompat.MountPoint + err = json.Unmarshal([]byte(mountsJSON), &mounts) + if err != nil { + return nil, err + } + for _, m := range mounts { + if m.Type == mountutil.Volume { + usedVolumes[m.Name] = struct{}{} + } + } + } + return usedVolumes, nil +}