c8d/load: Don't ignore missing platform when requested

Commit f143f4ec51 introduced platform support
when loading images. However when loading a specific platform variant from
a tar that contains multiple, we should not ignore cases if that platform is
missing.

Before this patch, the missing platform was silently ignored, potentially
loading an empty image:

    $ docker image load -i image.tar --platform=linux/riscv64
    Loaded image: alpine:latest

    $ docker image ls --tree
    IMAGE           ID             DISK USAGE   CONTENT SIZE   USED
    alpine:latest   beefdbd8a1da           0B             0B

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2024-10-21 17:44:13 +02:00
parent 3bbb9749f4
commit 4ab7644d8d
2 changed files with 204 additions and 4 deletions

View File

@@ -256,8 +256,6 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, platf
opts := []containerd.ImportOpt{
containerd.WithImportPlatform(pm),
containerd.WithSkipMissing(),
// Create an additional image with dangling name for imported images...
containerd.WithDigestRef(danglingImageName),
// ... but only if they don't have a name or it's invalid.
@@ -270,13 +268,51 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, platf
}),
}
if platform == nil {
// Allow variants to be missing if no specific platform is requested.
opts = append(opts, containerd.WithSkipMissing())
}
imgs, err := i.client.Import(ctx, decompressed, opts...)
if err != nil {
if platform != nil {
p := platforms.FormatAll(*platform)
log.G(ctx).WithFields(log.Fields{"error": err, "platform": p}).Debug("failed to import image to containerd")
// Note: ErrEmptyWalk will not be returned in most cases as
// index.json will contain a descriptor of the actual OCI index or
// Docker manifest list, so the walk is never empty.
// Even in case of a single-platform image, the manifest descriptor
// doesn't have a platform set, so it won't be filtered out by the
// FilterPlatform containerd handler.
if errors.Is(err, containerdimages.ErrEmptyWalk) {
return errdefs.NotFound(errors.Wrapf(err, "requested platform (%s) not found", p))
}
if cerrdefs.IsNotFound(err) {
return errdefs.NotFound(errors.Wrapf(err, "requested platform (%s) found, but some content is missing", p))
}
}
log.G(ctx).WithError(err).Debug("failed to import image to containerd")
return errdefs.System(err)
}
if platform != nil {
// Verify that the requested platform is available for the loaded images.
// While the ideal behavior here would be to verify whether the input
// archive actually supplied them, we're not able to determine that
// as the imported index is not returned by the import operation.
if err := i.verifyImagesProvidePlatform(ctx, imgs, *platform, pm); err != nil {
return err
}
}
progress := streamformatter.NewStdoutWriter(outStream)
// Unpack only an image of the host platform
unpackPm := i.hostPlatformMatcher()
// If a load of specific platform is requested, unpack it
if platform != nil {
unpackPm = pm
}
for _, img := range imgs {
name := img.Name
@@ -310,8 +346,7 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, platf
return nil
}
// Only unpack the image if it matches the host platform
if !i.hostPlatformMatcher().Match(imgPlat) {
if !unpackPm.Match(imgPlat) {
return nil
}
@@ -342,3 +377,57 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, platf
return nil
}
// verifyImagesProvidePlatform checks if the requested platform is loaded.
// If the requested platform is not loaded, it returns an error.
func (i *ImageService) verifyImagesProvidePlatform(ctx context.Context, imgs []containerdimages.Image, platform ocispec.Platform, pm platforms.Matcher) error {
if len(imgs) == 0 {
return errdefs.NotFound(fmt.Errorf("no images providing the requested platform %s found", platforms.FormatAll(platform)))
}
var incompleteImgs []string
for _, img := range imgs {
hasRequestedPlatform := false
err := i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
imgPlat, err := platformImg.ImagePlatform(ctx)
if err != nil {
if cerrdefs.IsNotFound(err) {
return nil
}
return errors.Wrapf(err, "failed to determine image platform")
}
if !pm.Match(imgPlat) {
return nil
}
available, err := platformImg.CheckContentAvailable(ctx)
if err != nil {
return errors.Wrapf(err, "failed to determine image content availability for platform %s", platforms.FormatAll(platform))
}
if available {
hasRequestedPlatform = true
return nil
}
return nil
})
if err != nil {
return errdefs.System(err)
}
if !hasRequestedPlatform {
incompleteImgs = append(incompleteImgs, imageFamiliarName(img))
}
}
msg := ""
switch len(incompleteImgs) {
case 0:
// Success - All images provide the requested platform.
return nil
case 1:
msg = "image %s was loaded, but doesn't provide the requested platform (%s)"
default:
msg = "images [%s] were loaded, but don't provide the requested platform (%s)"
}
return errdefs.NotFound(fmt.Errorf(msg, strings.Join(incompleteImgs, ", "), platforms.FormatAll(platform)))
}

View File

@@ -0,0 +1,111 @@
package containerd
import (
"bytes"
"context"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/content/local"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/platforms"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/internal/testutils/specialimage"
"github.com/docker/docker/pkg/archive"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestImageLoadMissing(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"}
ctx := namespaces.WithNamespace(context.TODO(), "testing-"+t.Name())
store, err := local.NewLabeledStore(t.TempDir(), &memoryLabelStore{})
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, platform ocispec.Platform) error {
tarRc, err := archive.Tar(dir, archive.Uncompressed)
assert.NilError(t, err)
defer tarRc.Close()
buf := bytes.Buffer{}
defer func() {
t.Log(buf.String())
}()
return imgSvc.LoadImage(ctx, tarRc, &platform, &buf, true)
}
clearStore := func(ctx context.Context, t *testing.T) {
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, 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, errdefs.IsNotFound))
})
clearStore(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)
err = tryLoad(ctx, t, imgDataDir, linuxArm64)
assert.Check(t, is.ErrorContains(err, "doesn't provide the requested platform (linux/arm64)"))
assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
})
clearStore(ctx, t)
t.Run("2 platform image", func(t *testing.T) {
imgDataDir := t.TempDir()
_, mfstDescs, err := specialimage.MultiPlatform(imgDataDir, "multiplatform:latest", []ocispec.Platform{linuxAmd64, linuxArm64})
assert.NilError(t, err)
t.Run("platform not included in index", func(t *testing.T) {
err = tryLoad(ctx, t, imgDataDir, 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, errdefs.IsNotFound))
})
clearStore(ctx, t)
t.Run("platform 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, linuxArm64)
assert.Check(t, is.ErrorContains(err, "requested platform (linux/arm64) found, but some content is missing"))
assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
})
})
}