Skip to content

Commit 5d90d27

Browse files
authored
feat(spec): add containerName field to DAG-level container (#1496)
1 parent f04786c commit 5d90d27

8 files changed

Lines changed: 124 additions & 13 deletions

File tree

internal/core/container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
// Container defines the container configuration for the DAG.
99
type Container struct {
10+
// Name is the container name to use. If empty, Docker generates a random name.
11+
Name string `yaml:"name,omitempty"`
1012
// Image is the container image to use.
1113
Image string `yaml:"image,omitempty"`
1214
// PullPolicy is the policy to pull the image (e.g., "Always", "IfNotPresent").

internal/core/spec/builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ func buildContainer(ctx BuildContext, spec *definition, dag *core.DAG) error {
330330
}
331331

332332
container := core.Container{
333+
Name: strings.TrimSpace(spec.Container.Name),
333334
Image: spec.Container.Image,
334335
PullPolicy: pullPolicy,
335336
Env: envs,

internal/core/spec/builder_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2836,6 +2836,54 @@ steps:
28362836
assert.Equal(t, core.PullPolicyAlways, dag.Container.PullPolicy)
28372837
})
28382838

2839+
t.Run("ContainerWithName", func(t *testing.T) {
2840+
yaml := `
2841+
container:
2842+
name: my-dag-container
2843+
image: alpine:latest
2844+
steps:
2845+
- name: step1
2846+
command: echo hello
2847+
`
2848+
ctx := context.Background()
2849+
dag, err := spec.LoadYAML(ctx, []byte(yaml))
2850+
require.NoError(t, err)
2851+
require.NotNil(t, dag.Container)
2852+
assert.Equal(t, "my-dag-container", dag.Container.Name)
2853+
assert.Equal(t, "alpine:latest", dag.Container.Image)
2854+
})
2855+
2856+
t.Run("ContainerNameEmpty", func(t *testing.T) {
2857+
yaml := `
2858+
container:
2859+
image: alpine:latest
2860+
steps:
2861+
- name: step1
2862+
command: echo hello
2863+
`
2864+
ctx := context.Background()
2865+
dag, err := spec.LoadYAML(ctx, []byte(yaml))
2866+
require.NoError(t, err)
2867+
require.NotNil(t, dag.Container)
2868+
assert.Equal(t, "", dag.Container.Name)
2869+
})
2870+
2871+
t.Run("ContainerNameTrimmed", func(t *testing.T) {
2872+
yaml := `
2873+
container:
2874+
name: " my-container "
2875+
image: alpine:latest
2876+
steps:
2877+
- name: step1
2878+
command: echo hello
2879+
`
2880+
ctx := context.Background()
2881+
dag, err := spec.LoadYAML(ctx, []byte(yaml))
2882+
require.NoError(t, err)
2883+
require.NotNil(t, dag.Container)
2884+
assert.Equal(t, "my-container", dag.Container.Name)
2885+
})
2886+
28392887
t.Run("ContainerWithAllFields", func(t *testing.T) {
28402888
yaml := `
28412889
container:

internal/core/spec/definition.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ type mailOnDef struct {
216216

217217
// containerDef defines the container configuration for the DAG.
218218
type containerDef struct {
219+
// Name is the container name to use. If empty, Docker generates a random name.
220+
Name string `yaml:"name,omitempty"`
219221
// Image is the container image to use.
220222
Image string `yaml:"image,omitempty"`
221223
// PullPolicy is the policy to pull the image (e.g., "Always", "IfNotPresent").

internal/runtime/builtin/docker/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,23 @@ func (c *Client) CreateContainerKeepAlive(ctx context.Context) error {
164164
return fmt.Errorf("container already exists. id=%s", c.containerID)
165165
}
166166

167+
// Check if a container with the specified name already exists
168+
if name := c.cfg.ContainerName; name != "" {
169+
info, err := c.cli.ContainerInspect(ctx, name)
170+
if err == nil {
171+
// Container exists - fail regardless of state
172+
if info.State != nil && info.State.Running {
173+
return fmt.Errorf("container with name %q already exists and is running", name)
174+
}
175+
return fmt.Errorf("container with name %q already exists", name)
176+
}
177+
// If error is not "not found", it's an unexpected error
178+
if !errdefs.IsNotFound(err) {
179+
return fmt.Errorf("failed to check existing container %q: %w", name, err)
180+
}
181+
// Container doesn't exist, proceed to create
182+
}
183+
167184
// Choose startup mode and command
168185
var cmd []string
169186
mode := c.cfg.Startup

internal/runtime/builtin/docker/client_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,41 @@ func TestLoadConfig(t *testing.T) {
11091109
ExecOptions: &container.ExecOptions{},
11101110
},
11111111
},
1112+
{
1113+
name: "ContainerNamePropagation",
1114+
input: core.Container{
1115+
Name: "my-dag-container",
1116+
Image: "alpine",
1117+
},
1118+
expected: &Config{
1119+
ContainerName: "my-dag-container",
1120+
Image: "alpine",
1121+
AutoRemove: true,
1122+
Container: &container.Config{
1123+
Image: "alpine",
1124+
},
1125+
Host: &container.HostConfig{},
1126+
Network: &network.NetworkingConfig{},
1127+
ExecOptions: &container.ExecOptions{},
1128+
},
1129+
},
1130+
{
1131+
name: "ContainerNameEmptyWhenNotSpecified",
1132+
input: core.Container{
1133+
Image: "alpine",
1134+
},
1135+
expected: &Config{
1136+
ContainerName: "",
1137+
Image: "alpine",
1138+
AutoRemove: true,
1139+
Container: &container.Config{
1140+
Image: "alpine",
1141+
},
1142+
Host: &container.HostConfig{},
1143+
Network: &network.NetworkingConfig{},
1144+
ExecOptions: &container.ExecOptions{},
1145+
},
1146+
},
11121147
}
11131148

11141149
for _, tt := range tests {
@@ -1122,6 +1157,7 @@ func TestLoadConfig(t *testing.T) {
11221157
}
11231158

11241159
require.NoError(t, err)
1160+
assert.Equal(t, tt.expected.ContainerName, result.ContainerName)
11251161
assert.Equal(t, tt.expected.Image, result.Image)
11261162
assert.Equal(t, tt.expected.Platform, result.Platform)
11271163
assert.Equal(t, tt.expected.Pull, result.Pull)

internal/runtime/builtin/docker/config.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -224,19 +224,20 @@ func LoadConfig(workDir string, ct core.Container, registryAuths map[string]*cor
224224
}
225225

226226
return loadDefaults(&Config{
227-
Image: ct.Image,
228-
Platform: ct.Platform,
229-
Pull: ct.PullPolicy,
230-
AutoRemove: autoRemove,
231-
Container: containerConfig,
232-
Host: hostConfig,
233-
Network: networkConfig,
234-
ExecOptions: execOptions,
235-
Startup: strings.ToLower(strings.TrimSpace(string(ct.Startup))),
236-
WaitFor: strings.ToLower(strings.TrimSpace(string(ct.WaitFor))),
237-
LogPattern: ct.LogPattern,
238-
StartCmd: append([]string{}, ct.Command...),
239-
AuthManager: authManager,
227+
ContainerName: ct.Name,
228+
Image: ct.Image,
229+
Platform: ct.Platform,
230+
Pull: ct.PullPolicy,
231+
AutoRemove: autoRemove,
232+
Container: containerConfig,
233+
Host: hostConfig,
234+
Network: networkConfig,
235+
ExecOptions: execOptions,
236+
Startup: strings.ToLower(strings.TrimSpace(string(ct.Startup))),
237+
WaitFor: strings.ToLower(strings.TrimSpace(string(ct.WaitFor))),
238+
LogPattern: ct.LogPattern,
239+
StartCmd: append([]string{}, ct.Command...),
240+
AuthManager: authManager,
240241
}), nil
241242
}
242243

schemas/dag.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,10 @@
11351135
}
11361136
],
11371137
"properties": {
1138+
"name": {
1139+
"type": "string",
1140+
"description": "Custom container name. If empty, Docker generates a random name. Must be unique - if a container with this name already exists (running or stopped), the DAG will fail."
1141+
},
11381142
"image": {
11391143
"type": "string",
11401144
"description": "Container image to use (e.g., 'python:3.11', 'node:20'). Required when using container configuration."

0 commit comments

Comments
 (0)