Explorar el Código

Merge pull request #40725 from cpuguy83/check_img_platform

Accept platform spec on container create
Tibor Vass hace 5 años
padre
commit
5c10ea6ae8

+ 25 - 0
api/server/router/container/container_routes.go

@@ -9,6 +9,7 @@ import (
 	"strconv"
 	"syscall"
 
+	"github.com/containerd/containerd/platforms"
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
@@ -19,6 +20,7 @@ import (
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/signal"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/net/websocket"
@@ -502,6 +504,28 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
 		}
 	}
 
+	var platform *specs.Platform
+	if versions.GreaterThanOrEqualTo(version, "1.41") {
+		if v := r.Form.Get("platform"); v != "" {
+			p, err := platforms.Parse(v)
+			if err != nil {
+				return errdefs.InvalidParameter(err)
+			}
+			platform = &p
+		}
+		defaultPlatform := platforms.DefaultSpec()
+		if platform == nil {
+			platform = &defaultPlatform
+		}
+		if platform.OS == "" {
+			platform.OS = defaultPlatform.OS
+		}
+		if platform.Architecture == "" {
+			platform.Architecture = defaultPlatform.Architecture
+			platform.Variant = defaultPlatform.Variant
+		}
+	}
+
 	if hostConfig != nil && hostConfig.PidsLimit != nil && *hostConfig.PidsLimit <= 0 {
 		// Don't set a limit if either no limit was specified, or "unlimited" was
 		// explicitly set.
@@ -516,6 +540,7 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
 		HostConfig:       hostConfig,
 		NetworkingConfig: networkingConfig,
 		AdjustCPUShares:  adjustCPUShares,
+		Platform:         platform,
 	})
 	if err != nil {
 		return err

+ 2 - 0
api/types/configs.go

@@ -3,6 +3,7 @@ package types // import "github.com/docker/docker/api/types"
 import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 // configs holds structs used for internal communication between the
@@ -15,6 +16,7 @@ type ContainerCreateConfig struct {
 	Config           *container.Config
 	HostConfig       *container.HostConfig
 	NetworkingConfig *network.NetworkingConfig
+	Platform         *specs.Platform
 	AdjustCPUShares  bool
 }
 

+ 12 - 1
client/container_create.go

@@ -5,20 +5,23 @@ import (
 	"encoding/json"
 	"net/url"
 
+	"github.com/containerd/containerd/platforms"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/versions"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 type configWrapper struct {
 	*container.Config
 	HostConfig       *container.HostConfig
 	NetworkingConfig *network.NetworkingConfig
+	Platform         *specs.Platform
 }
 
 // ContainerCreate creates a new container based in the given configuration.
 // It can be associated with a name, but it's not mandatory.
-func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) {
+func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *specs.Platform, containerName string) (container.ContainerCreateCreatedBody, error) {
 	var response container.ContainerCreateCreatedBody
 
 	if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
@@ -30,7 +33,15 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
 		hostConfig.AutoRemove = false
 	}
 
+	if err := cli.NewVersionError("1.41", "specify container image platform"); platform != nil && err != nil {
+		return response, err
+	}
+
 	query := url.Values{}
+	if platform != nil {
+		query.Set("platform", platforms.Format(*platform))
+	}
+
 	if containerName != "" {
 		query.Set("name", containerName)
 	}

+ 6 - 6
client/container_create_test.go

@@ -18,7 +18,7 @@ func TestContainerCreateError(t *testing.T) {
 	client := &Client{
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
 	}
-	_, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
+	_, err := client.ContainerCreate(context.Background(), nil, nil, nil, nil, "nothing")
 	if !errdefs.IsSystem(err) {
 		t.Fatalf("expected a Server Error while testing StatusInternalServerError, got %T", err)
 	}
@@ -27,7 +27,7 @@ func TestContainerCreateError(t *testing.T) {
 	client = &Client{
 		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
 	}
-	_, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
+	_, err = client.ContainerCreate(context.Background(), nil, nil, nil, nil, "nothing")
 	if err == nil || !IsErrNotFound(err) {
 		t.Fatalf("expected a Server Error while testing StatusNotFound, got %T", err)
 	}
@@ -37,7 +37,7 @@ func TestContainerCreateImageNotFound(t *testing.T) {
 	client := &Client{
 		client: newMockClient(errorMock(http.StatusNotFound, "No such image")),
 	}
-	_, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown")
+	_, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, nil, "unknown")
 	if err == nil || !IsErrNotFound(err) {
 		t.Fatalf("expected an imageNotFound error, got %v, %T", err, err)
 	}
@@ -67,7 +67,7 @@ func TestContainerCreateWithName(t *testing.T) {
 		}),
 	}
 
