diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index c1db782db8..67205071f4 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" @@ -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 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 4f9fd67354..aabad4a911 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 dd1160971d..2bf4418419 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 aedc4c10a5..fde1342e69 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 @@ -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 } 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 44858e9d07..3a30ea8ab2 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 3d35c0b30c..f3e1e348b0 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 abd5f10890..d5c415125c 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 6e9fc8cfa9..1ebdb32fbd 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 1aed0d4745..e09a6e6081 100644 --- a/integration/container/restart_test.go +++ b/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}) 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)