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