Skip to content
Merged
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
4 changes: 4 additions & 0 deletions cmd/nerdctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/containerd/nerdctl/v2/cmd/nerdctl/internal"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/login"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
Expand Down Expand Up @@ -344,6 +345,9 @@ Config file ($NERDCTL_TOML): %s

// IPFS
ipfs.NewIPFSCommand(),

// Manifest
manifest.Command(),
)
addApparmorCommand(rootCmd)
container.AddCpCommand(rootCmd)
Expand Down
40 changes: 40 additions & 0 deletions cmd/nerdctl/manifest/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
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 manifest

import (
"github.com/spf13/cobra"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)

func Command() *cobra.Command {
cmd := &cobra.Command{
Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "manifest",
Short: "Manage image manifests.",
RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}

cmd.AddCommand(
InspectCommand(),
)

return cmd
}
95 changes: 95 additions & 0 deletions cmd/nerdctl/manifest/manifest_inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
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 manifest

import (
"fmt"

"github.com/spf13/cobra"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/cmd/manifest"
"github.com/containerd/nerdctl/v2/pkg/formatter"
)

func InspectCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "inspect MANIFEST",
Short: "Display the contents of a manifest or image index/manifest list",
Args: cobra.MinimumNArgs(1),
RunE: inspectAction,
ValidArgsFunction: inspectShellComplete,
SilenceUsage: true,
SilenceErrors: true,
}
cmd.Flags().Bool("verbose", false, "Verbose output additional info including layers and platform")
cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry")
return cmd
}

func processInspectFlags(cmd *cobra.Command) (types.ManifestInspectOptions, error) {
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ManifestInspectOptions{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return types.ManifestInspectOptions{}, err
}
insecure, err := cmd.Flags().GetBool("insecure")
if err != nil {
return types.ManifestInspectOptions{}, err
}
return types.ManifestInspectOptions{
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
Verbose: verbose,
Insecure: insecure,
}, nil
}

func inspectAction(cmd *cobra.Command, args []string) error {
inspectOptions, err := processInspectFlags(cmd)
if err != nil {
return err
}
rawRef := args[0]
res, err := manifest.Inspect(cmd.Context(), rawRef, inspectOptions)
if err != nil {
return err
}

// Output format: single object for single result, array for multiple results
if len(res) == 1 {
jsonStr, err := formatter.ToJSON(res[0], "", " ")
if err != nil {
return err
}
fmt.Fprint(inspectOptions.Stdout, jsonStr)
} else {
if formatErr := formatter.FormatSlice("", inspectOptions.Stdout, res); formatErr != nil {
return formatErr
}
}
return nil
}

func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completion.ImageNames(cmd)
}
149 changes: 149 additions & 0 deletions cmd/nerdctl/manifest/manifest_inspect_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
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 manifest

import (
"encoding/json"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/mod/tigron/test"
"github.com/containerd/nerdctl/mod/tigron/tig"

"github.com/containerd/nerdctl/v2/pkg/manifesttypes"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)

const (
testImageName = "alpine"
testPlatform = "linux/amd64"
)

type testData struct {
imageName string
platform string
imageRef string
manifestDigest string
configDigest string
rawData string
}

func newTestData(imageName, platform string) *testData {
return &testData{
imageName: imageName,
platform: platform,
imageRef: testutil.GetTestImage(imageName),
manifestDigest: testutil.GetTestImageManifestDigest(imageName, platform),
configDigest: testutil.GetTestImageConfigDigest(imageName, platform),
rawData: testutil.GetTestImageRaw(imageName, platform),
}
}

func (td *testData) imageWithDigest() string {
return testutil.GetTestImageWithoutTag(td.imageName) + "@" + td.manifestDigest
}

func (td *testData) isAmd64Platform(platform *ocispec.Platform) bool {
return platform != nil &&
platform.Architecture == "amd64" &&
platform.OS == "linux"
}

