Files
moby/daemon/containerd/image_push_test.go
Sebastiaan van Stijn c1c9087404 daemon/containerd: use t.Context() in tests
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-25 21:15:02 +02:00

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