-	r, err := client.ContainerCreate(context.Background(), nil, nil, nil, "container_name")
+	r, err := client.ContainerCreate(context.Background(), nil, nil, nil, nil, "container_name")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -106,14 +106,14 @@ func TestContainerCreateAutoRemove(t *testing.T) {
 		client:  newMockClient(autoRemoveValidator(false)),
 		version: "1.24",
 	}
-	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
+	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, nil, ""); err != nil {
 		t.Fatal(err)
 	}
 	client = &Client{
 		client:  newMockClient(autoRemoveValidator(true)),
 		version: "1.25",
 	}
-	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
+	if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, nil, ""); err != nil {
 		t.Fatal(err)
 	}
 }

+ 2 - 1
client/interface.go

@@ -16,6 +16,7 @@ import (
 	"github.com/docker/docker/api/types/registry"
 	"github.com/docker/docker/api/types/swarm"
 	volumetypes "github.com/docker/docker/api/types/volume"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 // CommonAPIClient is the common methods between stable and experimental versions of APIClient.
@@ -47,7 +48,7 @@ type CommonAPIClient interface {
 type ContainerAPIClient interface {
 	ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error)
 	ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error)
-	ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, containerName string) (containertypes.ContainerCreateCreatedBody, error)
+	ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error)
 	ContainerDiff(ctx context.Context, container string) ([]containertypes.ContainerChangeResponseItem, error)
 	ContainerExecAttach(ctx context.Context, execID string, config types.ExecStartCheck) (types.HijackedResponse, error)
 	ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error)

+ 2 - 2
daemon/create.go

