| package containerd |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "math/rand" |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "github.com/containerd/containerd/v2/core/content" |
| "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" |
| 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 = platforms.Only(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("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)) |
| }) |
| 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 |
| } |