From 7a9cb29fb980c0ab3928272cdc24c7089b2fcf64 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 19 Mar 2020 13:54:48 -0700 Subject: [PATCH] Accept platform spec on container create This enables image lookup when creating a container to fail when the reference exists but it is for the wrong platform. This prevents trying to run an image for the wrong platform, as can be the case with, for example binfmt_misc+qemu. Signed-off-by: Brian Goff --- .../router/container/container_routes.go | 25 ++++++++++ api/types/configs.go | 2 + client/container_create.go | 13 ++++- client/container_create_test.go | 12 ++--- client/interface.go | 3 +- daemon/create.go | 4 +- daemon/images/cache.go | 2 +- daemon/images/image.go | 37 +++++++++++++- daemon/images/image_builder.go | 4 +- daemon/images/image_delete.go | 2 +- daemon/images/image_events.go | 2 +- daemon/images/image_history.go | 4 +- daemon/images/image_inspect.go | 2 +- daemon/images/image_tag.go | 2 +- daemon/images/images.go | 4 +- daemon/list.go | 4 +- daemon/oci_windows.go | 2 +- integration-cli/docker_api_containers_test.go | 49 ++++++++++--------- .../docker_api_containers_windows_test.go | 2 +- integration-cli/docker_cli_volume_test.go | 2 +- integration/container/create_test.go | 42 +++++++++++++++- integration/container/ipcmode_linux_test.go | 10 ++-- integration/container/mounts_linux_test.go | 4 +- integration/container/restart_test.go | 2 +- integration/internal/container/container.go | 4 +- integration/internal/container/ops.go | 8 +++ integration/plugin/logging/read_test.go | 1 + testutil/fakestorage/storage.go | 2 +- 28 files changed, 188 insertions(+), 62 deletions(-) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index b41e540ad0..4f90bdf29c 100644 --- a/api/server/router/container/container_routes.go +++ b/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" @@ -497,6 +499,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. @@ -511,6 +535,7 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo HostConfig: hostConfig, NetworkingConfig: networkingConfig, AdjustCPUShares: adjustCPUShares, + Platform: platform, }) if err != nil { return err diff --git a/api/types/configs.go b/api/types/configs.go index 178e911a7a..3dd133a3a5 100644 --- a/api/types/configs.go +++ b/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 } diff --git a/client/container_create.go b/client/container_create.go index 5b795e0c17..b1d5fea5bd 100644 --- a/client/container_create.go +++ b/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) } diff --git a/client/container_create_test.go b/client/container_create_test.go index 3922ebc3be..7b12df6090 100644 --- a/client/container_create_test.go +++ b/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) } } diff --git a/client/interface.go b/client/interface.go index cde64be4b5..c972cefa0c 100644 --- a/client/interface.go +++ b/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) diff --git a/daemon/create.go b/daemon/create.go index 7310c7cf1d..920fbde258 100644 --- a/daemon/create.go +++ b/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 } diff --git a/daemon/images/cache.go b/daemon/images/cache.go index 3b433106e8..445b1b9261 100644 --- a/daemon/images/cache.go +++ b/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 diff --git a/daemon/images/image.go b/daemon/images/image.go index 79cc07c4fd..f060700f67 100644 --- a/daemon/images/image.go +++ b/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) diff --git a/daemon/images/image_builder.go b/daemon/images/image_builder.go index 320ffcf4cb..2fd4335d6b 100644 --- a/daemon/images/image_builder.go +++ b/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 } diff --git a/daemon/images/image_delete.go b/daemon/images/image_delete.go index fbd6c16b74..c4acf89999 100644 --- a/daemon/images/image_delete.go +++ b/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 } diff --git a/daemon/images/image_events.go b/daemon/images/image_events.go index d0b3064d70..1d8cfcd914 100644 --- a/daemon/images/image_events.go +++ b/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`. diff --git a/daemon/images/image_history.go b/daemon/images/image_history.go index b4ca25b1b6..06422bd842 100644 --- a/daemon/images/image_history.go +++ b/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 } diff --git a/daemon/images/image_inspect.go b/daemon/images/image_inspect.go index 60a673d950..12742e8bf1 100644 --- a/daemon/images/image_inspect.go +++ b/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) } diff --git a/daemon/images/image_tag.go b/daemon/images/image_tag.go index 4693611c3a..becd2e2df3 100644 --- a/daemon/images/image_tag.go +++ b/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 } diff --git a/daemon/images/images.go b/daemon/images/images.go index 0288556e68..a15231da82 100644 --- a/daemon/images/images.go +++ b/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 { diff --git a/daemon/list.go b/daemon/list.go index 19f806ce43..12d9c84ced 100644 --- a/daemon/list.go +++ b/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 @@ -585,7 +585,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 } diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 3b4d46dd12..cc94c9ecdd 100644 --- a/daemon/oci_windows.go +++ b/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 } diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index af46897930..daf48e0cbe 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/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 { diff --git a/integration-cli/docker_api_containers_windows_test.go b/integration-cli/docker_api_containers_windows_test.go index 0fcbcd8128..0423d18426 100644 --- a/integration-cli/docker_api_containers_windows_test.go +++ b/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{}) diff --git a/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go index c43dd088e8..0218a8c9dc 100644 --- a/integration-cli/docker_cli_volume_test.go +++ b/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) diff --git a/integration/container/create_test.go b/integration/container/create_test.go index f1b6d32906..ff701e619e 100644 --- a/integration/container/create_test.go +++ b/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) + }) +} diff --git a/integration/container/ipcmode_linux_test.go b/integration/container/ipcmode_linux_test.go index b80f4cff79..199087c06c 100644 --- a/integration/container/ipcmode_linux_test.go +++ b/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)) diff --git a/integration/container/mounts_linux_test.go b/integration/container/mounts_linux_test.go index 7a515e51ea..edae6a65eb 100644 --- a/integration/container/mounts_linux_test.go +++ b/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 != "" { diff --git a/integration/container/restart_test.go b/integration/container/restart_test.go index e74809d4f5..15459a80cf 100644 --- a/integration/container/restart_test.go +++ b/integration/container/restart_test.go @@ -76,7 +76,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}) diff --git a/integration/internal/container/container.go b/integration/internal/container/container.go index 46a2c51daa..d082c6000b 100644 --- a/integration/internal/container/container.go +++ b/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 diff --git a/integration/internal/container/ops.go b/integration/internal/container/ops.go index 57275587ac..dae5a2a512 100644 --- a/integration/internal/container/ops.go +++ b/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 + } +} diff --git a/integration/plugin/logging/read_test.go b/integration/plugin/logging/read_test.go index 855e671866..028a488cdf 100644 --- a/integration/plugin/logging/read_test.go +++ b/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) diff --git a/testutil/fakestorage/storage.go b/testutil/fakestorage/storage.go index b5fb474c34..85ff858c72 100644 --- a/testutil/fakestorage/storage.go +++ b/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)