@@ -60,7 +60,7 @@ func (daemon *Daemon) containerCreate(opts createOpts) (containertypes.Container
 
 	os := runtime.GOOS
 	if opts.params.Config.Image != "" {
-		img, err := daemon.imageService.GetImage(opts.params.Config.Image)
+		img, err := daemon.imageService.GetImage(opts.params.Config.Image, opts.params.Platform)
 		if err == nil {
 			os = img.OS
 		}
@@ -114,7 +114,7 @@ func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr
 
 	os := runtime.GOOS
 	if opts.params.Config.Image != "" {
-		img, err = daemon.imageService.GetImage(opts.params.Config.Image)
+		img, err = daemon.imageService.GetImage(opts.params.Config.Image, opts.params.Platform)
 		if err != nil {
 			return nil, err
 		}

+ 1 - 1
daemon/images/cache.go

@@ -15,7 +15,7 @@ func (i *ImageService) MakeImageCache(sourceRefs []string) builder.ImageCache {
 	cache := cache.New(i.imageStore)
 
 	for _, ref := range sourceRefs {
-		img, err := i.GetImage(ref)
+		img, err := i.GetImage(ref, nil)
 		if err != nil {
 			logrus.Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
 			continue

+ 36 - 1
daemon/images/image.go

@@ -3,9 +3,12 @@ package images // import "github.com/docker/docker/daemon/images"
 import (
 	"fmt"
 
+	"github.com/pkg/errors"
+
 	"github.com/docker/distribution/reference"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/image"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 // ErrImageDoesNotExist is error returned when no image can be found for a reference.
@@ -25,7 +28,39 @@ func (e ErrImageDoesNotExist) Error() string {
 func (e ErrImageDoesNotExist) NotFound() {}
 
 // GetImage returns an image corresponding to the image referred to by refOrID.
-func (i *ImageService) GetImage(refOrID string) (*image.Image, error) {
+func (i *ImageService) GetImage(refOrID string, platform *specs.Platform) (retImg *image.Image, retErr error) {
+	defer func() {
+		if retErr != nil || retImg == nil || platform == nil {
+			return
+		}
+
+		// This allows us to tell clients that we don't have the image they asked for
+		// Where this gets hairy is the image store does not currently support multi-arch images, e.g.:
+		//   An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform
+		//   The image store does not store the manifest list and image tags are assigned to architecture specific images.
+		//   So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images.
+		//   This may be confusing.
+		//   The alternative to this is to return a errdefs.Conflict error with a helpful message, but clients will not be
+		//   able to automatically tell what causes the conflict.
+		if retImg.OS != platform.OS {
+			retErr = errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified OS platform: wanted: %s, actual: %s", refOrID, platform.OS, retImg.OS))
+			retImg = nil
+			return
+		}
+		if retImg.Architecture != platform.Architecture {
+			retErr = errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform cpu architecture: wanted: %s, actual: %s", refOrID, platform.Architecture, retImg.Architecture))
+			retImg = nil
+			return
+		}
+
+		// Only validate variant if retImg has a variant set.
+		// The image variant may not be set since it's a newer field.
+		if platform.Variant != "" && retImg.Variant != "" && retImg.Variant != platform.Variant {
+			retErr = errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform cpu architecture variant: wanted: %s, actual: %s", refOrID, platform.Variant, retImg.Variant))
+			retImg = nil
+			return
+		}
+	}()
 	ref, err := reference.ParseAnyReference(refOrID)
 	if err != nil {
 		return nil, errdefs.InvalidParameter(err)

+ 2 - 2
daemon/images/image_builder.go

@@ -161,7 +161,7 @@ func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConf
 	if err := i.pullImageWithReference(ctx, ref, platform, nil, pullRegistryAuth, output); err != nil {
 		return nil, err
 	}
-	return i.GetImage(name)
+	return i.GetImage(name, platform)
 }
 
 // GetImageAndReleasableLayer returns an image and releaseable layer for a reference or ID.
@@ -184,7 +184,7 @@ func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID s
 	}
 
 	if opts.PullOption != backend.PullOptionForcePull {
-		image, err := i.GetImage(refOrID)
+		image, err := i.GetImage(refOrID, opts.Platform)
 		if err != nil && opts.PullOption == backend.PullOptionNoPull {
 			return nil, nil, err
 		}

+ 1 - 1
daemon/images/image_delete.go

@@ -64,7 +64,7 @@ func (i *ImageService) ImageDelete(imageRef string, force, prune bool) ([]types.
 	start := time.Now()
 	records := []types.ImageDeleteResponseItem{}
 
-	img, err := i.GetImage(imageRef)
+	img, err := i.GetImage(imageRef, nil)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
daemon/images/image_events.go

@@ -11,7 +11,7 @@ func (i *ImageService) LogImageEvent(imageID, refName, action string) {
 
 // LogImageEventWithAttributes generates an event related to an image with specific given attributes.
 func (i *ImageService) LogImageEventWithAttributes(imageID, refName, action string, attributes map[string]string) {
-	img, err := i.GetImage(imageID)
+	img, err := i.GetImage(imageID, nil)
 	if err == nil && img.Config != nil {
 		// image has not been removed yet.
 		// it could be missing if the event is `delete`.

+ 2 - 2
daemon/images/image_history.go

@@ -14,7 +14,7 @@ import (
 // name by walking the image lineage.
 func (i *ImageService) ImageHistory(name string) ([]*image.HistoryResponseItem, error) {
 	start := time.Now()
-	img, err := i.GetImage(name)
+	img, err := i.GetImage(name, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -77,7 +77,7 @@ func (i *ImageService) ImageHistory(name string) ([]*image.HistoryResponseItem,
 		if id == "" {
 			break
 		}
-		histImg, err = i.GetImage(id.String())
+		histImg, err = i.GetImage(id.String(), nil)
 		if err != nil {
 			break
 		}

+ 1 - 1
daemon/images/image_inspect.go

@@ -14,7 +14,7 @@ import (
 // LookupImage looks up an image by name and returns it as an ImageInspect
 // structure.
 func (i *ImageService) LookupImage(name string) (*types.ImageInspect, error) {
-	img, err := i.GetImage(name)
+	img, err := i.GetImage(name, nil)
 	if err != nil {
 		return nil, errors.Wrapf(err, "no such image: %s", name)
 	}

+ 1 - 1
daemon/images/image_tag.go

@@ -8,7 +8,7 @@ import (
 // TagImage creates the tag specified by newTag, pointing to the image named
 // imageName (alternatively, imageName can also be an image ID).
 func (i *ImageService) TagImage(imageName, repository, tag string) (string, error) {
-	img, err := i.GetImage(imageName)
+	img, err := i.GetImage(imageName, nil)
 	if err != nil {
 		return "", err
 	}

+ 2 - 2
daemon/images/images.go

@@ -69,7 +69,7 @@ func (i *ImageService) Images(imageFilters filters.Args, all bool, withExtraAttr
 
 	var beforeFilter, sinceFilter *image.Image
 	err = imageFilters.WalkValues("before", func(value string) error {
-		beforeFilter, err = i.GetImage(value)
+		beforeFilter, err = i.GetImage(value, nil)
 		return err
 	})
 	if err != nil {
@@ -77,7 +77,7 @@ func (i *ImageService) Images(imageFilters filters.Args, all bool, withExtraAttr
 	}
 
 	err = imageFilters.WalkValues("since", func(value string) error {
-		sinceFilter, err = i.GetImage(value)
+		sinceFilter, err = i.GetImage(value, nil)
 		return err
 	})
 	if err != nil {

+ 2 - 2
daemon/list.go

@@ -317,7 +317,7 @@ func (daemon *Daemon) foldFilter(view container.View, config *types.ContainerLis
 	if psFilters.Contains("ancestor") {
 		ancestorFilter = true
 		psFilters.WalkValues("ancestor", func(ancestor string) error {
-			img, err := daemon.imageService.GetImage(ancestor)
+			img, err := daemon.imageService.GetImage(ancestor, nil)
 			if err != nil {
 				logrus.Warnf("Error while looking up for image %v", ancestor)
 				return nil
@@ -581,7 +581,7 @@ func (daemon *Daemon) refreshImage(s *container.Snapshot, ctx *listContext) (*ty
 	c := s.Container
 	image := s.Image // keep the original ref if still valid (hasn't changed)
 	if image != s.ImageID {
-		img, err := daemon.imageService.GetImage(image)
+		img, err := daemon.imageService.GetImage(image, nil)
 		if _, isDNE := err.(images.ErrImageDoesNotExist); err != nil && !isDNE {
 			return nil, err
 		}

+ 1 - 1
daemon/oci_windows.go

@@ -29,7 +29,7 @@ const (
 
 func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 
-	img, err := daemon.imageService.GetImage(string(c.ImageID))
+	img, err := daemon.imageService.GetImage(string(c.ImageID), nil)
 	if err != nil {
 		return nil, err
 	}

+ 25 - 24
integration-cli/docker_api_containers_test.go

@@ -523,7 +523,7 @@ func (s *DockerSuite) TestContainerAPIBadPort(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "")
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 	assert.ErrorContains(c, err, `invalid port specification: "aa80"`)
 }
 
@@ -537,7 +537,7 @@ func (s *DockerSuite) TestContainerAPICreate(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	out, _ := dockerCmd(c, "start", "-a", container.ID)
@@ -550,7 +550,7 @@ func (s *DockerSuite) TestContainerAPICreateEmptyConfig(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &containertypes.Config{}, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	_, err = cli.ContainerCreate(context.Background(), &containertypes.Config{}, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 
 	expected := "No command specified"
 	assert.ErrorContains(c, err, expected)
@@ -574,7 +574,7 @@ func (s *DockerSuite) TestContainerAPICreateMultipleNetworksConfig(c *testing.T)
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networkingConfig, "")
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networkingConfig, nil, "")
 	msg := err.Error()
 	// network name order in error message is not deterministic
 	assert.Assert(c, strings.Contains(msg, "Container cannot be connected to network endpoints"))
@@ -609,7 +609,7 @@ func UtilCreateNetworkMode(c *testing.T, networkMode containertypes.NetworkMode)
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -636,7 +636,7 @@ func (s *DockerSuite) TestContainerAPICreateWithCpuSharesCpuset(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -948,7 +948,7 @@ func (s *DockerSuite) TestContainerAPIStart(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, name)
 	assert.NilError(c, err)
 
 	err = cli.ContainerStart(context.Background(), name, types.ContainerStartOptions{})
@@ -1272,7 +1272,7 @@ func (s *DockerSuite) TestPostContainerAPICreateWithStringOrSliceEntrypoint(c *t
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "echotest")
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "echotest")
 	assert.NilError(c, err)
 	out, _ := dockerCmd(c, "start", "-a", "echotest")
 	assert.Equal(c, strings.TrimSpace(out), "hello world")
@@ -1299,7 +1299,7 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCmd(c *testing.T)
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "echotest")
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "echotest")
 	assert.NilError(c, err)
 	out, _ := dockerCmd(c, "start", "-a", "echotest")
 	assert.Equal(c, strings.TrimSpace(out), "hello world")
@@ -1342,7 +1342,7 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *tes
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config2, &hostConfig, &networktypes.NetworkingConfig{}, "capaddtest1")
+	_, err = cli.ContainerCreate(context.Background(), &config2, &hostConfig, &networktypes.NetworkingConfig{}, nil, "capaddtest1")
 	assert.NilError(c, err)
 }
 
@@ -1356,7 +1356,7 @@ func (s *DockerSuite) TestContainerAPICreateNoHostConfig118(c *testing.T) {
 	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("v1.18"))
 	assert.NilError(c, err)
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 }
 
@@ -1407,7 +1407,7 @@ func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *testing.T
 	}
 	name := "wrong-cpuset-cpus"
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig1, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig1, &networktypes.NetworkingConfig{}, nil, name)
 	expected := "Invalid value 1-42,, for cpuset cpus"
 	assert.ErrorContains(c, err, expected)
 
@@ -1417,7 +1417,7 @@ func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *testing.T
 		},
 	}
 	name = "wrong-cpuset-mems"
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig2, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig2, &networktypes.NetworkingConfig{}, nil, name)
 	expected = "Invalid value 42-3,1-- for cpuset mems"
 	assert.ErrorContains(c, err, expected)
 }
@@ -1436,7 +1436,7 @@ func (s *DockerSuite) TestPostContainersCreateShmSizeNegative(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "")
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 	assert.ErrorContains(c, err, "SHM size can not be less than 0")
 }
 
@@ -1453,7 +1453,7 @@ func (s *DockerSuite) TestPostContainersCreateShmSizeHostConfigOmitted(c *testin
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -1480,7 +1480,7 @@ func (s *DockerSuite) TestPostContainersCreateShmSizeOmitted(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -1511,7 +1511,7 @@ func (s *DockerSuite) TestPostContainersCreateWithShmSize(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -1537,7 +1537,7 @@ func (s *DockerSuite) TestPostContainersCreateMemorySwappinessHostConfigOmitted(
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, "")
+	container, err := cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, "")
 	assert.NilError(c, err)
 
 	containerJSON, err := cli.ContainerInspect(context.Background(), container.ID)
@@ -1568,7 +1568,7 @@ func (s *DockerSuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *tes
 	defer cli.Close()
 
 	name := "oomscoreadj-over"
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, name)
 
 	expected := "Invalid value 1001, range for oom score adj is [-1000, 1000]"
 	assert.ErrorContains(c, err, expected)
@@ -1578,7 +1578,7 @@ func (s *DockerSuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *tes
 	}
 
 	name = "oomscoreadj-low"
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, name)
 
 	expected = "Invalid value -1001, range for oom score adj is [-1000, 1000]"
 	assert.ErrorContains(c, err, expected)
@@ -1610,7 +1610,7 @@ func (s *DockerSuite) TestContainerAPIStatsWithNetworkDisabled(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, name)
+	_, err = cli.ContainerCreate(context.Background(), &config, &containertypes.HostConfig{}, &networktypes.NetworkingConfig{}, nil, name)
 	assert.NilError(c, err)
 
 	err = cli.ContainerStart(context.Background(), name, types.ContainerStartOptions{})
@@ -1926,7 +1926,7 @@ func (s *DockerSuite) TestContainersAPICreateMountsValidation(c *testing.T) {
 	for i, x := range cases {
 		x := x
 		c.Run(fmt.Sprintf("case %d", i), func(c *testing.T) {
-			_, err = apiClient.ContainerCreate(context.Background(), &x.config, &x.hostConfig, &networktypes.NetworkingConfig{}, "")
+			_, err = apiClient.ContainerCreate(context.Background(), &x.config, &x.hostConfig, &networktypes.NetworkingConfig{}, nil, "")
 			if len(x.msg) > 0 {
 				assert.ErrorContains(c, err, x.msg, "%v", cases[i].config)
 			} else {
@@ -1959,7 +1959,7 @@ func (s *DockerSuite) TestContainerAPICreateMountsBindRead(c *testing.T) {
 	assert.NilError(c, err)
 	defer cli.Close()
 
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, "test")
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, "test")
 	assert.NilError(c, err)
 
 	out, _ := dockerCmd(c, "start", "-a", "test")
@@ -2106,6 +2106,7 @@ func (s *DockerSuite) TestContainersAPICreateMountsCreate(c *testing.T) {
 				&containertypes.Config{Image: testImg},
 				&containertypes.HostConfig{Mounts: []mounttypes.Mount{x.spec}},
 				&networktypes.NetworkingConfig{},
+				nil,
 				"")
 			assert.NilError(c, err)
 
@@ -2213,7 +2214,7 @@ func (s *DockerSuite) TestContainersAPICreateMountsTmpfs(c *testing.T) {
 			Mounts: []mounttypes.Mount{x.cfg},
 		}
 
-		_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, cName)
+		_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &networktypes.NetworkingConfig{}, nil, cName)
 		assert.NilError(c, err)
 		out, _ := dockerCmd(c, "start", "-a", cName)
 		for _, option := range x.expectedOptions {

+ 1 - 1
integration-cli/docker_api_containers_windows_test.go

@@ -65,7 +65,7 @@ func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *testing.T) {
 				},
 			},
 		},
-		nil, name)
+		nil, nil, name)
 	assert.NilError(c, err)
 
 	err = client.ContainerStart(ctx, name, types.ContainerStartOptions{})

+ 1 - 1
integration-cli/docker_cli_volume_test.go

@@ -578,7 +578,7 @@ func (s *DockerSuite) TestDuplicateMountpointsForVolumesFromAndMounts(c *testing
 			},
 		},
 	}
-	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &network.NetworkingConfig{}, "app")
+	_, err = cli.ContainerCreate(context.Background(), &config, &hostConfig, &network.NetworkingConfig{}, nil, "app")
 
 	assert.NilError(c, err)
 

+ 41 - 1
integration/container/create_test.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
+	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/client"
@@ -17,6 +18,7 @@ import (
 	ctr "github.com/docker/docker/integration/internal/container"
 	"github.com/docker/docker/oci"
 	"github.com/docker/docker/testutil/request"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 	"gotest.tools/v3/poll"
@@ -57,6 +59,7 @@ func TestCreateFailsWhenIdentifierDoesNotExist(t *testing.T) {
 				&container.Config{Image: tc.image},
 				&container.HostConfig{},
 				&network.NetworkingConfig{},
+				nil,
 				"",
 			)
 			assert.Check(t, is.ErrorContains(err, tc.expectedError))
@@ -81,6 +84,7 @@ func TestCreateLinkToNonExistingContainer(t *testing.T) {
 			Links: []string{"no-such-container"},
 		},
 		&network.NetworkingConfig{},
+		nil,
 		"",
 	)
 	assert.Check(t, is.ErrorContains(err, "could not get container for no-such-container"))
@@ -120,6 +124,7 @@ func TestCreateWithInvalidEnv(t *testing.T) {
 				},
 				&container.HostConfig{},
 				&network.NetworkingConfig{},
+				nil,
 				"",
 			)
 			assert.Check(t, is.ErrorContains(err, tc.expectedError))
@@ -166,6 +171,7 @@ func TestCreateTmpfsMountsTarget(t *testing.T) {
 				Tmpfs: map[string]string{tc.target: ""},
 			},
 			&network.NetworkingConfig{},
+			nil,
 			"",
 		)
 		assert.Check(t, is.ErrorContains(err, tc.expectedError))
@@ -235,6 +241,7 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
 			&config,
 			&hc,
 			&network.NetworkingConfig{},
+			nil,
 			name,
 		)
 		assert.NilError(t, err)
@@ -361,6 +368,7 @@ func TestCreateWithCapabilities(t *testing.T) {
 				&container.Config{Image: "busybox"},
 				&tc.hostConfig,
 				&network.NetworkingConfig{},
+				nil,
 				"",
 			)
 			if tc.expectedError == "" {
@@ -439,6 +447,7 @@ func TestCreateWithCustomReadonlyPaths(t *testing.T) {
 			&config,
 			&hc,
 			&network.NetworkingConfig{},
+			nil,
 			name,
 		)
 		assert.NilError(t, err)
@@ -522,7 +531,7 @@ func TestCreateWithInvalidHealthcheckParams(t *testing.T) {
 				cfg.Healthcheck.StartPeriod = tc.startPeriod
 			}
 
-			resp, err := client.ContainerCreate(ctx, &cfg, &container.HostConfig{}, nil, "")
+			resp, err := client.ContainerCreate(ctx, &cfg, &container.HostConfig{}, nil, nil, "")
 			assert.Check(t, is.Equal(len(resp.Warnings), 0))
 
 			if versions.LessThan(testEnv.DaemonAPIVersion(), "1.32") {
@@ -581,3 +590,34 @@ func TestCreateTmpfsOverrideAnonymousVolume(t *testing.T) {
 		assert.NilError(t, err)
 	}
 }
+
+// Test that if the referenced image platform does not match the requested platform on container create that we get an
+// error.
+func TestCreateDifferentPlatform(t *testing.T) {
+	defer setupTest(t)()
+	c := testEnv.APIClient()
+	ctx := context.Background()
+
+	img, _, err := c.ImageInspectWithRaw(ctx, "busybox:latest")
+	assert.NilError(t, err)
+	assert.Assert(t, img.Architecture != "")
+
+	t.Run("different os", func(t *testing.T) {
+		p := specs.Platform{
+			OS:           img.Os + "DifferentOS",
+			Architecture: img.Architecture,
+			Variant:      img.Variant,
+		}
+		_, err := c.ContainerCreate(ctx, &containertypes.Config{Image: "busybox:latest"}, &containertypes.HostConfig{}, nil, &p, "")
+		assert.Assert(t, client.IsErrNotFound(err), err)
+	})
+	t.Run("different cpu arch", func(t *testing.T) {
+		p := specs.Platform{
+			OS:           img.Os,
+			Architecture: img.Architecture + "DifferentArch",
+			Variant:      img.Variant,
+		}
+		_, err := c.ContainerCreate(ctx, &containertypes.Config{Image: "busybox:latest"}, &containertypes.HostConfig{}, nil, &p, "")
+		assert.Assert(t, client.IsErrNotFound(err), err)
+	})
+}

+ 5 - 5
integration/container/ipcmode_linux_test.go

@@ -66,7 +66,7 @@ func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool,
 	client := testEnv.APIClient()
 	ctx := context.Background()
 
-	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
+	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(len(resp.Warnings), 0))
 
@@ -138,7 +138,7 @@ func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
 	client := testEnv.APIClient()
 
 	// create and start the "donor" container
-	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
+	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(len(resp.Warnings), 0))
 	name1 := resp.ID
@@ -148,7 +148,7 @@ func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
 
 	// create and start the second container
 	hostCfg.IpcMode = containertypes.IpcMode("container:" + name1)
-	resp, err = client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
+	resp, err = client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(len(resp.Warnings), 0))
 	name2 := resp.ID
@@ -204,7 +204,7 @@ func TestAPIIpcModeHost(t *testing.T) {
 	ctx := context.Background()
 
 	client := testEnv.APIClient()
-	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
+	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(len(resp.Warnings), 0))
 	name := resp.ID
@@ -241,7 +241,7 @@ func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...strin
 	}
 	ctx := context.Background()
 
-	resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, "")
+	resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "")
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(len(resp.Warnings), 0))
 

+ 2 - 2
integration/container/mounts_linux_test.go

@@ -63,7 +63,7 @@ func TestContainerNetworkMountsNoChown(t *testing.T) {
 	assert.NilError(t, err)
 	defer cli.Close()
 
-	ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, "")
+	ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
 	assert.NilError(t, err)
 	// container will exit immediately because of no tty, but we only need the start sequence to test the condition
 	err = cli.ContainerStart(ctx, ctrCreate.ID, types.ContainerStartOptions{})
@@ -174,7 +174,7 @@ func TestMountDaemonRoot(t *testing.T) {
 					c, err := client.ContainerCreate(ctx, &containertypes.Config{
 						Image: "busybox",
 						Cmd:   []string{"true"},
-					}, hc, nil, "")
+					}, hc, nil, nil, "")
 
 					if err != nil {
 						if test.expected != "" {

+ 1 - 1
integration/container/restart_test.go

@@ -77,7 +77,7 @@ func TestDaemonRestartKillContainers(t *testing.T) {
 					defer d.Stop(t)
 					ctx := context.Background()
 
-					resp, err := client.ContainerCreate(ctx, c.config, c.hostConfig, nil, "")
+					resp, err := client.ContainerCreate(ctx, c.config, c.hostConfig, nil, nil, "")
 					assert.NilError(t, err)
 					defer client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
 

+ 3 - 1
integration/internal/container/container.go

@@ -9,6 +9,7 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/client"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"gotest.tools/v3/assert"
 )
 
@@ -19,6 +20,7 @@ type TestContainerConfig struct {
 	Config           *container.Config
 	HostConfig       *container.HostConfig
 	NetworkingConfig *network.NetworkingConfig
+	Platform         *specs.Platform
 }
 
 // create creates a container with the specified options
@@ -41,7 +43,7 @@ func create(ctx context.Context, t *testing.T, client client.APIClient, ops ...f
 		op(config)
 	}
 
-	return client.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Name)
+	return client.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
 }
 
 // Create creates a container with the specified options, asserting that there was no error

+ 8 - 0
integration/internal/container/ops.go

@@ -9,6 +9,7 @@ import (
 	networktypes "github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/strslice"
 	"github.com/docker/go-connections/nat"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
 // WithName sets the name of the container
@@ -205,3 +206,10 @@ func WithExtraHost(extraHost string) func(*TestContainerConfig) {
 		c.HostConfig.ExtraHosts = append(c.HostConfig.ExtraHosts, extraHost)
 	}
 }
+
+// WithPlatform specifies the desired platform the image should have.
+func WithPlatform(p *specs.Platform) func(*TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		c.Platform = p
+	}
+}

+ 1 - 0
integration/plugin/logging/read_test.go

@@ -54,6 +54,7 @@ func TestReadPluginNoRead(t *testing.T) {
 				cfg,
 				&container.HostConfig{LogConfig: container.LogConfig{Type: "test"}},
 				nil,
+				nil,
 				"",
 			)
 			assert.Assert(t, err)

+ 1 - 1
testutil/fakestorage/storage.go

@@ -155,7 +155,7 @@ COPY . /static`); err != nil {
 	// Start the container
 	b, err := c.ContainerCreate(context.Background(), &containertypes.Config{
 		Image: image,
-	}, &containertypes.HostConfig{}, nil, container)
+	}, &containertypes.HostConfig{}, nil, nil, container)
 	assert.NilError(t, err)
 	err = c.ContainerStart(context.Background(), b.ID, types.ContainerStartOptions{})
 	assert.NilError(t, err)