mirror of
https://github.com/moby/moby.git
synced 2026-08-05 23:51:18 +00:00
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:
@@ -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)))
|
||||
}
|
||||
|
||||
111
daemon/containerd/image_load_test.go
Normal file
111
daemon/containerd/image_load_test.go
Normal 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))
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user