Skip to content

Commit 2ef0b53

Browse files
committed
integration/save: Add tests checking OCI archive output
Signed-off-by: Paweł Gronowski <[email protected]>
1 parent 5e13f54 commit 2ef0b53

4 files changed

Lines changed: 338 additions & 26 deletions

File tree

integration/image/save_test.go

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/docker/docker/api/types/versions"
1919
"github.com/docker/docker/integration/internal/build"
2020
"github.com/docker/docker/integration/internal/container"
21+
"github.com/docker/docker/internal/testutils"
22+
"github.com/docker/docker/internal/testutils/specialimage"
2123
"github.com/docker/docker/pkg/archive"
2224
"github.com/docker/docker/testutil/fakecontext"
2325
"github.com/opencontainers/go-digest"
@@ -88,45 +90,126 @@ func TestSaveCheckTimes(t *testing.T) {
8890
}
8991

9092
// Regression test for https://github.com/moby/moby/issues/47065
91-
func TestSaveCheckManifestLayers(t *testing.T) {
93+
func TestSaveOCI(t *testing.T) {
9294
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25")
9395

9496
ctx := setupTest(t)
9597
client := testEnv.APIClient()
9698

97-
t.Parallel()
98-
99-
const repoName = "busybox:latest"
100-
img, _, err := client.ImageInspectWithRaw(ctx, repoName)
99+
const busybox = "busybox:latest"
100+
inspectBusybox, _, err := client.ImageInspectWithRaw(ctx, busybox)
101101
assert.NilError(t, err)
102102

103-
rdr, err := client.ImageSave(ctx, []string{repoName})
104-
assert.NilError(t, err)
103+
type testCase struct {
104+
image string
105+
expectedOCIRef string
106+
expectedContainerdRef string
107+
}
105108

106-
tarfs := tarIndexFS(t, rdr)
109+
testCases := []testCase{
110+
// Busybox by tagged name
111+
testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
107112

108-
indexData, err := fs.ReadFile(tarfs, "index.json")
109-
assert.NilError(t, err)
113+
// Busybox by ID
114+
testCase{image: inspectBusybox.ID},
115+
}
110116

111-
var index ocispec.Index
112-
assert.NilError(t, json.Unmarshal(indexData, &index))
117+
if testEnv.DaemonInfo.OSType != "windows" {
118+
multiLayerImage := specialimage.Load(ctx, t, client, specialimage.MultiLayer)
119+
// Multi-layer image
120+
testCases = append(testCases, testCase{image: multiLayerImage, expectedContainerdRef: "docker.io/library/multilayer:latest", expectedOCIRef: "latest"})
113121

114-
assert.Assert(t, is.Len(index.Manifests, 1))
122+
}
115123

116-
manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
117-
assert.NilError(t, err)
124+
// Busybox frozen image will have empty RepoDigests when loaded into the
125+
// graphdriver image store so we can't use it.
126+
// This will work with the containerd image store though.
127+
if len(inspectBusybox.RepoDigests) > 0 {
128+
// Digested reference
129+
testCases = append(testCases, testCase{
130+
image: inspectBusybox.RepoDigests[0],
131+
})
132+
}
118133

119-
var manifest ocispec.Manifest
120-
assert.NilError(t, json.Unmarshal(manifestData, &manifest))
134+
for _, tc := range testCases {
135+
tc := tc
136+
t.Run(tc.image, func(t *testing.T) {
137+
// Get information about the original image.
138+
inspect, _, err := client.ImageInspectWithRaw(ctx, tc.image)
139+
assert.NilError(t, err)
121140

122-
assert.Check(t, is.Len(manifest.Layers, len(img.RootFS.Layers)))
123-
for _, l := range manifest.Layers {
124-
stat, err := fs.Stat(tarfs, "blobs/sha256/"+l.Digest.Encoded())
125-
if !assert.Check(t, err) {
126-
continue
127-
}
141+
rdr, err := client.ImageSave(ctx, []string{tc.image})
142+
assert.NilError(t, err)
143+
defer rdr.Close()
144+
145+
tarfs := tarIndexFS(t, rdr)
146+
147+
indexData, err := fs.ReadFile(tarfs, "index.json")
148+
assert.NilError(t, err, "failed to read index.json")
149+
150+
var index ocispec.Index
151+
assert.NilError(t, json.Unmarshal(indexData, &index), "failed to unmarshal index.json")
128152

129-
assert.Check(t, is.Equal(l.Size, stat.Size()))
153+
// All test images are single-platform, so they should have only one manifest.
154+
assert.Assert(t, is.Len(index.Manifests, 1))
155+
156+
manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
157+
assert.NilError(t, err)
158+
159+
var manifest ocispec.Manifest
160+
assert.NilError(t, json.Unmarshal(manifestData, &manifest))
161+
162+
t.Run("Manifest", func(t *testing.T) {
163+
assert.Check(t, is.Len(manifest.Layers, len(inspect.RootFS.Layers)))
164+
165+
var digests []string
166+
// Check if layers referenced by the manifest exist in the archive
167+
// and match the layers from the original image.
168+
for _, l := range manifest.Layers {
169+
layerPath := "blobs/sha256/" + l.Digest.Encoded()
170+
stat, err := fs.Stat(tarfs, layerPath)
171+
assert.NilError(t, err)
172+
173+
assert.Check(t, is.Equal(l.Size, stat.Size()))
174+
175+
f, err := tarfs.Open(layerPath)
176+
assert.NilError(t, err)
177+
178+
layerDigest, err := testutils.UncompressedTarDigest(f)
179+
f.Close()
180+
181+
assert.NilError(t, err)
182+
183+
digests = append(digests, layerDigest.String())
184+
}
185+
186+
assert.Check(t, is.DeepEqual(digests, inspect.RootFS.Layers))
187+
})
188+
189+
t.Run("Config", func(t *testing.T) {
190+
configData, err := fs.ReadFile(tarfs, "blobs/sha256/"+manifest.Config.Digest.Encoded())
191+
assert.NilError(t, err)
192+
193+
var config ocispec.Image
194+
assert.NilError(t, json.Unmarshal(configData, &config))
195+
196+
var diffIDs []string
197+
for _, l := range config.RootFS.DiffIDs {
198+
diffIDs = append(diffIDs, l.String())
199+
}
200+
201+
assert.Check(t, is.DeepEqual(diffIDs, inspect.RootFS.Layers))
202+
})
203+
204+
t.Run("Containerd image name", func(t *testing.T) {
205+
assert.Check(t, is.Equal(index.Manifests[0].Annotations["io.containerd.image.name"], tc.expectedContainerdRef))
206+
})
207+
208+
t.Run("OCI reference tag", func(t *testing.T) {
209+
assert.Check(t, is.Equal(index.Manifests[0].Annotations["org.opencontainers.image.ref.name"], tc.expectedOCIRef))
210+
})
211+
212+
})
130213
}
131214
}
132215

internal/testutils/archive.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package testutils
2+
3+
import (
4+
"io"
5+
6+
"github.com/docker/docker/pkg/archive"
7+
"github.com/opencontainers/go-digest"
8+
)
9+
10+
// UncompressedTarDigest returns the canonical digest of the uncompressed tar stream.
11+
func UncompressedTarDigest(compressedTar io.Reader) (digest.Digest, error) {
12+
rd, err := archive.DecompressStream(compressedTar)
13+
if err != nil {
14+
return "", err
15+
}
16+
17+
defer rd.Close()
18+
19+
digester := digest.Canonical.Digester()
20+
if _, err := io.Copy(digester.Hash(), rd); err != nil {
21+
return "", err
22+
}
23+
return digester.Digest(), nil
24+
}

internal/testutils/specialimage/load.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package specialimage
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"errors"
@@ -41,7 +42,10 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu
4142
t.Fatalf("Failed load: %s", string(respBody))
4243
}
4344

44-
decoder := json.NewDecoder(resp.Body)
45+
all, err := io.ReadAll(resp.Body)
46+
assert.NilError(t, err)
47+
48+
decoder := json.NewDecoder(bytes.NewReader(all))
4549
for {
4650
var msg jsonmessage.JSONMessage
4751
err := decoder.Decode(&msg)
@@ -61,6 +65,6 @@ func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFu
6165
}
6266
}
6367

64-
t.Fatal("failed to read image ID")
68+
t.Fatalf("failed to read image ID\n%s", string(all))
6569
return ""
6670
}

0 commit comments

Comments
 (0)