Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: add Plaform (OS and Architecture) to /containers/json #42464

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/server/router/container/container_routes.go
Expand Up @@ -95,6 +95,14 @@ func (s *containerRouter) getContainersJSON(ctx context.Context, w http.Response
return err
}

version := httputils.VersionFromContext(ctx)
if versions.LessThan(version, "1.42") {
// Platform information was added in API 1.42
for _, c := range containers {
c.Platform = nil
}
}

return httputils.WriteJSON(w, http.StatusOK, containers)
}

Expand Down
8 changes: 8 additions & 0 deletions api/server/router/system/system_routes.go
Expand Up @@ -128,6 +128,14 @@ func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter,
du.BuildCache = []*types.BuildCache{}
}

version := httputils.VersionFromContext(ctx)
if versions.LessThan(version, "1.42") {
// Platform information was added in API 1.42
for _, c := range du.Containers {
c.Platform = nil
}
}

return httputils.WriteJSON(w, http.StatusOK, du)
}

Expand Down
50 changes: 50 additions & 0 deletions api/swagger.yaml
Expand Up @@ -4021,6 +4021,53 @@ definitions:
example:
Warning: "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"

ImagePlatform:
type: "object"
x-nullable: true
description: |
Platform information of the image that the container was created from.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this mention explicitly that this follows the OCI definition of the fields?

