blob: 2db5c9fa9baa357f96b674494a3d84fb8ec11576 [file] [edit]
package containerd
import (
"bytes"
"context"
"fmt"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/containerd/containerd/v2/core/content"
c8dimages "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/plugins/content/local"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
"github.com/moby/moby/v2/daemon/server/imagebackend"
"github.com/moby/moby/v2/internal/testutil/labelstore"
"github.com/moby/moby/v2/internal/testutil/specialimage"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestImageLoad(t *testing.T) {
linuxAmd64 := ocispec.Platform{OS: "linux", Architecture: "amd64"}
linuxArm64 := ocispec.Platform{OS: "linux", Architecture: "arm64"}
linuxArmv5 := ocispec.Platform{OS: "linux", Architecture: "arm", Variant: "v5"}
linuxRiscv64 := ocispec.Platform{OS: "linux", Architecture: "riskv64"}
ctx := namespaces.WithNamespace(t.Context(), "testing-"+t.Name())
store, err := local.NewLabeledStore(t.TempDir(), &labelstore.InMemory{})
assert.NilError(t, err)
imgSvc := fakeImageService(t, ctx, store)
// Mock the daemon platform.
imgSvc.defaultPlatformOverride = &linuxAmd64
tryLoad := func(ctx context.Context, t *testing.T, dir string, platformList []ocispec.Platform) error {
tarRc, err := archive.Tar(dir, compression.None)
assert.NilError(t, err)
defer tarRc.Close()
buf := bytes.Buffer{}
defer func() {
t.Log(buf.String())
}()
return imgSvc.LoadImage(ctx, tarRc, platformList, &buf, true)
}
cleanup := func(ctx context.Context, t *testing.T) {
// Remove all existing images to start fresh
images, err := imgSvc.Images(ctx, imagebackend.ListOptions{})
assert.NilError(t, err)
for _, img := range images {
_, err := imgSvc.ImageDelete(ctx, img.ID, imagebackend.RemoveOptions{PruneChildren: true})
assert.NilError(t, err)
}
// Remove all content from the store
assert.NilError(t, store.Walk(ctx, func(info content.Info) error {
return store.Delete(ctx, info.Digest)
}), "failed to delete all content")
}
t.Run("unchanged image", func(t *testing.T) {
imgDataDir := t.TempDir()
_, err := specialimage.MultiLayer(imgDataDir)
assert.NilError(t, err)
assert.NilError(t, tryLoad(ctx, t, imgDataDir, nil))
assert.NilError(t, tryLoad(ctx, t, imgDataDir, nil))
images, err := imgSvc.images.List(ctx)
assert.NilError(t, err)
assert.Check(t, is.Len(images, 1))
})
cleanup(ctx, t)
for _, tc := range []struct {
name string
existingTags []string
}{
{
name: "replaced image",
existingTags: []string{"docker.io/library/foo:latest"},
},
{
name: "replaced image with another named reference",
existingTags: []string{"docker.io/library/foo:latest", "docker.io/library/bar:latest"},
},
} {
t.Run(tc.name, func(t *testing.T) {
// Both versions use the first tag; the other tags stay on version A.
replacedTag := tc.existingTags[0]
extraTags := tc.existingTags[1:]
loadVariant := func(version string) ocispec.Descriptor {
t.Helper()
dir := t.TempDir()
index, err := specialimage.MultiLayerCustom(dir, replacedTag, []specialimage.SingleFileLayer{
{Name: "version", Content: []byte(version)},
})
assert.NilError(t, err)
err = tryLoad(ctx, t, dir, nil)
assert.NilError(t, err)
return index.Manifests[0]
}
previousTarget := loadVariant("A")
for _, tag := range extraTags {
_, err := imgSvc.images.Create(ctx, c8dimages.Image{Name: tag, Target: previousTarget})
assert.NilError(t, err)
}
replacementTarget := loadVariant("B")
assert.Assert(t, previousTarget.Digest != replacementTarget.Digest)
// Version A becomes dangling only if it lost its last tag.
want := map[string]digest.Digest{
replacedTag: replacementTarget.Digest,
}
for _, tag := range extraTags {
want[tag] = previousTarget.Digest
}
if len(extraTags) == 0 {
want[danglingImageName(previousTarget.Digest)] = previousTarget.Digest
}
images, err := imgSvc.images.List(ctx)
assert.NilError(t, err)
got := make(map[string]digest.Digest, len(images))
for _, img := range images {
got[img.Name] = img.Target.Digest
}
assert.Check(t, is.DeepEqual(got, want))
})
cleanup(ctx, t)
}
t.Run("empty index", func(t *testing.T) {
imgDataDir := t.TempDir()
_, err := specialimage.EmptyIndex(imgDataDir)
assert.NilError(t, err)
err = tryLoad(ctx, t, imgDataDir, []ocispec.Platform{linuxAmd64})
assert.Check(t, is.Error(err, "image emptyindex:latest was loaded, but doesn't provide the requested platform ([linux/amd64])"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
cleanup(ctx, t)
t.Run("single platform", func(t *testing.T) {
imgDataDir := t.TempDir()
r := rand.NewSource(0x9127371238)
_, err = specialimage.RandomSinglePlatform(imgDataDir, linuxAmd64, r)
assert.NilError(t, err)
platforms := []ocispec.Platform{linuxAmd64}
err = tryLoad(ctx, t, imgDataDir, platforms)
assert.NilError(t, err)
err = tryLoad(ctx, t, imgDataDir, []ocispec.Platform{linuxArm64})
assert.Check(t, is.ErrorContains(err, "doesn't provide the requested platform ([linux/arm64])"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
images, err := imgSvc.images.List(ctx)
assert.NilError(t, err)
assert.Check(t, is.Len(images, 1))
})
cleanup(ctx, t)
t.Run("multi-platform image", func(t *testing.T) {
imgDataDir := t.TempDir()
imgRef := "multiplatform:latest"
_, mfstDescs, err := specialimage.MultiPlatform(imgDataDir, imgRef, []ocispec.Platform{linuxAmd64, linuxArm64, linuxRiscv64})
assert.NilError(t, err)
t.Run("one platform in index", func(t *testing.T) {
platforms := []ocispec.Platform{linuxAmd64}
err = tryLoad(ctx, t, imgDataDir, platforms)
assert.NilError(t, err)
// verify that the loaded image has the correct platform
err = verifyImagePlatforms(ctx, imgSvc, imgRef, platforms)
assert.NilError(t, err)
})
cleanup(ctx, t)
t.Run("all platforms in index", func(t *testing.T) {
platforms := []ocispec.Platform{linuxAmd64, linuxArm64, linuxRiscv64}
err = tryLoad(ctx, t, imgDataDir, platforms)
assert.NilError(t, err)
// verify that the loaded image has the correct platforms
err = verifyImagePlatforms(ctx, imgSvc, imgRef, platforms)
assert.NilError(t, err)
})
cleanup(ctx, t)
t.Run("platform not included in index", func(t *testing.T) {
err = tryLoad(ctx, t, imgDataDir, []ocispec.Platform{linuxArmv5})
assert.Check(t, is.Error(err, "image multiplatform:latest was loaded, but doesn't provide the requested platform ([linux/arm/v5])"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
cleanup(ctx, t)
t.Run("platform included but blobs missing", func(t *testing.T) {
// Assumption: arm64 image is second in the index (implementation detail of specialimage.MultiPlatform)
mfstDesc := mfstDescs[1]
assert.Assert(t, mfstDesc.Platform.Architecture == linuxArm64.Architecture)
assert.Assert(t, mfstDesc.Platform.Variant == linuxArm64.Variant)
t.Log(mfstDesc.Digest)
// Delete arm64 manifest
mfstPath := filepath.Join(imgDataDir, "blobs/sha256", mfstDesc.Digest.Encoded())
assert.NilError(t, os.Remove(mfstPath))
err = tryLoad(ctx, t, imgDataDir, []ocispec.Platform{linuxArm64})
assert.Check(t, is.ErrorContains(err, "requested platform(s) ([linux/arm64]) found, but some content is missing"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
cleanup(ctx, t)
})
}
func verifyImagePlatforms(ctx context.Context, imgSvc *ImageService, imgRef string, expectedPlatforms []ocispec.Platform) error {
// get the manifest(s) for the image
img, err := imgSvc.ImageInspect(ctx, imgRef, imagebackend.ImageInspectOpts{Manifests: true})
if err != nil {
return err
}
// verify that the image manifest has the expected platforms
for _, ep := range expectedPlatforms {
want := platforms.FormatAll(ep)
found := false
for _, m := range img.Manifests {
if m.Descriptor.Platform != nil {
got := platforms.FormatAll(*m.Descriptor.Platform)
if got == want {
found = true
break
}
}
}
if !found {
return fmt.Errorf("expected platform %q not found in loaded images", want)
}
}
return nil
}