Browse code

client/container_create: Add `Image` outside of Config

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2025/10/23 22:13:55
Showing 6 changed files
... ...
@@ -16,8 +16,23 @@ import (
16 16
 // ContainerCreate creates a new container based on the given configuration.
17 17
 // It can be associated with a name, but it's not mandatory.
18 18
 func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) {
19
-	if options.Config == nil {
20
-		return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil")
19
+	cfg := options.Config
20
+
21
+	if cfg == nil {
22
+		cfg = &container.Config{}
23
+	}
24
+
25
+	if options.Image != "" {
26
+		if cfg.Image != "" {
27
+			return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set")
28
+		}
29
+		newCfg := *cfg
30
+		newCfg.Image = options.Image
31
+		cfg = &newCfg
32
+	}
33
+
34
+	if cfg.Image == "" {
35
+		return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required")
21 36
 	}
22 37
 
23 38
 	var response container.CreateResponse
... ...
@@ -39,7 +54,7 @@ func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateO
39 39
 	}
40 40
 
41 41
 	body := container.CreateRequest{
42
-		Config:           options.Config,
42
+		Config:           cfg,
43 43
 		HostConfig:       options.HostConfig,
44 44
 		NetworkingConfig: options.NetworkingConfig,
45 45
 	}
... ...
@@ -13,6 +13,9 @@ type ContainerCreateOptions struct {
13 13
 	NetworkingConfig *network.NetworkingConfig
14 14
 	Platform         *ocispec.Platform
15 15
 	Name             string
16
+
17
+	// Image is a shortcut for Config.Image - only one of Image or Config.Image should be set.
18
+	Image string
16 19
 }
17 20
 
18 21
 // ContainerCreateResult is the result from creating a container.
... ...
@@ -23,20 +23,11 @@ func TestContainerCreateError(t *testing.T) {
23 23
 	assert.NilError(t, err)
24 24
 
25 25
 	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: nil, Name: "nothing"})
26
-	assert.Error(t, err, "config is nil")
26
+	assert.Error(t, err, "config.Image or Image is required")
27 27
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
28 28
 
29 29
 	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "nothing"})
30
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
31
-
32
-	// 404 doesn't automatically means an unknown image
33
-	client, err = NewClientWithOpts(
34
-		WithMockClient(errorMock(http.StatusNotFound, "Server error")),
35
-	)
36
-	assert.NilError(t, err)
37
-
38
-	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "nothing"})
39
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
30
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
40 31
 }
41 32
 
42 33
 func TestContainerCreateImageNotFound(t *testing.T) {
... ...
@@ -74,7 +65,7 @@ func TestContainerCreateWithName(t *testing.T) {
74 74
 	)
75 75
 	assert.NilError(t, err)
76 76
 
77
-	r, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "container_name"})
77
+	r, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, Name: "container_name"})
78 78
 	assert.NilError(t, err)
79 79
 	assert.Check(t, is.Equal(r.ID, "container_id"))
80 80
 }
... ...
@@ -103,7 +94,7 @@ func TestContainerCreateAutoRemove(t *testing.T) {
103 103
 	)
104 104
 	assert.NilError(t, err)
105 105
 
106
-	resp, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, HostConfig: &container.HostConfig{AutoRemove: true}})
106
+	resp, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{AutoRemove: true}})
107 107
 	assert.NilError(t, err)
108 108
 	assert.Check(t, is.Equal(resp.ID, "container_id"))
109 109
 }
... ...
@@ -116,7 +107,7 @@ func TestContainerCreateConnectionError(t *testing.T) {
116 116
 	client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
117 117
 	assert.NilError(t, err)
118 118
 
119
-	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}})
119
+	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}})
120 120
 	assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
121 121
 }
122 122
 
... ...
@@ -165,6 +156,6 @@ func TestContainerCreateCapabilities(t *testing.T) {
165 165
 	)
166 166
 	assert.NilError(t, err)
167 167
 
168
-	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, HostConfig: &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}})
168
+	_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}})
169 169
 	assert.NilError(t, err)
170 170
 }
... ...
@@ -557,7 +557,7 @@ func (s *DockerAPISuite) TestContainerAPICreateEmptyConfig(c *testing.T) {
557 557
 		NetworkingConfig: &network.NetworkingConfig{},
558 558
 	})
559 559
 
560
-	assert.ErrorContains(c, err, "no command specified")
560
+	assert.ErrorContains(c, err, "config.Image or Image is required")
561 561
 }
562 562
 
563 563
 func (s *DockerAPISuite) TestContainerAPICreateBridgeNetworkMode(c *testing.T) {
... ...
@@ -16,8 +16,23 @@ import (
16 16
 // ContainerCreate creates a new container based on the given configuration.
17 17
 // It can be associated with a name, but it's not mandatory.
18 18
 func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) {
19
-	if options.Config == nil {
20
-		return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil")
19
+	cfg := options.Config
20
+
21
+	if cfg == nil {
22
+		cfg = &container.Config{}
23
+	}
24
+
25
+	if options.Image != "" {
26
+		if cfg.Image != "" {
27
+			return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set")
28
+		}
29
+		newCfg := *cfg
30
+		newCfg.Image = options.Image
31
+		cfg = &newCfg
32
+	}
33
+
34
+	if cfg.Image == "" {
35
+		return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required")
21 36
 	}
22 37
 
23 38
 	var response container.CreateResponse
... ...
@@ -39,7 +54,7 @@ func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateO
39 39
 	}
40 40
 
41 41
 	body := container.CreateRequest{
42
-		Config:           options.Config,
42
+		Config:           cfg,
43 43
 		HostConfig:       options.HostConfig,
44 44
 		NetworkingConfig: options.NetworkingConfig,
45 45
 	}
... ...
@@ -13,6 +13,9 @@ type ContainerCreateOptions struct {
13 13
 	NetworkingConfig *network.NetworkingConfig
14 14
 	Platform         *ocispec.Platform
15 15
 	Name             string
16
+
17
+	// Image is a shortcut for Config.Image - only one of Image or Config.Image should be set.
18
+	Image string
16 19
 }
17 20
 
18 21
 // ContainerCreateResult is the result from creating a container.