Skip to content

Commit d46cd70

Browse files
feat(actions/docker): parse runArgs whitelist and splice into HostConfig
Introduces ParseRunArgs in pkg/skaffold/actions/docker/runargs.go that accepts a docker-run-style flag list using a conservative whitelist (--network, -v/--volume, -e/--env, --user, --add-host, --tmpfs, --privileged, --cap-add, --cap-drop). Unknown flags return an error so users fail fast rather than have settings silently dropped. Only the '--flag=value' form is supported to keep the parser unambiguous. Two small helpers project the parsed result onto the existing docker API types: ApplyToContainerConfig writes User and appends Env onto container.Config; ApplyToHostConfig overlays NetworkMode, Binds, ExtraHosts, Tmpfs, Privileged, CapAdd and CapDrop onto a container.HostConfig. To make the overlay available at run time, ContainerCreateOpts in pkg/skaffold/docker/image.go gains an optional HostConfigApply hook that is invoked after the default HostConfig is constructed. The Task parses the owning action's runArgs once in createTasks, stores it on the Task, and wires both helpers through the existing config paths. No runtime change occurs when runArgs is absent: the overlay methods are no-ops on a nil receiver.
1 parent 629766c commit d46cd70

4 files changed

Lines changed: 179 additions & 1 deletion

File tree

