Skip to content

Commit 5ab6227

Browse files
authored
Merge pull request #4413 from ChengyuZhu6/manifest
cmd: support nerdctl manifeset inspect
2 parents 34c9345 + ca2ad1b commit 5ab6227

File tree

12 files changed

+845
-24
lines changed

12 files changed

+845
-24
lines changed

cmd/nerdctl/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/containerd/nerdctl/v2/cmd/nerdctl/internal"
4141
"github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs"
4242
"github.com/containerd/nerdctl/v2/cmd/nerdctl/login"
43+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest"
4344
"github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
4445
"github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
4546
"github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
@@ -344,6 +345,9 @@ Config file ($NERDCTL_TOML): %s
344345

345346
// IPFS
346347
ipfs.NewIPFSCommand(),
348+
349+
// Manifest
350+
manifest.Command(),
347351
)
348352
addApparmorCommand(rootCmd)
349353
container.AddCpCommand(rootCmd)

cmd/nerdctl/manifest/manifest.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
23+
)
24+
25+
func Command() *cobra.Command {
26+
cmd := &cobra.Command{
27+
Annotations: map[string]string{helpers.Category: helpers.Management},
28+
Use: "manifest",
29+
Short: "Manage image manifests.",
30+
RunE: helpers.UnknownSubcommandAction,
31+
SilenceUsage: true,
32+
SilenceErrors: true,
33+
}
34+
35+
cmd.AddCommand(
36+
InspectCommand(),
37+
)
38+
39+
return cmd
40+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
25+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
26+
"github.com/containerd/nerdctl/v2/pkg/api/types"
27+
"github.com/containerd/nerdctl/v2/pkg/cmd/manifest"
28+
"github.com/containerd/nerdctl/v2/pkg/formatter"
29+
)
30+
31+
func InspectCommand() *cobra.Command {
32+
var cmd = &cobra.Command{
33+
Use: "inspect MANIFEST",
34+
Short: "Display the contents of a manifest or image index/manifest list",
35+
Args: cobra.MinimumNArgs(1),
36+
RunE: inspectAction,
37+
ValidArgsFunction: inspectShellComplete,
38+
SilenceUsage: true,
39+
SilenceErrors: true,
40+
}
41+
cmd.Flags().Bool("verbose", false, "Verbose output additional info including layers and platform")
42+
cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry")
43+
return cmd
44+
}
45+
46+
func processInspectFlags(cmd *cobra.Command) (types.ManifestInspectOptions, error) {
47+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
48+
if err != nil {
49+
return types.ManifestInspectOptions{}, err
50+
}
51+
verbose, err := cmd.Flags().GetBool("verbose")
52+
if err != nil {
53+
return types.ManifestInspectOptions{}, err
54+
}
55+
insecure, err := cmd.Flags().GetBool("insecure")
56+
if err != nil {
57+
return types.ManifestInspectOptions{}, err
58+
}
59+
return types.ManifestInspectOptions{
60+
Stdout: cmd.OutOrStdout(),
61+
GOptions: globalOptions,
62+
Verbose: verbose,
63+
Insecure: insecure,
64+
}, nil
65+
}
66+
67+
func inspectAction(cmd *cobra.Command, args []string) error {
68+
inspectOptions, err := processInspectFlags(cmd)
69+
if err != nil {
70+
return err
71+
}
72+
rawRef := args[0]
73+
res, err := manifest.Inspect(cmd.Context(), rawRef, inspectOptions)
74+
if err != nil {
75+
return err
76+
}
77+
78+
// Output format: single object for single result, array for multiple results
79+
if len(res) == 1 {
80+
jsonStr, err := formatter.ToJSON(res[0], "", " ")
81+
if err != nil {
82+
return err
83+
}
84+
fmt.Fprint(inspectOptions.Stdout, jsonStr)
85+
} else {
86+
if formatErr := formatter.FormatSlice("", inspectOptions.Stdout, res); formatErr != nil {
87+
return formatErr
88+
}
89+
}
90+
return nil
91+
}
92+
93+
func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
94+
return completion.ImageNames(cmd)
95+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"encoding/json"
21+
"testing"
22+
23+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24+
"gotest.tools/v3/assert"
25+
26+
"github.com/containerd/nerdctl/mod/tigron/test"
27+
"github.com/containerd/nerdctl/mod/tigron/tig"
28+
29+
"github.com/containerd/nerdctl/v2/pkg/manifesttypes"
30+
"github.com/containerd/nerdctl/v2/pkg/testutil"
31+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
32+
)
33+
34+
const (
35+
testImageName = "alpine"
36+
testPlatform = "linux/amd64"
37+
)
38+
39+
type testData struct {
40+
imageName string
41+
platform string
42+
imageRef string
43+
manifestDigest string
44+
configDigest string
45+
rawData string
46+
}
47+
48+
func newTestData(imageName, platform string) *testData {
49+
return &testData{
50+
imageName: imageName,
51+
platform: platform,
52+
imageRef: testutil.GetTestImage(imageName),
53+
manifestDigest: testutil.GetTestImageManifestDigest(imageName, platform),
54+
configDigest: testutil.GetTestImageConfigDigest(imageName, platform),
55+
rawData: testutil.GetTestImageRaw(imageName, platform),
56+
}
57+
}
58+
59+
func (td *testData) imageWithDigest() string {
60+
return testutil.GetTestImageWithoutTag(td.imageName) + "@" + td.manifestDigest
61+
}
62+
63+
func (td *testData) isAmd64Platform(platform *ocispec.Platform) bool {
64+
return platform != nil &&
65+
platform.Architecture == "amd64" &&
66+
platform.OS == "linux"
67+
}
68+
69+
func TestManifestInspect(t *testing.T) {
70+
testCase := nerdtest.Setup()
71+
td := newTestData(testImageName, testPlatform)
72+
73+
testCase.SubTests = []*test.Case{
74+
{
75+
Description: "tag-non-verbose",
76+
Command: test.Command("manifest", "inspect", td.imageRef),
77+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
78+
var manifest manifesttypes.DockerManifestListStruct
79+
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))
80+
81+
assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName))
82+
assert.Equal(t, manifest.MediaType, testutil.GetTestImageMediaType(td.imageName))
83+
assert.Assert(t, len(manifest.Manifests) > 0)
84+
85+
var foundManifest *ocispec.Descriptor
86+
for _, m := range manifest.Manifests {
87+
if td.isAmd64Platform(m.Platform) {
88+
foundManifest = &m
89+
break
90+
}
91+
}
92+
assert.Assert(t, foundManifest != nil, "should find amd64 platform manifest")
93+
assert.Equal(t, foundManifest.Digest.String(), td.manifestDigest)
94+
assert.Equal(t, foundManifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
95+
}),
96+
},
97+
{
98+
Description: "tag-verbose",
99+
Command: test.Command("manifest", "inspect", td.imageRef, "--verbose"),
100+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
101+
var entries []manifesttypes.DockerManifestEntry
102+
assert.NilError(t, json.Unmarshal([]byte(stdout), &entries))
103+
assert.Assert(t, len(entries) > 0)
104+
105+
var foundEntry *manifesttypes.DockerManifestEntry
106+
for _, e := range entries {
107+
if td.isAmd64Platform(e.Descriptor.Platform) {
108+
foundEntry = &e
109+
break
110+
}
111+
}
112+
assert.Assert(t, foundEntry != nil, "should find amd64 platform entry")
113+
114+
expectedRef := td.imageRef + "@" + td.manifestDigest
115+
assert.Equal(t, foundEntry.Ref, expectedRef)
116+
assert.Equal(t, foundEntry.Descriptor.Digest.String(), td.manifestDigest)
117+
assert.Equal(t, foundEntry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
118+
assert.Equal(t, foundEntry.Raw, td.rawData)
119+
}),
120+
},
121+
{
122+
Description: "digest-non-verbose",
123+
Command: test.Command("manifest", "inspect", td.imageWithDigest()),
124+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
125+
var manifest manifesttypes.DockerManifestStruct
126+
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))
127+
128+
assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName))
129+
assert.Equal(t, manifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
130+
assert.Equal(t, manifest.Config.Digest.String(), td.configDigest)
131+
}),
132+
},
133+
{
134+
Description: "digest-verbose",
135+
Command: test.Command("manifest", "inspect", td.imageWithDigest(), "--verbose"),
136+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
137+
var entry manifesttypes.DockerManifestEntry
138+
assert.NilError(t, json.Unmarshal([]byte(stdout), &entry))
139+
140+
assert.Equal(t, entry.Ref, td.imageWithDigest())
141+
assert.Equal(t, entry.Descriptor.Digest.String(), td.manifestDigest)
142+
assert.Equal(t, entry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform))
143+
assert.Equal(t, entry.Raw, td.rawData)
144+
}),
145+
},
146+
}
147+
148+
testCase.Run(t)
149+
}