(Maybe we need a PR to the image-spec to add an anchor to https://github.com/opencontainers/image-spec/blob/v1.1.0/image-index.md#:~:text=generate%20an%20error.-,platform%20object,-This%20OPTIONAL%20property -- I find myself linking to it constantly with this janky browser-specific syntax 😂)

properties:
architecture:
type: "string"
description: |
The CPU architecture, for example `amd64` or `ppc64`.
example: "arm"
os:
type: "string"
description: |
The operating system, for example `linux` or `windows`.
example: "linux"
os.version:
type: "string"
description: |
Optional field specifying the operating system version of the image
This field is propagated for Windows containers containers, and empty
otherwise. For example on Windows `10.0.14393.1066`.

This field is omitted when empty.
example: ""
x-nullable: true
os.features:
type: "array"
items:
type: "string"
description: |
Optional field specifying an array of strings, each listing a required
OS feature (for example on Windows `win32k`) for the image.

This field is omitted when empty.
x-nullable: true
example: []
variant:
type: "string"
description: |
Optional field specifying the CPU variant image, for example `v7` to
specify ARMv7 when architecture is `arm`.

This field is omitted when empty.
x-nullable: true
example: "v7"

ContainerSummary:
type: "array"
items:
Expand All @@ -4041,6 +4088,9 @@ definitions:
ImageID:
description: "The ID of the image that this container was created from"
type: "string"
Platform:
$ref: "#/definitions/ImagePlatform"
x-nullable: true
Command:
description: "Command to run when starting the container"
type: "string"
Expand Down
43 changes: 43 additions & 0 deletions api/types/types.go
Expand Up @@ -54,13 +54,56 @@ type ImageMetadata struct {
LastTagTime time.Time `json:",omitempty"`
}

// ImagePlatform describes the platform which the image in the manifest runs on.
// This type is our equivalent of ocispec.Platform, but implements fmt.Stringer
// interface.
type ImagePlatform struct {
// Architecture field specifies the CPU architecture, for example
// `amd64` or `ppc64`.
Architecture string `json:"architecture"`

// OS specifies the operating system, for example `linux` or `windows`.
OS string `json:"os"`

// OSVersion is an optional field specifying the operating system
// version, for example on Windows `10.0.14393.1066`.
OSVersion string `json:"os.version,omitempty"`
cpuguy83 marked this conversation as resolved.
Show resolved Hide resolved

// OSFeatures is an optional field specifying an array of strings,
// each listing a required OS feature (for example on Windows `win32k`).
OSFeatures []string `json:"os.features,omitempty"`

// Variant is an optional field specifying a variant of the CPU, for
// example `v7` to specify ARMv7 when architecture is `arm`.
Variant string `json:"variant,omitempty"`
}

// String implements the fmt.Stringer interface, and returns a formatted string
// representing the container image's platform (e.g. linux/arm/v7).
//
// Similar to containerd's platforms.Format(), but returns an empty string
// instead of "unknown" if no OS is present: https://github.com/containerd/containerd/blob/v1.5.2/platforms/platforms.go#L243-L263
func (platform *ImagePlatform) String() string {
if platform == nil || platform.OS == "" {
return ""
}
var ss []string
for _, s := range []string{platform.OS, platform.Architecture, platform.Variant} {
if s != "" {
ss = append(ss, s)
}
}
return strings.Join(ss, "/")
}

// Container contains response of Engine API:
// GET "/containers/json"
type Container struct {
ID string `json:"Id"`
Names []string
Image string
ImageID string
Platform *ImagePlatform `json:"Platform,omitempty"`
Command string
Created int64
Ports []Port
Expand Down
4 changes: 3 additions & 1 deletion container/container.go
Expand Up @@ -39,6 +39,7 @@ import (
units "github.com/docker/go-units"
agentexec "github.com/docker/swarmkit/agent/exec"
"github.com/moby/sys/symlink"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -79,7 +80,8 @@ type Container struct {
LogPath string
Name string
Driver string
OS string
OS string // Deprecated: kept for backward-compatibility. Use Platform.OS instead.
Platform ocispec.Platform
// MountLabel contains the options for the 'mount' command
MountLabel string
ProcessLabel string
Expand Down
18 changes: 10 additions & 8 deletions container/view.go
Expand Up @@ -294,16 +294,18 @@ func (v *memdbView) transform(container *Container) *Snapshot {
if container.Health != nil {
health = container.Health.Status()
}
platform := types.ImagePlatform(container.Platform)
snapshot := &Snapshot{
Container: types.Container{
ID: container.ID,
Names: v.getNames(container.ID),
ImageID: container.ImageID.String(),
Ports: []types.Port{},
Mounts: container.GetMountPoints(),
State: container.State.StateString(),
Status: container.State.String(),
Created: container.Created.Unix(),
ID: container.ID,
Names: v.getNames(container.ID),
ImageID: container.ImageID.String(),
Platform: &platform,
Ports: []types.Port{},
Mounts: container.GetMountPoints(),
State: container.State.StateString(),
Status: container.State.String(),
Created: container.Created.Unix(),
},
CreatedAt: container.Created,
StartedAt: container.StartedAt,
Expand Down
16 changes: 14 additions & 2 deletions daemon/container.go
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/containerd/containerd/platforms"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/container"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/docker/docker/runconfig"
volumemounts "github.com/docker/docker/volume/mounts"
"github.com/docker/go-connections/nat"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/selinux/go-selinux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -125,7 +127,7 @@ func (daemon *Daemon) Register(c *container.Container) error {
return c.CheckpointTo(daemon.containersReplica)
}

func (daemon *Daemon) newContainer(name string, operatingSystem string, config *containertypes.Config, hostConfig *containertypes.HostConfig, imgID image.ID, managed bool) (*container.Container, error) {
func (daemon *Daemon) newContainer(name string, operatingSystem string, config *containertypes.Config, hostConfig *containertypes.HostConfig, img *image.Image, managed bool) (*container.Container, error) {
var (
id string
err error
Expand Down Expand Up @@ -155,11 +157,21 @@ func (daemon *Daemon) newContainer(name string, operatingSystem string, config *
base.Args = args // FIXME: de-duplicate from config
base.Config = config
base.HostConfig = &containertypes.HostConfig{}
base.ImageID = imgID
base.NetworkSettings = &network.Settings{IsAnonymousEndpoint: noExplicitName}
base.Name = name
base.Driver = daemon.imageService.GraphDriverName()
base.OS = operatingSystem
if img != nil {
base.ImageID = img.ID()
// TODO should we add a "img.Platform()" method to do this?
base.Platform = platforms.Normalize(ocispec.Platform{
Architecture: img.Architecture,
OS: img.OS,
OSVersion: img.OSVersion,
OSFeatures: img.OSFeatures,
Variant: img.Variant,
})
}
return base, err
}

Expand Down
29 changes: 15 additions & 14 deletions daemon/create.go
Expand Up @@ -118,10 +118,9 @@ func (daemon *Daemon) containerCreate(opts createOpts) (containertypes.Container
// Create creates a new container from the given configuration with a given name.
func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
var (
ctr *container.Container
img *image.Image
imgID image.ID
err error
ctr *container.Container
img *image.Image
err error
)

os := runtime.GOOS
Expand All @@ -138,25 +137,27 @@ func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr
os = "linux"
}
}
imgID = img.ID()

if isWindows && img.OS == "linux" && !system.LCOWSupported() {
return nil, errors.New("operating system on which parent image was created is not Windows")
}

// On WCOW, if are not being invoked by the builder to create this container (where
// ignoreImagesArgEscaped will be true) - if the image already has its arguments escaped,
// ensure that this is replicated across to the created container to avoid double-escaping
// of the arguments/command line when the runtime attempts to run the container.
if os == "windows" && !opts.ignoreImagesArgsEscaped && img.RunConfig().ArgsEscaped {
opts.params.Config.ArgsEscaped = true
}
} else {
// TODO instead of a "nil" image, perhaps we should have a "scratch" dummy image that
// - Returns an empty `.ID()`
// - Returns "linux" as OS, and a sensible "architecture" (always runtime.GOARCH?)
if isWindows {
os = "linux" // 'scratch' case.
}
}

// On WCOW, if are not being invoked by the builder to create this container (where
// ignoreImagesArgEscaped will be true) - if the image already has its arguments escaped,
// ensure that this is replicated across to the created container to avoid double-escaping
// of the arguments/command line when the runtime attempts to run the container.
if os == "windows" && !opts.ignoreImagesArgsEscaped && img != nil && img.RunConfig().ArgsEscaped {
opts.params.Config.ArgsEscaped = true
}

if err := daemon.mergeAndVerifyConfig(opts.params.Config, img); err != nil {
return nil, errdefs.InvalidParameter(err)
}
Expand All @@ -165,7 +166,7 @@ func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr
return nil, errdefs.InvalidParameter(err)
}

if ctr, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, imgID, opts.managed); err != nil {
if ctr, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, img, opts.managed); err != nil {
return nil, err
}
defer func() {
Expand Down
3 changes: 3 additions & 0 deletions docs/api/version-history.md
Expand Up @@ -24,6 +24,9 @@ keywords: "API, Docker, rcli, REST, documentation"
* `GET /images/json` now accepts query parameter `shared-size`. When set `true`,
images returned will include `SharedSize`, which provides the size on disk shared
with other images present on the system.
* `GET /containers/json` now includes `Platform` information for containers,
describing platform details of the image that the container was created with
(`Architecture`, `OS`, and optional: `OSVersion`, `OSFeatures`, `Variant`).

## v1.41 API changes

Expand Down
23 changes: 23 additions & 0 deletions integration/container/ps_test.go
Expand Up @@ -46,3 +46,26 @@ func TestPsFilter(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(containerIDs(q2), prev))
}

// TestPsPlatform verifies that containers have a platform set
func TestPsPlatform(t *testing.T) {
defer setupTest(t)()
client := testEnv.APIClient()
ctx := context.Background()

container.Create(ctx, t, client)

containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: true,
})
assert.NilError(t, err)
assert.Check(t, len(containers) > 0)

ctr := containers[0]
if assert.Check(t, ctr.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.Platform.OS, testEnv.DaemonInfo.OSType)
assert.Check(t, ctr.Platform.Architecture != "")
}
}