func TestManifestInspect(t *testing.T) {
testCase := nerdtest.Setup()
td := newTestData(testImageName, testPlatform)

testCase.SubTests = []*test.Case{
{
Description: "tag-non-verbose",
Command: test.Command("manifest", "inspect", td.imageRef),
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
var manifest manifesttypes.DockerManifestListStruct
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))

assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName))
assert.Equal(t, manifest.MediaType, testutil.GetTestImageMediaType(td.imageName))
assert.Assert(t, len(manifest.Manifests) > 0)

var foundManifest *ocispec.Descriptor
for _, m := range manifest.Manifests {
if td.isAmd64Platform(m.Platform) {
foundManifest = &m
break
}
}
assert.Assert(t, foundManifest != nil, "should find amd64 platform manifest")
assert.Equal(t, foundManifest.Digest.String(), td.manifestDigest)
assert.Equal(t, foundManifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
}),
},
{
Description: "tag-verbose",
Command: test.Command("manifest", "inspect", td.imageRef, "--verbose"),
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
var entries []manifesttypes.DockerManifestEntry
assert.NilError(t, json.Unmarshal([]byte(stdout), &entries))
assert.Assert(t, len(entries) > 0)

var foundEntry *manifesttypes.DockerManifestEntry
for _, e := range entries {
if td.isAmd64Platform(e.Descriptor.Platform) {
foundEntry = &e
break
}
}
assert.Assert(t, foundEntry != nil, "should find amd64 platform entry")

expectedRef := td.imageRef + "@" + td.manifestDigest
assert.Equal(t, foundEntry.Ref, expectedRef)
assert.Equal(t, foundEntry.Descriptor.Digest.String(), td.manifestDigest)
assert.Equal(t, foundEntry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
assert.Equal(t, foundEntry.Raw, td.rawData)
}),
},
{
Description: "digest-non-verbose",
Command: test.Command("manifest", "inspect", td.imageWithDigest()),
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
var manifest manifesttypes.DockerManifestStruct
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))

assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName))
assert.Equal(t, manifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
assert.Equal(t, manifest.Config.Digest.String(), td.configDigest)
}),
},
{
Description: "digest-verbose",
Command: test.Command("manifest", "inspect", td.imageWithDigest(), "--verbose"),
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
var entry manifesttypes.DockerManifestEntry
assert.NilError(t, json.Unmarshal([]byte(stdout), &entry))

assert.Equal(t, entry.Ref, td.imageWithDigest())
assert.Equal(t, entry.Descriptor.Digest.String(), td.manifestDigest)
assert.Equal(t, entry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
assert.Equal(t, entry.Raw, td.rawData)
}),
},
}

testCase.Run(t)
}
27 changes: 27 additions & 0 deletions cmd/nerdctl/manifest/manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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 manifest

import (
"testing"

"github.com/containerd/nerdctl/v2/pkg/testutil"
)

func TestMain(m *testing.M) {
testutil.M(m)
}
27 changes: 27 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ It does not necessarily mean that the corresponding features are missing in cont
- [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert)
- [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt)
- [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt)
- [Manifest management](#manifest-management)
- [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect)
- [Registry](#registry)
- [:whale: nerdctl login](#whale-nerdctl-login)
- [:whale: nerdctl logout](#whale-nerdctl-logout)
Expand Down Expand Up @@ -1035,6 +1037,31 @@ Flags:
- `--platform=<PLATFORM>` : Convert content for a specific platform
- `--all-platforms` : Convert content for all platforms (default: false)

## Manifest management

### :whale: nerdctl manifest inspect

Display the contents of a manifest list or manifest.

Usage: `nerdctl manifest inspect [OPTIONS] MANIFEST`

#### Input formats

You can specify the manifest to inspect using one of the following formats:
- **Image name with tag**: `alpine:3.22.1`
- **Image name with digest**: `alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f`

Flags:

- `--verbose` : Verbose output, show additional info including layers and platform
- `--insecure`: Allow communication with an insecure registry
Example:

```bash
nerdctl manifest inspect alpine:3.22.1
nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f
```

## Registry

### :whale: nerdctl login
Expand Down
Loading