Files
moby/integration/image/inspect_test.go
Paweł Gronowski 2e3a23c8ec c8d/inspect: Fix image inspect for incomplete images
When inspecting multi-platform images where some layer blobs were
missing from the content store, the image inspect operation would return
too early causing some data (like config details or unpacked size) to be
omitted even though are available.

This ensures that `docker image inspect` returns as much information as
possible.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-12-01 17:38:45 +01:00

272 lines
8.3 KiB
Go

package image
import (
"bytes"
"encoding/json"
"testing"
"github.com/moby/moby/client"
iimage "github.com/moby/moby/v2/integration/internal/image"
"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"
"gotest.tools/v3/skip"
)
// Regression test for: https://github.com/moby/moby/issues/45556
func TestImageInspectEmptyTagsAndDigests(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "build-empty-images is not called on Windows")
ctx := setupTest(t)
apiClient := testEnv.APIClient()
danglingID := iimage.Load(ctx, t, apiClient, specialimage.Dangling)
var raw bytes.Buffer
inspect, err := apiClient.ImageInspect(ctx, danglingID, client.ImageInspectWithRawResponse(&raw))
assert.NilError(t, err)
// Must be a zero length array, not null.
assert.Check(t, is.Len(inspect.RepoTags, 0))
assert.Check(t, is.Len(inspect.RepoDigests, 0))
var rawJson map[string]any
err = json.Unmarshal(raw.Bytes(), &rawJson)
assert.NilError(t, err)
// Check if the raw json is also an array, not null.
assert.Check(t, is.Len(rawJson["RepoTags"], 0))
assert.Check(t, is.Len(rawJson["RepoDigests"], 0))
}
// Regression test for: https://github.com/moby/moby/issues/48747
func TestImageInspectUniqueRepoDigests(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
before, err := apiClient.ImageInspect(ctx, "busybox")
assert.NilError(t, err)
for _, tag := range []string{"master", "newest"} {
imgName := "busybox:" + tag
_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox", Target: imgName})
assert.NilError(t, err)
defer func() {
_, _ = apiClient.ImageRemove(ctx, imgName, client.ImageRemoveOptions{Force: true})
}()
}
after, err := apiClient.ImageInspect(ctx, "busybox")
assert.NilError(t, err)
assert.Check(t, is.Len(after.RepoDigests, len(before.RepoDigests)))
}
func TestImageInspectDescriptor(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
inspect, err := apiClient.ImageInspect(ctx, "busybox")
assert.NilError(t, err)
if !testEnv.UsingSnapshotter() {
assert.Check(t, is.Nil(inspect.Descriptor))
return
}
assert.Assert(t, inspect.Descriptor != nil)
assert.Check(t, inspect.Descriptor.Digest.String() == inspect.ID)
assert.Check(t, inspect.Descriptor.Size > 0)
}
// Regression test for: https://github.com/moby/moby/issues/51566
//
// This can be reproduced with two image that share the same uncompressed layer
// but have a different compressed blob is pulled.
//
// Example:
// ```
// docker pull nginx@sha256:3b7732505933ca591ce4a6d860cb713ad96a3176b82f7979a8dfa9973486a0d6
// docker pull gotenberg/gotenberg@sha256:b116a40a1c24917e2bf3e153692da5acd2e78e7cd67e1b2d243b47c178f31c90
// ```
//
// In this case, it's the base debian trixie image that's used as a base.
// They're effectively the same layer (unpacked diff ID
// `sha256:1d46119d249f7719e1820e24a311aa7c453f166f714969cffe89504678eaa447`),
// but different compressed blobs:
//
// # nginx
// {
// "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
// "size": 29777766,
// "digest": "sha256:8c7716127147648c1751940b9709b6325f2256290d3201662eca2701cadb2cdf"
// }
//
// # gotenberg
// {
// "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
// "size": 30781333,
// "digest": "sha256:b96413fb491a5ed179bb2746ff3be6cbddd72e14c6503bea80d58e579a3b92bc"
// },
func TestImageInspectWithoutSomeBlobs(t *testing.T) {
t.Skip("TODO(vvoland): Come up with minimal images for this test")
skip.If(t, testEnv.DaemonInfo.OSType != "linux", "The test images are Linux-only")
ctx := setupTest(t)
apiClient := testEnv.APIClient()
const baseImage = "nginx@sha256:3b7732505933ca591ce4a6d860cb713ad96a3176b82f7979a8dfa9973486a0d6"
const childImage = "gotenberg/gotenberg:8.24@sha256:b116a40a1c24917e2bf3e153692da5acd2e78e7cd67e1b2d243b47c178f31c90"
// Pull the base image first and then the child image
for _, image := range []string{baseImage, childImage} {
rdr, err := apiClient.ImagePull(ctx, image, client.ImagePullOptions{})
assert.NilError(t, err)
assert.NilError(t, rdr.Wait(ctx))
t.Cleanup(func() {
_, _ = apiClient.ImageRemove(ctx, image, client.ImageRemoveOptions{})
})
}
var raw bytes.Buffer
inspect, err := apiClient.ImageInspect(ctx, childImage, client.ImageInspectWithRawResponse(&raw))
assert.NilError(t, err)
var rawJson map[string]any
err = json.Unmarshal(raw.Bytes(), &rawJson)
assert.NilError(t, err)
configVal, hasConfig := rawJson["Config"]
assert.Check(t, hasConfig, "Config field should exist in JSON response")
if assert.Check(t, configVal != nil, "Config should not be null in JSON response") {
assert.Check(t, is.DeepEqual(inspect.Config.Cmd, []string{"gotenberg"}))
assert.Check(t, inspect.Os != "")
assert.Check(t, inspect.Architecture != "")
}
}
func TestImageInspectWithPlatform(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "The test image is a Linux image")
ctx := setupTest(t)
apiClient := testEnv.APIClient()
nativePlatform := ocispec.Platform{
OS: testEnv.DaemonInfo.OSType,
Architecture: testEnv.DaemonInfo.Architecture,
}
// Create a platform that does not match the host platform
differentOS := "linux"
if nativePlatform.OS == "linux" {
differentOS = "windows"
}
differentPlatform := ocispec.Platform{
OS: differentOS,
Architecture: "amd64",
}
imageID := iimage.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
i, descs, err := specialimage.MultiPlatform(dir, "multiplatform:latest", []ocispec.Platform{nativePlatform, differentPlatform})
assert.NilError(t, err)
err = specialimage.LegacyManifest(dir, "multiplatform:latest", descs[0])
assert.NilError(t, err)
return i, err
})
for _, tc := range []struct {
name string
requestedPlatform *ocispec.Platform
expectedPlatform *ocispec.Platform
expectedError string
withManifests bool
snapshotterOnly bool
graphdriverOnly bool
}{
{
name: "default",
requestedPlatform: nil,
expectedPlatform: &nativePlatform,
},
{
name: "snapshotter/with-manifests",
requestedPlatform: nil,
expectedPlatform: &nativePlatform,
snapshotterOnly: true,
withManifests: true,
},
{
name: "native",
requestedPlatform: &nativePlatform,
expectedPlatform: &nativePlatform,
},
{
name: "different",
requestedPlatform: &differentPlatform,
expectedPlatform: &differentPlatform,
snapshotterOnly: true,
},
{
name: "different not supported on graphdriver",
requestedPlatform: &differentPlatform,
graphdriverOnly: true,
// image with reference multiplatform:latest was found but its platform (linux/aarch64) does not match the specified platform (windows/amd64)
expectedError: "image with reference multiplatform:latest was found but its platform",
},
} {
if tc.snapshotterOnly && !testEnv.UsingSnapshotter() {
continue
}
if tc.graphdriverOnly && testEnv.UsingSnapshotter() {
continue
}
t.Run(tc.name, func(t *testing.T) {
var opts []client.ImageInspectOption
if tc.requestedPlatform != nil {
opts = append(opts, client.ImageInspectWithPlatform(tc.requestedPlatform))
}
if tc.withManifests {
opts = append(opts, client.ImageInspectWithManifests(true))
}
inspect, err := apiClient.ImageInspect(ctx, imageID, opts...)
if tc.expectedError != "" {
assert.Assert(t, is.ErrorContains(err, tc.expectedError))
return
}
assert.NilError(t, err)
assert.Check(t, is.Equal(inspect.Architecture, tc.expectedPlatform.Architecture))
assert.Check(t, is.Equal(inspect.Os, tc.expectedPlatform.OS))
if testEnv.UsingSnapshotter() {
assert.Assert(t, inspect.Descriptor != nil)
if tc.requestedPlatform != nil {
if assert.Check(t, inspect.Descriptor.Platform != nil) {
assert.Check(t, is.DeepEqual(*inspect.Descriptor.Platform, *tc.expectedPlatform))
}
}
} else {
assert.Check(t, inspect.Descriptor == nil)
}
if tc.withManifests {
t.Run("has manifests", func(t *testing.T) {
assert.Check(t, is.Len(inspect.Manifests, 2))
})
} else {
t.Run("has no manifests", func(t *testing.T) {
assert.Check(t, is.Nil(inspect.Manifests))
})
}
})
}
}