From 8fb561ca9ade6db0b7a510ce9032a25b18ec96f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 23 Oct 2025 15:13:55 +0200 Subject: [PATCH] client/container_create: Add `Image` outside of Config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- client/container_create.go | 21 ++++++++++++++++--- client/container_create_opts.go | 3 +++ client/container_create_test.go | 21 ++++++------------- integration-cli/docker_api_containers_test.go | 2 +- .../moby/moby/client/container_create.go | 21 ++++++++++++++++--- .../moby/moby/client/container_create_opts.go | 3 +++ 6 files changed, 49 insertions(+), 22 deletions(-) diff --git a/client/container_create.go b/client/container_create.go index 9f5bbb28fb..d941a37207 100644 --- a/client/container_create.go +++ b/client/container_create.go @@ -16,8 +16,23 @@ import ( // ContainerCreate creates a new container based on the given configuration. // It can be associated with a name, but it's not mandatory. func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) { - if options.Config == nil { - return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil") + cfg := options.Config + + if cfg == nil { + cfg = &container.Config{} + } + + if options.Image != "" { + if cfg.Image != "" { + return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set") + } + newCfg := *cfg + newCfg.Image = options.Image + cfg = &newCfg + } + + if cfg.Image == "" { + return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required") } var response container.CreateResponse @@ -39,7 +54,7 @@ func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateO } body := container.CreateRequest{ - Config: options.Config, + Config: cfg, HostConfig: options.HostConfig, NetworkingConfig: options.NetworkingConfig, } diff --git a/client/container_create_opts.go b/client/container_create_opts.go index 7f5d234cbe..8580e20d3a 100644 --- a/client/container_create_opts.go +++ b/client/container_create_opts.go @@ -13,6 +13,9 @@ type ContainerCreateOptions struct { NetworkingConfig *network.NetworkingConfig Platform *ocispec.Platform Name string + + // Image is a shortcut for Config.Image - only one of Image or Config.Image should be set. + Image string } // ContainerCreateResult is the result from creating a container. diff --git a/client/container_create_test.go b/client/container_create_test.go index 807b963abd..b4ea8be603 100644 --- a/client/container_create_test.go +++ b/client/container_create_test.go @@ -23,20 +23,11 @@ func TestContainerCreateError(t *testing.T) { assert.NilError(t, err) _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: nil, Name: "nothing"}) - assert.Error(t, err, "config is nil") + assert.Error(t, err, "config.Image or Image is required") assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "nothing"}) - assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) - - // 404 doesn't automatically means an unknown image - client, err = NewClientWithOpts( - WithMockClient(errorMock(http.StatusNotFound, "Server error")), - ) - assert.NilError(t, err) - - _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "nothing"}) - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) } func TestContainerCreateImageNotFound(t *testing.T) { @@ -74,7 +65,7 @@ func TestContainerCreateWithName(t *testing.T) { ) assert.NilError(t, err) - r, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "container_name"}) + r, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, Name: "container_name"}) assert.NilError(t, err) assert.Check(t, is.Equal(r.ID, "container_id")) } @@ -103,7 +94,7 @@ func TestContainerCreateAutoRemove(t *testing.T) { ) assert.NilError(t, err) - resp, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, HostConfig: &container.HostConfig{AutoRemove: true}}) + resp, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{AutoRemove: true}}) assert.NilError(t, err) assert.Check(t, is.Equal(resp.ID, "container_id")) } @@ -116,7 +107,7 @@ func TestContainerCreateConnectionError(t *testing.T) { client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) - _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}}) + _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}}) assert.Check(t, is.ErrorType(err, IsErrConnectionFailed)) } @@ -165,6 +156,6 @@ func TestContainerCreateCapabilities(t *testing.T) { ) assert.NilError(t, err) - _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, HostConfig: &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}}) + _, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}}) assert.NilError(t, err) } diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index b1848a31c0..9ef700f01d 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -557,7 +557,7 @@ func (s *DockerAPISuite) TestContainerAPICreateEmptyConfig(c *testing.T) { NetworkingConfig: &network.NetworkingConfig{}, }) - assert.ErrorContains(c, err, "no command specified") + assert.ErrorContains(c, err, "config.Image or Image is required") } func (s *DockerAPISuite) TestContainerAPICreateBridgeNetworkMode(c *testing.T) { diff --git a/vendor/github.com/moby/moby/client/container_create.go b/vendor/github.com/moby/moby/client/container_create.go index 9f5bbb28fb..d941a37207 100644 --- a/vendor/github.com/moby/moby/client/container_create.go +++ b/vendor/github.com/moby/moby/client/container_create.go @@ -16,8 +16,23 @@ import ( // ContainerCreate creates a new container based on the given configuration. // It can be associated with a name, but it's not mandatory. func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) { - if options.Config == nil { - return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil") + cfg := options.Config + + if cfg == nil { + cfg = &container.Config{} + } + + if options.Image != "" { + if cfg.Image != "" { + return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set") + } + newCfg := *cfg + newCfg.Image = options.Image + cfg = &newCfg + } + + if cfg.Image == "" { + return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required") } var response container.CreateResponse @@ -39,7 +54,7 @@ func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateO } body := container.CreateRequest{ - Config: options.Config, + Config: cfg, HostConfig: options.HostConfig, NetworkingConfig: options.NetworkingConfig, } diff --git a/vendor/github.com/moby/moby/client/container_create_opts.go b/vendor/github.com/moby/moby/client/container_create_opts.go index 7f5d234cbe..8580e20d3a 100644 --- a/vendor/github.com/moby/moby/client/container_create_opts.go +++ b/vendor/github.com/moby/moby/client/container_create_opts.go @@ -13,6 +13,9 @@ type ContainerCreateOptions struct { NetworkingConfig *network.NetworkingConfig Platform *ocispec.Platform Name string + + // Image is a shortcut for Config.Image - only one of Image or Config.Image should be set. + Image string } // ContainerCreateResult is the result from creating a container.