mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
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>
272 lines
8.3 KiB
Go
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))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|