mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
286 lines
10 KiB
Go
286 lines
10 KiB
Go
package containerd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
c8dimages "github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/containerd/platforms"
|
|
"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"
|
|
)
|
|
|
|
type pushTestCase struct {
|
|
name string
|
|
indexPlatforms []ocispec.Platform // all platforms supported by the image
|
|
availablePlatforms []ocispec.Platform // platforms available locally
|
|
requestPlatform *ocispec.Platform // platform requested by the client (not the platform selected for push!)
|
|
check func(t *testing.T, img c8dimages.Image, pushDescriptor ocispec.Descriptor, err error)
|
|
daemonPlatform *ocispec.Platform
|
|
}
|
|
|
|
func TestImagePushIndex(t *testing.T) {
|
|
ctx := namespaces.WithNamespace(t.Context(), "testing-"+t.Name())
|
|
|
|
csDir := t.TempDir()
|
|
store := &blobsDirContentStore{blobs: filepath.Join(csDir, "blobs/sha256")}
|
|
|
|
linuxAmd64 := platforms.MustParse("linux/amd64")
|
|
darwinArm64 := platforms.MustParse("darwin/arm64")
|
|
windowsAmd64 := platforms.MustParse("windows/amd64")
|
|
|
|
linuxArm64 := platforms.MustParse("linux/arm64")
|
|
linuxArmv5 := platforms.MustParse("linux/arm/v5")
|
|
linuxArmv7 := platforms.MustParse("linux/arm/v7")
|
|
|
|
// Image service will have the daemon host platform mocked to linux/amd64.
|
|
// Unless test cases specify a different platform.
|
|
defaultDaemonPlatform := linuxAmd64
|
|
|
|
for _, tc := range []pushTestCase{
|
|
// No explicit platform requested
|
|
{
|
|
name: "none requested, all present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
check: wholeIndexSelected,
|
|
},
|
|
{
|
|
name: "none requested, one present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{linuxAmd64},
|
|
check: singleManifestSelected(linuxAmd64),
|
|
},
|
|
{
|
|
name: "none requested, two present, daemon platform available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64},
|
|
check: singleManifestSelected(linuxAmd64),
|
|
},
|
|
{
|
|
name: "none requested, two present, daemon platform NOT available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{darwinArm64, windowsAmd64},
|
|
check: multipleCandidates,
|
|
},
|
|
|
|
// Specific platform requested
|
|
{
|
|
name: "linux/amd64 requested, all present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
requestPlatform: &linuxAmd64,
|
|
check: singleManifestSelected(linuxAmd64),
|
|
},
|
|
{
|
|
name: "linux/amd64 requested, but not present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
|
|
availablePlatforms: []ocispec.Platform{darwinArm64, windowsAmd64},
|
|
requestPlatform: &linuxAmd64,
|
|
check: candidateNotFound,
|
|
},
|
|
|
|
// Variant tests
|
|
{
|
|
name: "linux/arm/v5 requested, but not in index",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxAmd64, linuxArmv7},
|
|
availablePlatforms: []ocispec.Platform{linuxAmd64, linuxArmv7},
|
|
requestPlatform: &linuxArmv5,
|
|
check: candidateNotFound,
|
|
},
|
|
{
|
|
name: "linux/arm/v5 requested, but not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7},
|
|
requestPlatform: &linuxArmv5,
|
|
check: candidateNotFound,
|
|
},
|
|
{
|
|
name: "linux/arm/v7 requested, but not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
requestPlatform: &linuxArmv7,
|
|
check: candidateNotFound,
|
|
},
|
|
{
|
|
name: "linux/arm/v7 requested on v7 daemon, but not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: &linuxArmv7,
|
|
check: candidateNotFound,
|
|
},
|
|
{
|
|
name: "linux/arm/v7 requested on v5 daemon, all available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
daemonPlatform: &linuxArmv5,
|
|
requestPlatform: &linuxArmv7,
|
|
check: singleManifestSelected(linuxArmv7),
|
|
},
|
|
{
|
|
name: "linux/arm/v5 requested on v7 daemon, all available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: &linuxArmv5,
|
|
check: singleManifestSelected(linuxArmv5),
|
|
},
|
|
{
|
|
name: "none requested on v5 daemon, arm64 not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArmv7, linuxArmv5},
|
|
daemonPlatform: &linuxArmv5,
|
|
requestPlatform: nil,
|
|
check: singleManifestSelected(linuxArmv5),
|
|
},
|
|
{
|
|
name: "none requested on v7 daemon, arm64 not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArmv7, linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: nil,
|
|
check: singleManifestSelected(linuxArmv7),
|
|
},
|
|
{
|
|
name: "none requested on v7 daemon, v7 not available",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: nil,
|
|
check: singleManifestSelected(linuxArmv5), // Should it fail, because v5 can't be pushed?
|
|
},
|
|
|
|
{
|
|
name: "none requested on v7 daemon, v5 in index but not v7, all present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: nil,
|
|
check: wholeIndexSelected,
|
|
},
|
|
{
|
|
name: "none requested on v7 daemon, v5 in index but not v7, v5 present",
|
|
|
|
indexPlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
|
|
availablePlatforms: []ocispec.Platform{linuxArmv5},
|
|
daemonPlatform: &linuxArmv7,
|
|
requestPlatform: nil,
|
|
check: singleManifestSelected(linuxArmv5),
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
imgSvc := fakeImageService(t, ctx, store)
|
|
// Mock the daemon platform.
|
|
if tc.daemonPlatform != nil {
|
|
imgSvc.defaultPlatformOverride = platforms.Only(*tc.daemonPlatform)
|
|
} else {
|
|
imgSvc.defaultPlatformOverride = platforms.Only(defaultDaemonPlatform)
|
|
}
|
|
|
|
idx, _, err := specialimage.MultiPlatform(csDir, "multiplatform:latest", tc.indexPlatforms)
|
|
assert.NilError(t, err)
|
|
|
|
imgs := imagesFromIndex(idx)
|
|
assert.Assert(t, is.Len(imgs, 1))
|
|
|
|
img := imgs[0]
|
|
_, err = imgSvc.images.Create(ctx, img)
|
|
assert.NilError(t, err)
|
|
|
|
for _, platform := range tc.indexPlatforms {
|
|
if slices.ContainsFunc(tc.availablePlatforms, platforms.OnlyStrict(platform).Match) {
|
|
continue
|
|
}
|
|
assert.NilError(t, deletePlatform(ctx, imgSvc, img, platform))
|
|
}
|
|
|
|
desc, err := imgSvc.getPushDescriptor(ctx, img, tc.requestPlatform)
|
|
|
|
tc.check(t, img, desc, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func deletePlatform(ctx context.Context, imgSvc *ImageService, img c8dimages.Image, platform ocispec.Platform) error {
|
|
var blobs []ocispec.Descriptor
|
|
pm := platforms.OnlyStrict(platform)
|
|
err := imgSvc.walkImageManifests(ctx, img, func(im *ImageManifest) error {
|
|
imPlatform, err := im.ImagePlatform(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to determine platform of image manifest %v: %w", im.Target(), err)
|
|
}
|
|
|
|
if !pm.Match(imPlatform) {
|
|
return nil
|
|
}
|
|
|
|
return imgSvc.walkPresentChildren(ctx, im.Target(), func(ctx context.Context, d ocispec.Descriptor) error {
|
|
blobs = append(blobs, d)
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to walk image manifests: %w", err)
|
|
}
|
|
|
|
for _, d := range blobs {
|
|
err := imgSvc.content.Delete(ctx, d.Digest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete blob %v: %w", d.Digest, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wholeIndexSelected asserts that the push descriptor candidate is for the whole index.
|
|
func wholeIndexSelected(t *testing.T, img c8dimages.Image, pushDescriptor ocispec.Descriptor, err error) {
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(pushDescriptor.Digest, img.Target.Digest))
|
|
}
|
|
|
|
// singleManifestSelected asserts that the push descriptor candidate is for a single platform-specific manifest.
|
|
func singleManifestSelected(platform ocispec.Platform) func(t *testing.T, img c8dimages.Image, pushDescriptor ocispec.Descriptor, err error) {
|
|
pm := platforms.OnlyStrict(platform)
|
|
return func(t *testing.T, img c8dimages.Image, pushDescriptor ocispec.Descriptor, err error) {
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Equal(pushDescriptor.MediaType, ocispec.MediaTypeImageManifest), "the push descriptor isn't for a manifest")
|
|
assert.Assert(t, pushDescriptor.Platform != nil, "the push descriptor doesn't have a platform")
|
|
assert.Assert(t, pm.Match(*pushDescriptor.Platform), "the push descriptor isn't for the selected platform")
|
|
}
|
|
}
|
|
|
|
// candidateNotFound asserts that the no matching candidate was found.
|
|
func candidateNotFound(t *testing.T, _ c8dimages.Image, desc ocispec.Descriptor, err error) {
|
|
assert.Check(t, cerrdefs.IsNotFound(err), "expected NotFound error, got %v, candidate: %v", err, desc.Platform)
|
|
}
|
|
|
|
// multipleCandidates asserts that multiple matching candidates were found and no decision could be made.
|
|
func multipleCandidates(t *testing.T, _ c8dimages.Image, desc ocispec.Descriptor, err error) {
|
|
assert.Check(t, cerrdefs.IsConflict(err), "expected Conflict error, got %v, candidate: %v", err, desc.Platform)
|
|
}
|