From 85c2878a7180b16cb5bf6b5835a3e3eee37f4405 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 25 Nov 2025 21:10:32 +0900 Subject: [PATCH 1/6] fix(namespace): require --label flag for update command Signed-off-by: Park jungtae --- cmd/nerdctl/namespace/namespace_update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/namespace/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go index dd15cd91a49..0e02f78a9c8 100644 --- a/cmd/nerdctl/namespace/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -36,7 +36,8 @@ func updateCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } - cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace") + cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace (required)") + cmd.MarkFlagRequired("label") return cmd } From 7f8ea7cc80e69c0c6292615f0ec6692693c6e147 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Wed, 26 Nov 2025 07:55:06 +0900 Subject: [PATCH 2/6] fix(namespace): add namespace existence check in update command Signed-off-by: Park jungtae --- pkg/cmd/namespace/common.go | 23 ++++++++++++++++++++++- pkg/cmd/namespace/update.go | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/namespace/common.go b/pkg/cmd/namespace/common.go index e08939e0427..4aace52e4e9 100644 --- a/pkg/cmd/namespace/common.go +++ b/pkg/cmd/namespace/common.go @@ -16,7 +16,15 @@ package namespace -import "strings" +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/errdefs" +) func objectWithLabelArgs(args []string) map[string]string { if len(args) >= 1 { @@ -39,3 +47,16 @@ func labelArgs(labelStrings []string) map[string]string { return labels } + +// namespaceExists checks if the namespace exists +func namespaceExists(ctx context.Context, store namespaces.Store, namespace string) error { + nsList, err := store.List(ctx) + if err != nil { + return err + } + if slices.Contains(nsList, namespace) { + return nil + } + + return fmt.Errorf("namespace %s: %w", namespace, errdefs.ErrNotFound) +} diff --git a/pkg/cmd/namespace/update.go b/pkg/cmd/namespace/update.go index 63d2d8a5971..d127db2a2bf 100644 --- a/pkg/cmd/namespace/update.go +++ b/pkg/cmd/namespace/update.go @@ -27,6 +27,10 @@ import ( func Update(ctx context.Context, client *containerd.Client, namespace string, options types.NamespaceUpdateOptions) error { labelsArg := objectWithLabelArgs(options.Labels) namespaces := client.NamespaceService() + if err := namespaceExists(ctx, namespaces, namespace); err != nil { + return err + } + for k, v := range labelsArg { if err := namespaces.SetLabel(ctx, namespace, k, v); err != nil { return err From 8fb7681379e0e336d9d2df0a9177de67f12fb356 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Wed, 26 Nov 2025 08:20:45 +0900 Subject: [PATCH 3/6] fix(namespace): add namespace existence check in Inspect command Signed-off-by: Park jungtae --- pkg/cmd/namespace/inspect.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/namespace/inspect.go b/pkg/cmd/namespace/inspect.go index 3a7a4932815..297252b2c2f 100644 --- a/pkg/cmd/namespace/inspect.go +++ b/pkg/cmd/namespace/inspect.go @@ -18,9 +18,11 @@ package namespace import ( "context" + "errors" 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/formatter" @@ -28,10 +30,17 @@ import ( ) func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces []string, options types.NamespaceInspectOptions) error { - result := make([]interface{}, len(inspectedNamespaces)) - for index, ns := range inspectedNamespaces { + result := []interface{}{} + + warns := []error{} + for _, ns := range inspectedNamespaces { ctx = namespaces.WithNamespace(ctx, ns) - labels, err := client.NamespaceService().Labels(ctx, ns) + namespaceService := client.NamespaceService() + if err := namespaceExists(ctx, namespaceService, ns); err != nil { + warns = append(warns, err) + continue + } + labels, err := namespaceService.Labels(ctx, ns) if err != nil { return err } @@ -39,7 +48,18 @@ func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces Name: ns, Labels: &labels, } - result[index] = nsInspect + result = append(result, nsInspect) + } + if err := formatter.FormatSlice(options.Format, options.Stdout, result); err != nil { + return err + } + for _, warn := range warns { + log.G(ctx).Warn(warn) } - return formatter.FormatSlice(options.Format, options.Stdout, result) + + if len(warns) != 0 { + return errors.New("some namespaces could not be inspected") + } + + return nil } From 0a3ac586116ee564454f3e04ec2de0ce60f2a333 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sat, 29 Nov 2025 09:34:37 +0900 Subject: [PATCH 4/6] feat(namespace): include containers, images, and volumes details in inspect Signed-off-by: Park jungtae --- pkg/cmd/namespace/inspect.go | 90 +++++++++++++++++++++++++++- pkg/inspecttypes/native/namespace.go | 19 ++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/namespace/inspect.go b/pkg/cmd/namespace/inspect.go index 297252b2c2f..6a8f00f017c 100644 --- a/pkg/cmd/namespace/inspect.go +++ b/pkg/cmd/namespace/inspect.go @@ -19,14 +19,17 @@ package namespace import ( "context" "errors" + "strings" 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/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" ) func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces []string, options types.NamespaceInspectOptions) error { @@ -40,13 +43,33 @@ func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces warns = append(warns, err) continue } + labels, err := namespaceService.Labels(ctx, ns) if err != nil { return err } + + containerInfo, err := containerInfo(ctx, client) + if err != nil { + warns = append(warns, err) + } + + imageInfo, err := imageInfo(ctx, client) + if err != nil { + warns = append(warns, err) + } + + volumeInfo, err := volumeInfo(ns, options) + if err != nil { + warns = append(warns, err) + } + nsInspect := native.Namespace{ - Name: ns, - Labels: &labels, + Name: ns, + Labels: &labels, + Containers: &containerInfo, + Images: &imageInfo, + Volumes: &volumeInfo, } result = append(result, nsInspect) } @@ -63,3 +86,66 @@ func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces return nil } +func containerInfo(ctx context.Context, client *containerd.Client) (native.ContainerInfo, error) { + info := native.ContainerInfo{} + containers, err := client.Containers(ctx) + if err != nil { + return info, err + } + + info.Count = len(containers) + ids := make([]string, info.Count) + + info.IDs = ids + for idx, container := range containers { + ids[idx] = container.ID() + } + + return info, nil +} +func imageInfo(ctx context.Context, client *containerd.Client) (native.ImageInfo, error) { + info := native.ImageInfo{} + imageService := client.ImageService() + images, err := imageService.List(ctx) + if err != nil { + return info, err + } + + info.Count = len(images) + ids := make([]string, info.Count) + + info.IDs = ids + for idx, img := range images { + ids[idx] = strings.Split(img.Target.Digest.String(), ":")[1] + } + + return info, nil +} + +func volumeInfo(namespace string, options types.NamespaceInspectOptions) (native.VolumeInfo, error) { + info := native.VolumeInfo{} + + dataStore, err := clientutil.DataStore(options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return info, err + } + volStore, err := volumestore.New(dataStore, namespace) + if err != nil { + return info, err + } + + volumes, err := volStore.List(false) + if err != nil { + return info, err + } + + info.Count = len(volumes) + names := make([]string, 0, info.Count) + + for _, v := range volumes { + names = append(names, v.Name) + } + + info.Names = names + return info, nil +} diff --git a/pkg/inspecttypes/native/namespace.go b/pkg/inspecttypes/native/namespace.go index 4e66a4d45d4..ae907f16fd3 100644 --- a/pkg/inspecttypes/native/namespace.go +++ b/pkg/inspecttypes/native/namespace.go @@ -19,4 +19,23 @@ package native type Namespace struct { Name string `json:"Name"` Labels *map[string]string `json:"Labels,omitempty"` + + Containers *ContainerInfo `json:"Containers"` + Images *ImageInfo `json:"Images"` + Volumes *VolumeInfo `json:"Volumes"` +} + +type ContainerInfo struct { + Count int `json:"count"` + IDs []string `json:"ids"` +} + +type ImageInfo struct { + Count int `json:"count"` + IDs []string `json:"ids"` +} + +type VolumeInfo struct { + Count int `json:"count"` + Names []string `json:"names"` } From 5ea5532be6ad086b2f4471ec042ba024e74f974a Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sat, 29 Nov 2025 18:47:16 +0900 Subject: [PATCH 5/6] chore(namespace): Add safety check for unexpected digest format Signed-off-by: Park jungtae --- pkg/cmd/namespace/inspect.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/namespace/inspect.go b/pkg/cmd/namespace/inspect.go index 6a8f00f017c..1c969a312a1 100644 --- a/pkg/cmd/namespace/inspect.go +++ b/pkg/cmd/namespace/inspect.go @@ -18,7 +18,6 @@ package namespace import ( "context" - "errors" "strings" containerd "github.com/containerd/containerd/v2/client" @@ -80,10 +79,6 @@ func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces log.G(ctx).Warn(warn) } - if len(warns) != 0 { - return errors.New("some namespaces could not be inspected") - } - return nil } func containerInfo(ctx context.Context, client *containerd.Client) (native.ContainerInfo, error) { @@ -111,14 +106,20 @@ func imageInfo(ctx context.Context, client *containerd.Client) (native.ImageInfo return info, err } - info.Count = len(images) - ids := make([]string, info.Count) + ids := make([]string, 0, len(images)) - info.IDs = ids - for idx, img := range images { - ids[idx] = strings.Split(img.Target.Digest.String(), ":")[1] + for _, img := range images { + digestStrSplit := strings.SplitN(img.Target.Digest.String(), ":", 2) + if len(digestStrSplit) == 2 { + ids = append(ids, digestStrSplit[1][:12]) + } else { + log.G(ctx).Warnf("invalid image digest format:%s", img.Target.Digest.String()) + } } + info.IDs = ids + info.Count = len(ids) + return info, nil } From e1b84aeea8e23a6c339dcd035390d1328dbcc079 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sat, 29 Nov 2025 19:25:53 +0900 Subject: [PATCH 6/6] test: add namespace inspect test cases Signed-off-by: Park jungtae --- .../namespace/namespace_inspect_test.go | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 cmd/nerdctl/namespace/namespace_inspect_test.go diff --git a/cmd/nerdctl/namespace/namespace_inspect_test.go b/cmd/nerdctl/namespace/namespace_inspect_test.go new file mode 100644 index 00000000000..82ec31685ee --- /dev/null +++ b/cmd/nerdctl/namespace/namespace_inspect_test.go @@ -0,0 +1,156 @@ +/* + 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 ( + "errors" + "fmt" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestNamespaceInspect(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("namespace", "create", data.Identifier("first")) + helpers.Ensure("namespace", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier("second")) + + // Create some resources in the namespaces + helpers.Ensure("--namespace", data.Identifier("first"), "run", "-d", "--name", data.Identifier("container1"), testutil.CommonImage) + helpers.Ensure("--namespace", data.Identifier("first"), "run", "-d", "--name", data.Identifier("container2"), testutil.CommonImage) + helpers.Ensure("--namespace", data.Identifier("second"), "run", "-d", "--name", data.Identifier("container3"), testutil.CommonImage) + // Create a volume + helpers.Ensure("--namespace", data.Identifier("first"), "volume", "create", data.Identifier("volume1")) + + data.Labels().Set("ns1", data.Identifier("first")) + data.Labels().Set("ns2", data.Identifier("second")) + data.Labels().Set("container1", data.Identifier("container1")) + data.Labels().Set("container2", data.Identifier("container2")) + data.Labels().Set("container3", data.Identifier("container3")) + data.Labels().Set("volume1", data.Identifier("volume1")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("--namespace", data.Identifier("first"), "image", "rm", "-f", testutil.CommonImage) + helpers.Anyhow("--namespace", data.Identifier("second"), "image", "rm", "-f", testutil.CommonImage) + helpers.Anyhow("--namespace", data.Identifier("first"), "rm", "-f", data.Identifier("container1")) + helpers.Anyhow("--namespace", data.Identifier("first"), "rm", "-f", data.Identifier("container2")) + helpers.Anyhow("--namespace", data.Identifier("second"), "rm", "-f", data.Identifier("container3")) + helpers.Anyhow("--namespace", data.Identifier("first"), "volume", "rm", "-f", data.Identifier("volume1")) + helpers.Anyhow("namespace", "remove", data.Identifier("first")) + helpers.Anyhow("namespace", "remove", data.Identifier("second")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "arg missing should fail", + Command: test.Command("namespace", "inspect"), + Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), + }, + { + Description: "non existent namespace returns empty array", + Command: test.Command("namespace", "inspect", "doesnotexist"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.JSON([]native.Namespace{}, func(dc []native.Namespace, t tig.T) { + assert.Assert(t, len(dc) == 0, "expected empty array") + }), + ), + } + }, + }, + { + Description: "inspect labels", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("namespace", "inspect", data.Labels().Get("ns2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("ns2")), + expect.JSON([]native.Namespace{}, func(dc []native.Namespace, t tig.T) { + labels := *dc[0].Labels + assert.Assert(t, len(labels) == 2, fmt.Sprintf("two labels, not %d", len(labels))) + assert.Assert(t, labels["foo"] == "fooval", + fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) + assert.Assert(t, labels["bar"] == "barval", + fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) + }), + ), + } + }, + }, + { + Description: "inspect details single namespace", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("namespace", "inspect", data.Labels().Get("ns1")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("ns1")), + expect.JSON([]native.Namespace{}, func(dc []native.Namespace, t tig.T) { + assert.Assert(t, len(dc[0].Volumes.Names) == 1, fmt.Sprintf("expected 1 volume name (was %d)", len(dc[0].Volumes.Names))) + assert.Assert(t, dc[0].Containers.Count == 2, fmt.Sprintf("expected 2 container (was %d)", dc[0].Containers.Count)) + assert.Assert(t, len(dc[0].Images.IDs) == 1, fmt.Sprintf("expected 1 image (was %d)", dc[0].Images.Count)) + }), + ), + } + }, + }, + { + Description: "inspect details both namespaces", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("namespace", "inspect", data.Labels().Get("ns1"), data.Labels().Get("ns2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("ns1")), + expect.JSON([]native.Namespace{}, func(dc []native.Namespace, t tig.T) { + assert.Assert(t, len(dc[0].Volumes.Names) == 1, fmt.Sprintf("expected 1 volume (was %d)", len(dc[0].Volumes.Names))) + assert.Assert(t, dc[0].Containers.Count == 2, fmt.Sprintf("expected 2 container (was %d)", dc[0].Containers.Count)) + assert.Assert(t, len(dc[0].Images.IDs) == 1, fmt.Sprintf("expected 1 image (was %d)", dc[0].Images.Count)) + }), + + expect.Contains(data.Labels().Get("ns2")), + expect.JSON([]native.Namespace{}, func(dc []native.Namespace, t tig.T) { + assert.Assert(t, len(dc[1].Volumes.Names) == 0, fmt.Sprintf("expected 0 volume (was %d)", len(dc[1].Volumes.Names))) + assert.Assert(t, dc[1].Containers.Count == 1, fmt.Sprintf("expected 1 container (was %d)", dc[1].Containers.Count)) + assert.Assert(t, dc[1].Images.Count == 1, fmt.Sprintf("expected 1 image (was %d)", dc[1].Images.Count)) + }), + ), + } + }, + }, + } + + testCase.Run(t) +}