pkg/skaffold/actions/docker/exec_env.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,20 @@ func (e ExecEnv) createTasks(ctx context.Context, out io.Writer, aCfgs latest.Ac
169169
timeout := *aCfgs.Config.Timeout
170170
useLocalImages := aCfgs.ExecutionModeConfig.LocalExecutionMode.UseLocalImages
171171

172+
runArgs, err := ParseRunArgs(aCfgs.ExecutionModeConfig.LocalExecutionMode.RunArgs)
173+
if err != nil {
174+
return nil, nil, fmt.Errorf("action %q: %w", aCfgs.Name, err)
175+
}
176+
172177
for _, cCfg := range containerCfgs {
173178
art, err := e.pullArtifact(ctx, out, builts, useLocalImages, cCfg)
174179
if err != nil {
175180
return nil, nil, err
176181
}
177182

178-
ts = append(ts, NewTask(cCfg, e.client, e.portManager, e.pResources, *art, timeout, &e))
183+
task := NewTask(cCfg, e.client, e.portManager, e.pResources, *art, timeout, &e)
184+
task.runArgs = runArgs
185+
ts = append(ts, task)
179186

180187
tracked = append(tracked, graph.Artifact{
181188
ImageName: cCfg.Image,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Copyright 2026 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package docker
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"github.com/docker/docker/api/types/container"
24+
)
25+
26+
// RunArgs is the parsed, whitelisted projection of a user-supplied
27+
// customActions.*.executionMode.local.runArgs list.
28+
//
29+
// Only a small, deliberately conservative subset of `docker run` flags is
30+
// recognised. Unknown flags are rejected so users fail fast instead of
31+
// silently being ignored. See ParseRunArgs for the full list.
32+
type RunArgs struct {
33+
NetworkMode string
34+
Binds []string
35+
Env []string
36+
User string
37+
ExtraHosts []string
38+
Tmpfs map[string]string
39+
Privileged bool
40+
CapAdd []string
41+
CapDrop []string
42+
}
43+
44+
// ParseRunArgs parses a docker-run-style argument list and returns a
45+
// whitelisted RunArgs projection. Supported flags:
46+
//
47+
// --network=VALUE
48+
// -v=SRC:DST[:MODE] (also --volume=...)
49+
// -e=KEY=VALUE (also --env=...)
50+
// --user=UID[:GID]
51+
// --add-host=HOST:IP
52+
// --tmpfs=PATH[:OPTIONS]
53+
// --privileged
54+
// --cap-add=CAP
55+
// --cap-drop=CAP
56+
//
57+
// Each flag must be in the `--flag=value` (or `-f=value`) form; the
58+
// space-separated variant is not supported to keep the parser unambiguous.
59+
// A nil / empty input returns a nil *RunArgs.
60+
func ParseRunArgs(args []string) (*RunArgs, error) {
61+
if len(args) == 0 {
62+
return nil, nil
63+
}
64+
out := &RunArgs{}
65+
for i, raw := range args {
66+
arg := strings.TrimSpace(raw)
67+
if arg == "" {
68+
continue
69+
}
70+
if arg == "--privileged" {
71+
out.Privileged = true
72+
continue
73+
}
74+
key, val, ok := strings.Cut(arg, "=")
75+
if !ok {
76+
return nil, fmt.Errorf("runArgs[%d] %q: only --flag=value form is supported (no space-separated values)", i, raw)
77+
}
78+
switch key {
79+
case "--network":
80+
out.NetworkMode = val
81+
case "-v", "--volume":
82+
out.Binds = append(out.Binds, val)
83+
case "-e", "--env":
84+
out.Env = append(out.Env, val)
85+
case "--user":
86+
out.User = val
87+
case "--add-host":
88+
out.ExtraHosts = append(out.ExtraHosts, val)
89+
case "--tmpfs":
90+
if out.Tmpfs == nil {
91+
out.Tmpfs = map[string]string{}
92+
}
93+
mountPath, opts, _ := strings.Cut(val, ":")
94+
out.Tmpfs[mountPath] = opts
95+
case "--cap-add":
96+
out.CapAdd = append(out.CapAdd, val)
97+
case "--cap-drop":
98+
out.CapDrop = append(out.CapDrop, val)
99+
default:
100+
return nil, fmt.Errorf("runArgs[%d] %q: unsupported flag %q (allowed: --network, -v/--volume, -e/--env, --user, --add-host, --tmpfs, --privileged, --cap-add, --cap-drop)", i, raw, key)
101+
}
102+
}
103+
return out, nil
104+
}
105+
106+
// ApplyToContainerConfig overlays parsed runArgs fields that belong on the
107+
// container.Config (User, additional env vars) onto cfg in place. A nil
108+
// receiver is a no-op.
109+
func (r *RunArgs) ApplyToContainerConfig(cfg *container.Config) {
110+
if r == nil || cfg == nil {
111+
return
112+
}
113+
if r.User != "" {
114+
cfg.User = r.User
115+
}
116+
if len(r.Env) > 0 {
117+
// RunArgs env entries win over anything already on the container
118+
// config — consistent with deploy-parameter precedence.
119+
cfg.Env = append(cfg.Env, r.Env...)
120+
}
121+
}
122+
123+
// ApplyToHostConfig overlays parsed runArgs fields that belong on the
124+
// container.HostConfig onto hc in place. NetworkMode is only overridden
125+
// when the user provided one. A nil receiver is a no-op.
126+
func (r *RunArgs) ApplyToHostConfig(hc *container.HostConfig) {
127+
if r == nil || hc == nil {
128+
return
129+
}
130+
if r.NetworkMode != "" {
131+
hc.NetworkMode = container.NetworkMode(r.NetworkMode)
132+
}
133+
if len(r.Binds) > 0 {
134+
hc.Binds = append(hc.Binds, r.Binds...)
135+
}
136+
if len(r.ExtraHosts) > 0 {
137+
hc.ExtraHosts = append(hc.ExtraHosts, r.ExtraHosts...)
138+
}
139+
if len(r.Tmpfs) > 0 {
140+
if hc.Tmpfs == nil {
141+
hc.Tmpfs = map[string]string{}
142+
}
143+
for k, v := range r.Tmpfs {
144+
hc.Tmpfs[k] = v
145+
}
146+
}
147+
if r.Privileged {
148+
hc.Privileged = true
149+
}
150+
if len(r.CapAdd) > 0 {
151+
hc.CapAdd = append(hc.CapAdd, r.CapAdd...)
152+
}
153+
if len(r.CapDrop) > 0 {
154+
hc.CapDrop = append(hc.CapDrop, r.CapDrop...)
155+
}
156+
}

pkg/skaffold/actions/docker/task.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ type Task struct {
6464

6565
// Reference to the associated execution environment.
6666
execEnv *ExecEnv
67+
68+
// Optional whitelisted docker-run-style flags parsed from the owning
69+
// action's executionMode.local.runArgs. Nil when the user didn't
70+
// provide any.
71+
runArgs *RunArgs
6772
}
6873

6974
var NewTask = newTask
@@ -153,6 +158,7 @@ func (t Task) containerCreateOpts(ctx context.Context, containerName string) (*d
153158
ContainerConfig: containerCfg,
154159
Bindings: bindings,
155160
Wait: true,
161+
HostConfigApply: t.runArgs.ApplyToHostConfig,
156162
}, nil
157163
}
158164

@@ -177,6 +183,8 @@ func (t Task) generateContainerCfg(ctx context.Context) (*container.Config, erro
177183

178184
containerCfg.Env = append(envVars, t.envVars...)
179185

186+
t.runArgs.ApplyToContainerConfig(containerCfg)
187+
180188
return containerCfg, nil
181189
}
182190

pkg/skaffold/docker/image.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ type ContainerCreateOpts struct {
7676
ContainerConfig *container.Config
7777
VerifyTestName string
7878
Labels map[string]string
79+
// HostConfigApply, if non-nil, is invoked after the default HostConfig
80+
// has been constructed in Run and lets callers overlay additional
81+
// fields (e.g. Binds, ExtraHosts, NetworkMode overrides).
82+
HostConfigApply func(*container.HostConfig)
7983
}
8084

8185
// LocalDaemon talks to a local Docker API.
@@ -231,6 +235,9 @@ func (l *localDaemon) Run(ctx context.Context, out io.Writer, opts ContainerCrea
231235
PortBindings: opts.Bindings,
232236
Mounts: opts.Mounts,
233237
}
238+
if opts.HostConfigApply != nil {
239+
opts.HostConfigApply(hCfg)
240+
}
234241

235242
c, err := l.apiClient.ContainerCreate(ctx, opts.ContainerConfig, hCfg, nil, nil, opts.Name)
236243
if err != nil {

0 commit comments

Comments
 (0)