Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions cmd/nerdctl/namespace/namespace_inspect_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 2 additions & 1 deletion cmd/nerdctl/namespace/namespace_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
23 changes: 22 additions & 1 deletion pkg/cmd/namespace/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
121 changes: 114 additions & 7 deletions pkg/cmd/namespace/inspect.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will make inspect slower, no?

Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,135 @@ package namespace

import (
"context"
"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 {
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
}

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[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 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 formatter.FormatSlice(options.Format, options.Stdout, result)

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
}

ids := make([]string, 0, len(images))

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
}

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
}
4 changes: 4 additions & 0 deletions pkg/cmd/namespace/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions pkg/inspecttypes/native/namespace.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why include IDs?

Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Loading