diff --git a/api/server/router/container/inspect.go b/api/server/router/container/inspect.go index 44e78338c6..5458122129 100644 --- a/api/server/router/container/inspect.go +++ b/api/server/router/container/inspect.go @@ -33,6 +33,9 @@ func (c *containerRouter) getContainersByName(ctx context.Context, w http.Respon } } } + if versions.LessThan(version, "1.48") { + ctr.ImageManifestDescriptor = nil + } return httputils.WriteJSON(w, http.StatusOK, ctr) } diff --git a/api/swagger.yaml b/api/swagger.yaml index ccd55693b0..a237f374aa 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -7266,6 +7266,14 @@ paths: type: "string" Platform: type: "string" + ImageManifestDescriptor: + $ref: "#/definitions/OCIDescriptor" + description: | + OCI descriptor of the platform-specific manifest of the image + the container was created from. + + Note: Only available if the daemon provides a multi-platform + image store. MountLabel: type: "string" ProcessLabel: diff --git a/api/types/container/container.go b/api/types/container/container.go index 398fd6e886..0244a3549a 100644 --- a/api/types/container/container.go +++ b/api/types/container/container.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/storage" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // PruneReport contains the response for Engine API: @@ -171,4 +172,6 @@ type InspectResponse struct { Mounts []MountPoint Config *Config NetworkSettings *NetworkSettings + // ImageManifestDescriptor is the descriptor of a platform-specific manifest of the image used to create the container. + ImageManifestDescriptor *ocispec.Descriptor `json:",omitempty"` } diff --git a/daemon/info.go b/daemon/info.go index 2a39e19266..49e2b17d26 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -27,7 +27,7 @@ import ( "github.com/docker/docker/pkg/parsers/operatingsystem" "github.com/docker/docker/pkg/sysinfo" "github.com/docker/docker/registry" - metrics "github.com/docker/go-metrics" + "github.com/docker/go-metrics" "github.com/opencontainers/selinux/go-selinux" ) diff --git a/daemon/inspect.go b/daemon/inspect.go index 473c5ee583..fadbd14e16 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -77,11 +77,21 @@ func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options base.SizeRootFs = &sizeRootFs } + imageManifest := ctr.ImageManifest + if imageManifest != nil && imageManifest.Platform == nil { + // Copy the image manifest to avoid mutating the original + c := *imageManifest + imageManifest = &c + + imageManifest.Platform = &ctr.ImagePlatform + } + return &containertypes.InspectResponse{ - ContainerJSONBase: base, - Mounts: mountPoints, - Config: ctr.Config, - NetworkSettings: networkSettings, + ContainerJSONBase: base, + Mounts: mountPoints, + Config: ctr.Config, + NetworkSettings: networkSettings, + ImageManifestDescriptor: imageManifest, }, nil } diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 6cd5245157..15ba0941d4 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -40,6 +40,11 @@ keywords: "API, Docker, rcli, REST, documentation" image store. WARNING: This is experimental and may change at any time without any backward compatibility. +* `GET /containers/{name}/json` now returns an `ImageManifestDescriptor` field + containing the OCI descriptor of the platform-specific image manifest of the + image that was used to create the container. + This field is only populated if the daemon provides a multi-platform image + store. * `POST /networks/create` now has an `EnableIPv4` field. Setting it to `false` disables IPv4 IPAM for the network. It can only be set to `false` if the daemon has experimental features enabled. diff --git a/integration/container/inspect_test.go b/integration/container/inspect_test.go index 3f8e10628f..bdeaba8c9e 100644 --- a/integration/container/inspect_test.go +++ b/integration/container/inspect_test.go @@ -5,11 +5,14 @@ import ( "strings" "testing" + "github.com/containerd/platforms" containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/testutil/request" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" ) func TestInspectAnnotations(t *testing.T) { @@ -64,3 +67,78 @@ func TestNetworkAliasesAreEmpty(t *testing.T) { }) } } + +func TestInspectImageManifestPlatform(t *testing.T) { + skip.If(t, testEnv.IsRemoteDaemon) + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + skip.If(t, !testEnv.UsingSnapshotter()) + + tests := []struct { + name string + image string + skipIf func() bool + expectedPlatform platforms.Platform + }{ + { + name: "amd64 only on any host", + image: "busybox:latest", + expectedPlatform: platforms.Platform{ + OS: "linux", + Architecture: "amd64", + Variant: "", + }, + }, + { + skipIf: func() bool { return runtime.GOARCH != "amd64" }, + name: "amd64 image on non-amd64 host", + + image: "hello-world:amd64", + expectedPlatform: platforms.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + { + name: "arm64 image on non-arm64 host", + skipIf: func() bool { return runtime.GOARCH != "arm64" }, + image: "hello-world:arm64", + + expectedPlatform: platforms.Platform{ + OS: "linux", + Architecture: "arm64", + Variant: "", + }, + }, + } + + for _, tc := range tests { + if tc.skipIf != nil && tc.skipIf() { + continue + } + t.Run(tc.name, func(t *testing.T) { + ctx := setupTest(t) + apiClient := request.NewAPIClient(t) + + ctr := container.Create(ctx, t, apiClient, container.WithImage(tc.image)) + defer apiClient.ContainerRemove(ctx, ctr, containertypes.RemoveOptions{Force: true}) + + img, _, err := apiClient.ImageInspectWithRaw(ctx, tc.image) + assert.NilError(t, err) + + hostPlatform := platforms.Platform{ + OS: img.Os, + Architecture: img.Architecture, + Variant: img.Variant, + } + inspect := container.Inspect(ctx, t, apiClient, ctr) + assert.Assert(t, inspect.ImageManifestDescriptor != nil) + assert.Check(t, is.DeepEqual(*inspect.ImageManifestDescriptor.Platform, hostPlatform)) + + t.Run("pre 1.48", func(t *testing.T) { + oldClient := request.NewAPIClient(t, client.WithVersion("1.47")) + inspect := container.Inspect(ctx, t, oldClient, ctr) + assert.Check(t, is.Nil(inspect.ImageManifestDescriptor)) + }) + }) + } +}