cmd/nerdctl/manifest/manifest_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"testing"
21+
22+
"github.com/containerd/nerdctl/v2/pkg/testutil"
23+
)
24+
25+
func TestMain(m *testing.M) {
26+
testutil.M(m)
27+
}

docs/command-reference.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ It does not necessarily mean that the corresponding features are missing in cont
5151
- [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert)
5252
- [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt)
5353
- [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt)
54+
- [Manifest management](#manifest-management)
55+
- [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect)
5456
- [Registry](#registry)
5557
- [:whale: nerdctl login](#whale-nerdctl-login)
5658
- [:whale: nerdctl logout](#whale-nerdctl-logout)
@@ -1035,6 +1037,31 @@ Flags:
10351037
- `--platform=<PLATFORM>` : Convert content for a specific platform
10361038
- `--all-platforms` : Convert content for all platforms (default: false)
10371039

1040+
## Manifest management
1041+
1042+
### :whale: nerdctl manifest inspect
1043+
1044+
Display the contents of a manifest list or manifest.
1045+
1046+
Usage: `nerdctl manifest inspect [OPTIONS] MANIFEST`
1047+
1048+
#### Input formats
1049+
1050+
You can specify the manifest to inspect using one of the following formats:
1051+
- **Image name with tag**: `alpine:3.22.1`
1052+
- **Image name with digest**: `alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f`
1053+
1054+
Flags:
1055+
1056+
- `--verbose` : Verbose output, show additional info including layers and platform
1057+
- `--insecure`: Allow communication with an insecure registry
1058+
Example:
1059+
1060+
```bash
1061+
nerdctl manifest inspect alpine:3.22.1
1062+
nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f
1063+
```
1064+
10381065
## Registry
10391066

10401067
### :whale: nerdctl login

0 commit comments

Comments
 (0)