Skip to content

Commit 17c9a10

Browse files
committed
manifest: Add unit tests for manifest inspect command
Add tests for tag/digest references and verbose modes Signed-off-by: ChengyuZhu6 <[email protected]>
1 parent eef649f commit 17c9a10

File tree

4 files changed

+310
-10
lines changed

4 files changed

+310
-10
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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/base64"
21+
"encoding/json"
22+
"testing"
23+
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/testutil"
30+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
31+
)
32+
33+
func TestManifestInspect(t *testing.T) {
34+
testCase := nerdtest.Setup()
35+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
36+
helpers.Ensure("pull", "--quiet", "--platform=linux/amd64", testutil.AlpineImage)
37+
}
38+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
39+
helpers.Anyhow("rmi", "-f", testutil.AlpineImage)
40+
helpers.Anyhow("rmi", "-f", testutil.GetImageWithoutTag("alpine")+"@"+testutil.GetTestImageManifestDigest("alpine", "linux/amd64"))
41+
}
42+
43+
testCase.SubTests = []*test.Case{
44+
{
45+
Description: "tag-non-verbose",
46+
Command: test.Command("manifest", "inspect", testutil.AlpineImage),
47+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
48+
var manifest map[string]interface{}
49+
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))
50+
51+
validateManifestListFormat(t, manifest)
52+
53+
if manifests, ok := manifest["manifests"]; ok {
54+
manifestsArray, ok := manifests.([]interface{})
55+
assert.Assert(t, ok)
56+
assert.Assert(t, len(manifestsArray) > 0)
57+
findAndValidateAmd64ManifestInList(t, manifestsArray)
58+
}
59+
}),
60+
},
61+
{
62+
Description: "tag-verbose",
63+
Command: test.Command("manifest", "inspect", testutil.AlpineImage, "--verbose"),
64+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
65+
var dockerEntries []interface{}
66+
assert.NilError(t, json.Unmarshal([]byte(stdout), &dockerEntries))
67+
assert.Assert(t, len(dockerEntries) > 0)
68+
69+
findAndValidateAmd64DockerEntry(t, dockerEntries)
70+
}),
71+
},
72+
{
73+
Description: "digest-non-verbose",
74+
Command: test.Command("manifest", "inspect", testutil.GetImageWithoutTag("alpine")+"@"+testutil.GetTestImageManifestDigest("alpine", "linux/amd64")),
75+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
76+
var manifest map[string]interface{}
77+
assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest))
78+
79+
validateSingleManifest(t, manifest)
80+
}),
81+
},
82+
{
83+
Description: "digest-verbose",
84+
Command: test.Command("manifest", "inspect", testutil.GetImageWithoutTag("alpine")+"@"+testutil.GetTestImageManifestDigest("alpine", "linux/amd64"), "--verbose"),
85+
Expected: test.Expects(0, nil, func(stdout string, t tig.T) {
86+
var entry map[string]interface{}
87+
assert.NilError(t, json.Unmarshal([]byte(stdout), &entry))
88+
89+
validateDockerManifestEntry(t, entry, testutil.GetImageWithoutTag("alpine")+"@"+testutil.GetTestImageManifestDigest("alpine", "linux/amd64"))
90+
}),
91+
},
92+
}
93+
94+
testCase.Run(t)
95+
}
96+
97+
func validateManifestListFormat(t tig.T, manifest map[string]interface{}) {
98+
if schemaVersion, ok := manifest["schemaVersion"]; ok {
99+
assert.Equal(t, schemaVersion, 2.0) // JSON numbers are float64
100+
}
101+
}
102+
103+
func findAndValidateAmd64ManifestInList(t tig.T, manifests []interface{}) {
104+
foundAmd64 := false
105+
for _, m := range manifests {
106+
manifestEntry, ok := m.(map[string]interface{})
107+
if !ok {
108+
continue
109+
}
110+
111+
if platform, ok := manifestEntry["platform"].(map[string]interface{}); ok {
112+
if platform["architecture"] == "amd64" && platform["os"] == "linux" {
113+
if digest, ok := manifestEntry["digest"].(string); ok {
114+
assert.Equal(t, digest, testutil.GetTestImageManifestDigest("alpine", "linux/amd64"), "amd64 manifest digest should match expected value")
115+
foundAmd64 = true
116+
break
117+
}
118+
}
119+
}
120+
}
121+
assert.Assert(t, foundAmd64, "should find amd64 platform manifest")
122+
}
123+
124+
func validateDockerManifestEntryFields(t tig.T, entry map[string]interface{}) {
125+
_, hasRef := entry["Ref"]
126+
_, hasDescriptor := entry["Descriptor"]
127+
_, hasRaw := entry["Raw"]
128+
_, hasSchemaV2Manifest := entry["SchemaV2Manifest"]
129+
130+
assert.Assert(t, hasRef, "should have Ref field")
131+
assert.Assert(t, hasDescriptor, "should have Descriptor field")
132+
assert.Assert(t, hasRaw, "should have Raw field")
133+
assert.Assert(t, hasSchemaV2Manifest, "should have SchemaV2Manifest field")
134+
}
135+
136+
func findAndValidateAmd64DockerEntry(t tig.T, dockerEntries []interface{}) {
137+
foundAmd64 := false
138+
for _, e := range dockerEntries {
139+
entry, ok := e.(map[string]interface{})
140+
assert.Assert(t, ok)
141+
142+
validateDockerManifestEntryFields(t, entry)
143+
144+
if descriptor, ok := entry["Descriptor"].(map[string]interface{}); ok {
145+
if platform, ok := descriptor["platform"].(map[string]interface{}); ok {
146+
if platform["architecture"] == "amd64" && platform["os"] == "linux" {
147+
// Verify manifest digest
148+
if digest, ok := descriptor["digest"].(string); ok {
149+
assert.Equal(t, digest, testutil.GetTestImageManifestDigest("alpine", "linux/amd64"), "amd64 manifest digest should match expected value")
150+
}
151+
152+
// Verify config digest
153+
if schemaV2Manifest, ok := entry["SchemaV2Manifest"].(map[string]interface{}); ok {
154+
if config, ok := schemaV2Manifest["config"].(map[string]interface{}); ok {
155+
if configDigest, ok := config["digest"].(string); ok {
156+
assert.Equal(t, configDigest, testutil.GetTestImageConfigDigest("alpine", "linux/amd64"), "amd64 config digest should match expected value")
157+
}
158+
}
159+
}
160+
foundAmd64 = true
161+
break
162+
}
163+
}
164+
}
165+
}
166+
assert.Assert(t, foundAmd64, "should find amd64 platform entry")
167+
}
168+
169+
func validateSingleManifest(t tig.T, manifest map[string]interface{}) {
170+
if schemaVersion, ok := manifest["schemaVersion"]; ok {
171+
assert.Equal(t, schemaVersion, 2.0)
172+
}
173+
174+
assert.Equal(t, manifest["mediaType"], "application/vnd.docker.distribution.manifest.v2+json")
175+
176+
if config, ok := manifest["config"]; ok {
177+
configMap, ok := config.(map[string]interface{})
178+
assert.Assert(t, ok)
179+
180+
assert.Equal(t, configMap["digest"], testutil.GetTestImageConfigDigest("alpine", "linux/amd64"), "config digest should match expected value")
181+
assert.Equal(t, configMap["mediaType"], "application/vnd.docker.container.image.v1+json")
182+
assert.Equal(t, configMap["size"], 1472.0)
183+
}
184+
185+
if layers, ok := manifest["layers"]; ok {
186+
layersArray, ok := layers.([]interface{})
187+
assert.Assert(t, ok)
188+
assert.Assert(t, len(layersArray) > 0, "should have at least one layer")
189+
}
190+
}
191+
192+
func validateDockerManifestEntry(t tig.T, entry map[string]interface{}, expectedRef string) {
193+
validateDockerManifestEntryFields(t, entry)
194+
195+
// Verify Ref contains the specific digest
196+
if ref, ok := entry["Ref"].(string); ok {
197+
assert.Equal(t, ref, expectedRef, "Ref should match expected value")
198+
}
199+
200+
// Check descriptor
201+
if descriptor, ok := entry["Descriptor"].(map[string]interface{}); ok {
202+
assert.Equal(t, descriptor["digest"], testutil.GetTestImageManifestDigest("alpine", "linux/amd64"), "descriptor digest should match expected value")
203+
assert.Equal(t, descriptor["mediaType"], "application/vnd.docker.distribution.manifest.v2+json")
204+
assert.Equal(t, descriptor["size"], 528.0)
205+
206+
if platform, ok := descriptor["platform"].(map[string]interface{}); ok {
207+
assert.Equal(t, platform["architecture"], "amd64")
208+
assert.Equal(t, platform["os"], "linux")
209+
}
210+
}
211+
212+
// Verify SchemaV2Manifest config digest
213+
if schemaV2Manifest, ok := entry["SchemaV2Manifest"].(map[string]interface{}); ok {
214+
if config, ok := schemaV2Manifest["config"].(map[string]interface{}); ok {
215+
assert.Equal(t, config["digest"], testutil.GetTestImageConfigDigest("alpine", "linux/amd64"), "config digest should match expected value")
216+
assert.Equal(t, config["mediaType"], "application/vnd.docker.container.image.v1+json")
217+
assert.Equal(t, config["size"], 1472.0)
218+
}
219+
}
220+
221+
// Verify Raw field decodes correctly
222+
if raw, ok := entry["Raw"].(string); ok {
223+
decodedRaw, err := base64.StdEncoding.DecodeString(raw)
224+
assert.NilError(t, err)
225+
226+
var decodedManifest map[string]interface{}
227+
assert.NilError(t, json.Unmarshal(decodedRaw, &decodedManifest))
228+
229+
if decodedConfig, ok := decodedManifest["config"].(map[string]interface{}); ok {
230+
assert.Equal(t, decodedConfig["digest"], testutil.GetTestImageConfigDigest("alpine", "linux/amd64"), "decoded config digest should match expected value")
231+
}
232+
}
233+
}

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+
}

