moby/integration/image/save_test.go
Paweł Gronowski 2ef0b53e51
integration/save: Add tests checking OCI archive output
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-02-05 11:17:58 +01:00

365 lines
10 KiB
Go

package image
import (
"archive/tar"
"encoding/json"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/cpuguy83/tar2go"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/integration/internal/build"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/internal/testutils"
"github.com/docker/docker/internal/testutils/specialimage"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/testutil/fakecontext"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
type imageSaveManifestEntry struct {
Config string
RepoTags []string
Layers []string
}
func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS {
t.Helper()
dir := t.TempDir()
f, err := os.Create(filepath.Join(dir, "image.tar"))
assert.NilError(t, err)
// Do not close at the end of this function otherwise the indexer won't work
t.Cleanup(func() { f.Close() })
_, err = io.Copy(f, rdr)
assert.NilError(t, err)
return tar2go.NewIndex(f).FS()
}
func TestSaveCheckTimes(t *testing.T) {
ctx := setupTest(t)
t.Parallel()
client := testEnv.APIClient()
const repoName = "busybox:latest"
img, _, err := client.ImageInspectWithRaw(ctx, repoName)
assert.NilError(t, err)
rdr, err := client.ImageSave(ctx, []string{repoName})
assert.NilError(t, err)
tarfs := tarIndexFS(t, rdr)
dt, err := fs.ReadFile(tarfs, "manifest.json")
assert.NilError(t, err)
var ls []imageSaveManifestEntry
assert.NilError(t, json.Unmarshal(dt, &ls))
assert.Assert(t, cmp.Len(ls, 1))
info, err := fs.Stat(tarfs, ls[0].Config)
assert.NilError(t, err)
created, err := time.Parse(time.RFC3339, img.Created)
assert.NilError(t, err)
if testEnv.UsingSnapshotter() {
// containerd archive export sets the mod time to zero.
assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0)))
} else {
assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339)))
}
}
// Regression test for https://github.com/moby/moby/issues/47065
func TestSaveOCI(t *testing.T) {
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25")
ctx := setupTest(t)
client := testEnv.APIClient()
const busybox = "busybox:latest"
inspectBusybox, _, err := client.ImageInspectWithRaw(ctx, busybox)
assert.NilError(t, err)
type testCase struct {
image string
expectedOCIRef string
expectedContainerdRef string
}
testCases := []testCase{
// Busybox by tagged name
testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
// Busybox by ID
testCase{image: inspectBusybox.ID},
}
if testEnv.DaemonInfo.OSType != "windows" {
multiLayerImage := specialimage.Load(ctx, t, client, specialimage.MultiLayer)
// Multi-layer image
testCases = append(testCases, testCase{image: multiLayerImage, expectedContainerdRef: "docker.io/library/multilayer:latest", expectedOCIRef: "latest"})
}
// Busybox frozen image will have empty RepoDigests when loaded into the
// graphdriver image store so we can't use it.
// This will work with the containerd image store though.
if len(inspectBusybox.RepoDigests) > 0 {
// Digested reference
testCases = append(testCases, testCase{
image: inspectBusybox.RepoDigests[0],
})
}
for _, tc := range testCases {
tc := tc
t.Run(tc.image, func(t *testing.T) {
// Get information about the original image.
inspect, _, err := client.ImageInspectWithRaw(ctx, tc.image)
assert.NilError(t, err)
rdr, err := client.ImageSave(ctx, []string{tc.image})
assert.NilError(t, err)
defer rdr.Close()
tarfs := tarIndexFS(t, rdr)
indexData, err := fs.ReadFile(tarfs, "index.json")
assert.NilError(t, err, "failed to read index.json")
var index ocispec.Index
assert.NilError(t, json.Unmarshal(indexData, &index), "failed to unmarshal index.json")
// All test images are single-platform, so they should have only one manifest.
assert.Assert(t, is.Len(index.Manifests, 1))
manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded())
assert.NilError(t, err)
var manifest ocispec.Manifest
assert.NilError(t, json.Unmarshal(manifestData, &manifest))
t.Run("Manifest", func(t *testing.T) {
assert.Check(t, is.Len(manifest.Layers, len(inspect.RootFS.Layers)))
var digests []string
// Check if layers referenced by the manifest exist in the archive
// and match the layers from the original image.
for _, l := range manifest.Layers {
layerPath := "blobs/sha256/" + l.Digest.Encoded()
stat, err := fs.Stat(tarfs, layerPath)
assert.NilError(t, err)
assert.Check(t, is.Equal(l.Size, stat.Size()))
f, err := tarfs.Open(layerPath)
assert.NilError(t, err)
layerDigest, err := testutils.UncompressedTarDigest(f)
f.Close()
assert.NilError(t, err)
digests = append(digests, layerDigest.String())
}
assert.Check(t, is.DeepEqual(digests, inspect.RootFS.Layers))
})
t.Run("Config", func(t *testing.T) {
configData, err := fs.ReadFile(tarfs, "blobs/sha256/"+manifest.Config.Digest.Encoded())
assert.NilError(t, err)
var config ocispec.Image
assert.NilError(t, json.Unmarshal(configData, &config))
var diffIDs []string
for _, l := range config.RootFS.DiffIDs {
diffIDs = append(diffIDs, l.String())
}
assert.Check(t, is.DeepEqual(diffIDs, inspect.RootFS.Layers))
})
t.Run("Containerd image name", func(t *testing.T) {
assert.Check(t, is.Equal(index.Manifests[0].Annotations["io.containerd.image.name"], tc.expectedContainerdRef))
})
t.Run("OCI reference tag", func(t *testing.T) {
assert.Check(t, is.Equal(index.Manifests[0].Annotations["org.opencontainers.image.ref.name"], tc.expectedOCIRef))
})
})
}
}
func TestSaveRepoWithMultipleImages(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
makeImage := func(from string, tag string) string {
id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) {
cfg.Config.Image = from
cfg.Config.Cmd = []string{"true"}
})
res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag})
assert.NilError(t, err)
err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
assert.NilError(t, err)
return res.ID
}
busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
assert.NilError(t, err)
const repoName = "foobar-save-multi-images-test"
const tagFoo = repoName + ":foo"
const tagBar = repoName + ":bar"
idFoo := makeImage("busybox:latest", tagFoo)
idBar := makeImage("busybox:latest", tagBar)
idBusybox := busyboxImg.ID
rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"})
assert.NilError(t, err)
defer rdr.Close()
tarfs := tarIndexFS(t, rdr)
dt, err := fs.ReadFile(tarfs, "manifest.json")
assert.NilError(t, err)
var mfstLs []imageSaveManifestEntry
assert.NilError(t, json.Unmarshal(dt, &mfstLs))
actual := make([]string, 0, len(mfstLs))
for _, m := range mfstLs {
actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/"))
// make sure the blob actually exists
_, err = fs.Stat(tarfs, m.Config)
assert.Check(t, err)
}
expected := []string{idBusybox, idFoo, idBar}
// prefixes are not in tar
for i := range expected {
expected[i] = digest.Digest(expected[i]).Encoded()
}
// With snapshotters, ID of the image is the ID of the manifest/index
// With graphdrivers, ID of the image is the ID of the image config
if testEnv.UsingSnapshotter() {
// ID of image won't match the Config ID from manifest.json
// Just check if manifests exist in blobs
for _, blob := range expected {
_, err = fs.Stat(tarfs, "blobs/sha256/"+blob)
assert.Check(t, err)
}
} else {
sort.Strings(actual)
sort.Strings(expected)
assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
}
}
func TestSaveDirectoryPermissions(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details")
ctx := setupTest(t)
client := testEnv.APIClient()
layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"}
dockerfile := `FROM busybox
RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a
RUN touch /opt/a/b/c && chown user:user /opt/a/b/c`
imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile)))
rdr, err := client.ImageSave(ctx, []string{imgID})
assert.NilError(t, err)
defer rdr.Close()
tarfs := tarIndexFS(t, rdr)
dt, err := fs.ReadFile(tarfs, "manifest.json")
assert.NilError(t, err)
var mfstLs []imageSaveManifestEntry
assert.NilError(t, json.Unmarshal(dt, &mfstLs))
var found bool
for _, p := range mfstLs[0].Layers {
var entriesSansDev []string
f, err := tarfs.Open(p)
assert.NilError(t, err)
entries, err := listTar(f)
f.Close()
assert.NilError(t, err)
for _, e := range entries {
if !strings.Contains(e, "dev/") {
entriesSansDev = append(entriesSansDev, e)
}
}
assert.NilError(t, err, "encountered error while listing tar entries: %s", err)
if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) {
found = true
break
}
}
assert.Assert(t, found, "failed to find the layer with the right content listing")
}
func listTar(f io.Reader) ([]string, error) {
// If using the containerd snapshotter, the tar file may be compressed
dec, err := archive.DecompressStream(f)
if err != nil {
return nil, err
}
defer dec.Close()
tr := tar.NewReader(dec)
var entries []string
for {
th, err := tr.Next()
if err == io.EOF {
// end of tar archive
return entries, nil
}
if err != nil {
return entries, err
}
entries = append(entries, th.Name)
}
}