diff --git a/cmd/nerdctl/namespace/namespace.go b/cmd/nerdctl/namespace/namespace.go index 133e63cd6f1..0e88c8a4e17 100644 --- a/cmd/nerdctl/namespace/namespace.go +++ b/cmd/nerdctl/namespace/namespace.go @@ -17,19 +17,9 @@ package namespace import ( - "fmt" - "sort" - "strings" - "text/tabwriter" - "github.com/spf13/cobra" - "github.com/containerd/containerd/v2/pkg/namespaces" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/clientutil" - "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" ) func Command() *cobra.Command { @@ -50,90 +40,3 @@ func Command() *cobra.Command { cmd.AddCommand(inspectCommand()) return cmd } - -func listCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List containerd namespaces", - RunE: listAction, - SilenceUsage: true, - SilenceErrors: true, - } - cmd.Flags().BoolP("quiet", "q", false, "Only display names") - return cmd -} - -func listAction(cmd *cobra.Command, args []string) error { - globalOptions, err := helpers.ProcessRootCmdFlags(cmd) - if err != nil { - return err - } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if err != nil { - return err - } - defer cancel() - - nsService := client.NamespaceService() - nsList, err := nsService.List(ctx) - if err != nil { - return err - } - quiet, err := cmd.Flags().GetBool("quiet") - if err != nil { - return err - } - if quiet { - for _, ns := range nsList { - fmt.Fprintln(cmd.OutOrStdout(), ns) - } - return nil - } - dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) - if err != nil { - return err - } - - w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) - // no "NETWORKS", because networks are global objects - fmt.Fprintln(w, "NAME\tCONTAINERS\tIMAGES\tVOLUMES\tLABELS") - for _, ns := range nsList { - ctx = namespaces.WithNamespace(ctx, ns) - var numContainers, numImages, numVolumes int - var labelStrings []string - - containers, err := client.Containers(ctx) - if err != nil { - log.L.Warn(err) - } - numContainers = len(containers) - - images, err := client.ImageService().List(ctx) - if err != nil { - log.L.Warn(err) - } - numImages = len(images) - - volStore, err := volumestore.New(dataStore, ns) - if err != nil { - log.L.Warn(err) - } else { - numVolumes, err = volStore.Count() - if err != nil { - log.L.Warn(err) - } - } - - labels, err := client.NamespaceService().Labels(ctx, ns) - if err != nil { - return err - } - for k, v := range labels { - labelStrings = append(labelStrings, strings.Join([]string{k, v}, "=")) - } - sort.Strings(labelStrings) - fmt.Fprintf(w, "%s\t%d\t%d\t%d\t%v\t\n", ns, numContainers, numImages, numVolumes, strings.Join(labelStrings, ",")) - } - return w.Flush() -} diff --git a/cmd/nerdctl/namespace/namespace_inspect.go b/cmd/nerdctl/namespace/namespace_inspect.go index b79868dbb48..57c3ba5a197 100644 --- a/cmd/nerdctl/namespace/namespace_inspect.go +++ b/cmd/nerdctl/namespace/namespace_inspect.go @@ -75,6 +75,5 @@ func inspectAction(cmd *cobra.Command, args []string) error { } func namespaceInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/cmd/nerdctl/namespace/namespace_list.go b/cmd/nerdctl/namespace/namespace_list.go new file mode 100644 index 00000000000..d1c81dd1713 --- /dev/null +++ b/cmd/nerdctl/namespace/namespace_list.go @@ -0,0 +1,76 @@ +/* + 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 namespace + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" +) + +func listCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List containerd namespaces", + RunE: listAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().BoolP("quiet", "q", false, "Only display names") + cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") + return cmd +} + +func listOptions(cmd *cobra.Command) (types.NamespaceListOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.NamespaceListOptions{}, err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return types.NamespaceListOptions{}, err + } + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return types.NamespaceListOptions{}, err + } + return types.NamespaceListOptions{ + GOptions: globalOptions, + Format: format, + Quiet: quiet, + Stdout: cmd.OutOrStdout(), + }, nil +} + +func listAction(cmd *cobra.Command, args []string) error { + options, err := listOptions(cmd) + if err != nil { + return err + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return namespace.List(ctx, client, options) +} diff --git a/cmd/nerdctl/namespace/namespace_remove.go b/cmd/nerdctl/namespace/namespace_remove.go index 3ce29a5741f..5206b5e7ded 100644 --- a/cmd/nerdctl/namespace/namespace_remove.go +++ b/cmd/nerdctl/namespace/namespace_remove.go @@ -73,6 +73,5 @@ func removeAction(cmd *cobra.Command, args []string) error { } func namespaceRemoveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/cmd/nerdctl/namespace/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go index a15a865bde1..dd15cd91a49 100644 --- a/cmd/nerdctl/namespace/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -71,6 +71,5 @@ func updateAction(cmd *cobra.Command, args []string) error { } func namespaceUpdateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/docs/command-reference.md b/docs/command-reference.md index 110f792732f..a2380887e18 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1289,6 +1289,7 @@ Usage: `nerdctl namespace ls [OPTIONS]` Flags: - `-q, --quiet`: Only display namespace names +- `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}` ### :nerd_face: :blue_square: nerdctl namespace remove diff --git a/pkg/api/types/namespace_types.go b/pkg/api/types/namespace_types.go index c3e8d2c4b08..23b7814dd9e 100644 --- a/pkg/api/types/namespace_types.go +++ b/pkg/api/types/namespace_types.go @@ -43,3 +43,13 @@ type NamespaceInspectOptions struct { // Format the output using the given Go template, e.g, '{{json .}}' Format string } + +// NamespaceListOptions specifies options for `nerdctl namespace ls`. +type NamespaceListOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Format the output using the given Go template, e.g, '{{json .}}' + Format string + // Quiet suppresses extra information and only prints namespace names + Quiet bool +} diff --git a/pkg/cmd/namespace/list.go b/pkg/cmd/namespace/list.go new file mode 100644 index 00000000000..c01fb04c058 --- /dev/null +++ b/pkg/cmd/namespace/list.go @@ -0,0 +1,153 @@ +/* + 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 namespace + +import ( + "bytes" + "context" + "errors" + "fmt" + "sort" + "strings" + "text/tabwriter" + "text/template" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" +) + +func List(ctx context.Context, client *containerd.Client, options types.NamespaceListOptions) error { + nsStore := client.NamespaceService() + nsList, err := nsStore.List(ctx) + if err != nil { + return err + } + + dataStore, err := clientutil.DataStore(options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + + w := options.Stdout + var tmpl *template.Template + namespaceList := []namespace{} + for _, ns := range nsList { + ctx = namespaces.WithNamespace(ctx, ns) + var numContainers, numImages, numVolumes int + + containers, err := client.Containers(ctx) + if err != nil { + log.L.Warn(err) + } + numContainers = len(containers) + + images, err := client.ImageService().List(ctx) + if err != nil { + log.L.Warn(err) + } + numImages = len(images) + + volStore, err := volumestore.New(dataStore, ns) + if err != nil { + log.L.Warn(err) + } else { + numVolumes, err = volStore.Count() + if err != nil { + log.L.Warn(err) + } + } + + labels, err := client.NamespaceService().Labels(ctx, ns) + if err != nil { + return err + } + namespaceList = append(namespaceList, namespace{ + Name: ns, + Containers: numContainers, + Images: numImages, + Volumes: numVolumes, + Labels: labels, + }) + } + + switch options.Format { + case "", "table", "wide": + if !options.Quiet { + w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) + // no "NETWORKS", because networks are global objects + fmt.Fprintln(w, "NAME\tCONTAINERS\tIMAGES\tVOLUMES\tLABELS") + } + 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 + } + } + + for _, namespace := range namespaceList { + if tmpl != nil { + var b bytes.Buffer + if err := tmpl.Execute(&b, namespace); err != nil { + return err + } + if _, err := fmt.Fprintln(w, b.String()); err != nil { + return err + } + } else if options.Quiet { + if _, err := fmt.Fprintln(w, namespace.Name); err != nil { + return err + } + } else { + format := "%s\t%d\t%d\t%d\t%v\t\n" + var labelStrings []string + for k, v := range namespace.Labels { + labelStrings = append(labelStrings, strings.Join([]string{k, v}, "=")) + } + sort.Strings(labelStrings) + args := []interface{}{} + args = append(args, namespace.Name, namespace.Containers, namespace.Images, namespace.Volumes, strings.Join(labelStrings, ",")) + if _, err := fmt.Fprintf(w, format, args...); err != nil { + return err + } + } + } + + if f, ok := w.(formatter.Flusher); ok { + return f.Flush() + } + return nil +} + +type namespace struct { + Name string + Containers int + Images int + Volumes int + Labels map[string]string +}