pkg/testutil/images.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ alpine:
77
tag: "3.13-org"
88
digest: "sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a"
99
variants: ["linux/amd64", "linux/arm64"]
10+
platformDigests:
11+
linux/amd64:
12+
config: "sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c"
13+
manifest: "sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310"
1014

1115
busybox:
1216
ref: "ghcr.io/containerd/busybox"

pkg/testutil/images_linux.go

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,66 @@ var rawImagesList string
2929

3030
var testImagesOnce sync.Once
3131

32+
type PlatformDigest struct {
33+
Config string `yaml:"config,omitempty"`
34+
Manifest string `yaml:"manifest,omitempty"`
35+
}
36+
3237
type TestImage struct {
33-
Ref string `yaml:"ref"`
34-
Tag string `yaml:"tag,omitempty"`
35-
Digest string `yaml:"digest,omitempty"`
36-
Variants []string `yaml:"variants,omitempty"`
38+
Ref string `yaml:"ref"`
39+
Tag string `yaml:"tag,omitempty"`
40+
Digest string `yaml:"digest,omitempty"`
41+
Variants []string `yaml:"variants,omitempty"`
42+
PlatformDigests map[string]PlatformDigest `yaml:"platformDigests,omitempty"`
3743
}
3844

3945
var testImages map[string]TestImage
4046

