diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index daa74afec8..b4a26f5322 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -113,6 +113,13 @@ func (c *containerRouter) getContainersJSON(ctx context.Context, w http.Response } } + if versions.LessThan(version, "1.48") { + // ImageManifestDescriptor information was added in API 1.48 + for _, c := range containers { + c.ImageManifestDescriptor = nil + } + } + return httputils.WriteJSON(w, http.StatusOK, containers) } diff --git a/api/swagger.yaml b/api/swagger.yaml index 7aab0be3f4..d4e4f1197a 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -5109,6 +5109,17 @@ definitions: ImageID: description: "The ID of the image that this container was created from" type: "string" + ImageManifestDescriptor: + $ref: "#/definitions/OCIDescriptor" + x-nullable: true + 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. + + This field is not populated in the `GET /system/df` endpoint. Command: description: "Command to run when starting the container" type: "string" diff --git a/api/types/container/container.go b/api/types/container/container.go index b5e6243de6..65fabbf425 100644 --- a/api/types/container/container.go +++ b/api/types/container/container.go @@ -121,19 +121,20 @@ type State struct { // Summary contains response of Engine API: // GET "/containers/json" type Summary struct { - ID string `json:"Id"` - Names []string - Image string - ImageID string - Command string - Created int64 - Ports []Port - SizeRw int64 `json:",omitempty"` - SizeRootFs int64 `json:",omitempty"` - Labels map[string]string - State string - Status string - HostConfig struct { + ID string `json:"Id"` + Names []string + Image string + ImageID string + ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"` + Command string + Created int64 + Ports []Port + SizeRw int64 `json:",omitempty"` + SizeRootFs int64 `json:",omitempty"` + Labels map[string]string + State string + Status string + HostConfig struct { NetworkMode string `json:",omitempty"` Annotations map[string]string `json:",omitempty"` } @@ -183,5 +184,5 @@ type InspectResponse struct { 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"` + ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"` } diff --git a/container/view.go b/container/view.go index ba3c03de14..2dd9e6f087 100644 --- a/container/view.go +++ b/container/view.go @@ -301,14 +301,15 @@ func (v *View) transform(ctr *Container) *Snapshot { } snapshot := &Snapshot{ Summary: container.Summary{ - ID: ctr.ID, - Names: v.getNames(ctr.ID), - ImageID: ctr.ImageID.String(), - Ports: []container.Port{}, - Mounts: ctr.GetMountPoints(), - State: ctr.State.StateString(), - Status: ctr.State.String(), - Created: ctr.Created.Unix(), + ID: ctr.ID, + Names: v.getNames(ctr.ID), + ImageID: ctr.ImageID.String(), + ImageManifestDescriptor: ctr.ImageManifest, + Ports: []container.Port{}, + Mounts: ctr.GetMountPoints(), + State: ctr.State.StateString(), + Status: ctr.State.String(), + Created: ctr.Created.Unix(), }, CreatedAt: ctr.Created, StartedAt: ctr.StartedAt, @@ -416,6 +417,10 @@ func (v *View) transform(ctr *Container) *Snapshot { } snapshot.NetworkSettings = &container.NetworkSettingsSummary{Networks: networks} + if ctr.ImageManifest != nil && ctr.ImageManifest.Platform == nil { + ctr.ImageManifest.Platform = &ctr.ImagePlatform + } + return snapshot } diff --git a/daemon/disk_usage.go b/daemon/disk_usage.go index 63b4de0c01..da712605ae 100644 --- a/daemon/disk_usage.go +++ b/daemon/disk_usage.go @@ -26,6 +26,12 @@ func (daemon *Daemon) containerDiskUsage(ctx context.Context) ([]*container.Summ if err != nil { return nil, fmt.Errorf("failed to retrieve container list: %v", err) } + + // Remove image manifest descriptor from the result as it should not be included. + // https://github.com/moby/moby/pull/49407#discussion_r1954396666 + for _, c := range containers { + c.ImageManifestDescriptor = nil + } return containers, nil }) return res, err diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 080f0d6bf3..396b27ec16 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -89,6 +89,10 @@ keywords: "API, Docker, rcli, REST, documentation" paths (`/v/`). * `POST /build/prune` renames `keep-bytes` to `reserved-space` and now supports additional prune parameters `max-used-space` and `min-free-space`. +* `GET /containers/json` now returns an `ImageManifestDescriptor` field + matching the same field in `/containers/{name}/json`. + This field is only populated if the daemon provides a multi-platform image + store. ## v1.47 API changes diff --git a/integration/container/ps_test.go b/integration/container/ps_test.go index 97e7038076..7e617869d0 100644 --- a/integration/container/ps_test.go +++ b/integration/container/ps_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/testutil" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" ) func TestPsFilter(t *testing.T) { @@ -47,3 +48,29 @@ func TestPsFilter(t *testing.T) { assert.Check(t, is.Contains(containerIDs(results), prev)) }) } + +// TestPsPlatform verifies that containers have a platform set +func TestPsImageManifestPlatform(t *testing.T) { + skip.If(t, testEnv.IsRemoteDaemon) + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + skip.If(t, !testEnv.UsingSnapshotter()) + + ctx := setupTest(t) + apiClient := testEnv.APIClient() + + container.Create(ctx, t, apiClient) + + containers, err := apiClient.ContainerList(ctx, containertypes.ListOptions{ + All: true, + }) + assert.NilError(t, err) + assert.Check(t, len(containers) > 0) + + ctr := containers[0] + if assert.Check(t, ctr.ImageManifestDescriptor != nil && ctr.ImageManifestDescriptor.Platform != nil) { + // Check that at least OS and Architecture have a value. Other values + // depend on the platform on which we're running the test. + assert.Equal(t, ctr.ImageManifestDescriptor.Platform.OS, testEnv.DaemonInfo.OSType) + assert.Check(t, ctr.ImageManifestDescriptor.Platform.Architecture != "") + } +} diff --git a/integration/system/disk_usage_test.go b/integration/system/disk_usage_test.go index b6b11242f2..5f6e1d368b 100644 --- a/integration/system/disk_usage_test.go +++ b/integration/system/disk_usage_test.go @@ -107,6 +107,9 @@ func TestDiskUsage(t *testing.T) { // previously used prev.Images[0].Size, which may differ from content data assert.Check(t, is.Equal(du.Containers[0].SizeRootFs, du.LayersSize)) + // ImageManifestDescriptor should NOT be populated. + assert.Check(t, is.Nil(du.Containers[0].ImageManifestDescriptor)) + return du }, },