client/container_create: Add Image outside of Config

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-23 15:13:55 +02:00
parent 3340c86db9
commit 8fb561ca9a
6 changed files with 49 additions and 22 deletions

View File

@@ -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,
}

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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,
}

View File

@@ -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.