41-
func getImage(key string) string {
47+
// internal helper to lookup TestImage by key, panics if not found
48+
func lookup(key string) TestImage {
4249
testImagesOnce.Do(func() {
4350
if err := yaml.Unmarshal([]byte(rawImagesList), &testImages); err != nil {
4451
fmt.Printf("Error unmarshaling test images YAML file: %v\n", err)
4552
panic("testing is broken")
4653
}
4754
})
48-
49-
var im TestImage
50-
var ok bool
51-
52-
if im, ok = testImages[key]; !ok {
55+
im, ok := testImages[key]
56+
if !ok {
5357
fmt.Printf("Image %s was not found in images list\n", key)
5458
panic("testing is broken")
5559
}
60+
return im
61+
}
5662

63+
func getImage(key string) string {
64+
im := lookup(key)
5765
return im.Ref + ":" + im.Tag
5866
}
67+
68+
func GetImageWithoutTag(key string) string {
69+
im := lookup(key)
70+
return im.Ref
71+
}
72+
73+
func GetTestImageConfigDigest(key, platform string) string {
74+
im := lookup(key)
75+
pd, ok := im.PlatformDigests[platform]
76+
if !ok {
77+
panic(fmt.Sprintf("platform %s not found for image %s", platform, key))
78+
}
79+
return pd.Config
80+
}
81+
82+
func GetTestImageManifestDigest(key, platform string) string {
83+
im := lookup(key)
84+
pd, ok := im.PlatformDigests[platform]
85+
if !ok {
86+
panic(fmt.Sprintf("platform %s not found for image %s", platform, key))
87+
}
88+
return pd.Manifest
89+
}
90+
91+
func GetTestImageDigest(key string) string {
92+
im := lookup(key)
93+
return im.Digest
94+
}

0 commit comments

Comments
 (0)