Skip to content

Commit bbbbf98

Browse files
committed
refactor: address PR review comments
- Change buildArgs parameter from []string to map[string]string - Add exported BuildArgsMap helper for call-site conversion - Use child.Next.Value instead of fragile child.Original slicing - Use strings.Cut instead of strings.Contains + SplitN - Return ProcessWord errors instead of silently swallowing them - Add UserFromDockerfile_BuildArgs test with registry - Add MissingBuildArgUsesEmpty test case
1 parent 20e3937 commit bbbbf98

3 files changed

Lines changed: 75 additions & 43 deletions

File tree

devcontainer/devcontainer.go

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string,
204204
// We should make a best-effort attempt to find the user.
205205
// Features must be executed as root, so we need to swap back
206206
// to the running user afterwards.
207-
params.User, err = UserFromDockerfile(params.DockerfileContent, params.BuildArgs)
207+
params.User, err = UserFromDockerfile(params.DockerfileContent, BuildArgsMap(params.BuildArgs))
208208
if err != nil {
209209
return nil, fmt.Errorf("user from dockerfile: %w", err)
210210
}
@@ -306,9 +306,20 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir
306306
return strings.Join(lines, "\n"), featureContexts, err
307307
}
308308

309+
// BuildArgsMap converts a slice of "KEY=VALUE" strings to a map.
310+
func BuildArgsMap(buildArgs []string) map[string]string {
311+
m := make(map[string]string, len(buildArgs))
312+
for _, arg := range buildArgs {
313+
if key, val, ok := strings.Cut(arg, "="); ok {
314+
m[key] = val
315+
}
316+
}
317+
return m
318+
}
319+
309320
// UserFromDockerfile inspects the contents of a provided Dockerfile
310321
// and returns the user that will be used to run the container.
311-
func UserFromDockerfile(dockerfileContent string, buildArgs []string) (user string, err error) {
322+
func UserFromDockerfile(dockerfileContent string, buildArgs map[string]string) (user string, err error) {
312323
res, err := parser.Parse(strings.NewReader(dockerfileContent))
313324
if err != nil {
314325
return "", fmt.Errorf("parse dockerfile: %w", err)
@@ -319,27 +330,18 @@ func UserFromDockerfile(dockerfileContent string, buildArgs []string) (user stri
319330
lexer := shell.NewLex('\\')
320331
var argEnvs []string
321332
for _, child := range res.AST.Children {
322-
if strings.EqualFold(child.Value, "arg") {
323-
arg := strings.TrimSpace(child.Original[len("ARG "):])
324-
if strings.Contains(arg, "=") {
325-
parts := strings.SplitN(arg, "=", 2)
326-
key := parts[0]
327-
val := parts[1]
328-
// Allow buildArgs to override.
329-
for _, ba := range buildArgs {
330-
if k, v, ok := strings.Cut(ba, "="); ok && k == key {
331-
val = v
332-
break
333-
}
334-
}
335-
argEnvs = append(argEnvs, key+"="+val)
336-
} else {
337-
for _, ba := range buildArgs {
338-
if k, v, ok := strings.Cut(ba, "="); ok && k == arg {
339-
argEnvs = append(argEnvs, k+"="+v)
340-
break
341-
}
342-
}
333+
if !strings.EqualFold(child.Value, "arg") || child.Next == nil {
334+
continue
335+
}
336+
if key, val, ok := strings.Cut(child.Next.Value, "="); ok {
337+
if override, has := buildArgs[key]; has {
338+
val = override
339+
}
340+
argEnvs = append(argEnvs, key+"="+val)
341+
} else {
342+
arg := child.Next.Value
343+
if val, has := buildArgs[arg]; has {
344+
argEnvs = append(argEnvs, arg+"="+val)
343345
}
344346
}
345347
}
@@ -362,9 +364,10 @@ func UserFromDockerfile(dockerfileContent string, buildArgs []string) (user stri
362364
case *instructions.Stage:
363365
// Substitute ARG values in the base image name.
364366
baseName, _, err := lexer.ProcessWord(i.BaseName, shell.EnvsFromSlice(argEnvs))
365-
if err == nil {
366-
i.BaseName = baseName
367+
if err != nil {
368+
return "", fmt.Errorf("processing ARG substitution in FROM %q: %w", i.BaseName, err)
367369
}
370+
i.BaseName = baseName
368371
stages = append(stages, i)
369372
if i.Name != "" {
370373
stageNames[i.Name] = i
@@ -423,7 +426,7 @@ func UserFromDockerfile(dockerfileContent string, buildArgs []string) (user stri
423426

424427
// ImageFromDockerfile inspects the contents of a provided Dockerfile
425428
// and returns the image that will be used to run the container.
426-
func ImageFromDockerfile(dockerfileContent string, buildArgs []string) (name.Reference, error) {
429+
func ImageFromDockerfile(dockerfileContent string, buildArgs map[string]string) (name.Reference, error) {
427430
lexer := shell.NewLex('\\')
428431
var args []string
429432
var imageRef string
@@ -433,31 +436,24 @@ func ImageFromDockerfile(dockerfileContent string, buildArgs []string) (name.Ref
433436
line := lines[i]
434437
if arg, ok := strings.CutPrefix(line, "ARG "); ok {
435438
arg = strings.TrimSpace(arg)
436-
if strings.Contains(arg, "=") {
437-
parts := strings.SplitN(arg, "=", 2)
438-
key, _, err := lexer.ProcessWord(parts[0], shell.EnvsFromSlice(args))
439+
if key, val, ok := strings.Cut(arg, "="); ok {
440+
key, _, err := lexer.ProcessWord(key, shell.EnvsFromSlice(args))
439441
if err != nil {
440442
return nil, fmt.Errorf("processing %q: %w", line, err)
441443
}
442-
val, _, err := lexer.ProcessWord(parts[1], shell.EnvsFromSlice(args))
444+
val, _, err := lexer.ProcessWord(val, shell.EnvsFromSlice(args))
443445
if err != nil {
444446
return nil, fmt.Errorf("processing %q: %w", line, err)
445447
}
446448
// Allow buildArgs to override Dockerfile ARG defaults.
447-
for _, ba := range buildArgs {
448-
if k, v, ok := strings.Cut(ba, "="); ok && k == key {
449-
val = v
450-
break
451-
}
449+
if override, has := buildArgs[key]; has {
450+
val = override
452451
}
453452
args = append(args, key+"="+val)
454453
} else {
455454
// ARG without a default — look up in buildArgs.
456-
for _, ba := range buildArgs {
457-
if k, v, ok := strings.Cut(ba, "="); ok && k == arg {
458-
args = append(args, k+"="+v)
459-
break
460-
}
455+
if val, has := buildArgs[arg]; has {
456+
args = append(args, arg+"="+val)
461457
}
462458
}
463459
continue

devcontainer/devcontainer_test.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func TestImageFromDockerfile_BuildArgs(t *testing.T) {
227227
t.Run("OverridesDefault", func(t *testing.T) {
228228
t.Parallel()
229229
content := "ARG VARIANT=3.10\nFROM mcr.microsoft.com/devcontainers/python:0-${VARIANT}"
230-
ref, err := devcontainer.ImageFromDockerfile(content, []string{"VARIANT=3.11-bookworm"})
230+
ref, err := devcontainer.ImageFromDockerfile(content, map[string]string{"VARIANT": "3.11-bookworm"})
231231
require.NoError(t, err)
232232
require.Equal(t, "mcr.microsoft.com/devcontainers/python:0-3.11-bookworm", ref.Name())
233233
})
@@ -236,10 +236,46 @@ func TestImageFromDockerfile_BuildArgs(t *testing.T) {
236236
t.Run("SuppliesArgWithoutDefault", func(t *testing.T) {
237237
t.Parallel()
238238
content := "ARG VARIANT\nFROM mcr.microsoft.com/devcontainers/python:1-${VARIANT}"
239-
ref, err := devcontainer.ImageFromDockerfile(content, []string{"VARIANT=3.11-bookworm"})
239+
ref, err := devcontainer.ImageFromDockerfile(content, map[string]string{"VARIANT": "3.11-bookworm"})
240240
require.NoError(t, err)
241241
require.Equal(t, "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm", ref.Name())
242242
})
243+
244+
// Test that a missing build arg for an ARG without default results
245+
// in the variable being substituted as empty string.
246+
t.Run("MissingBuildArgUsesEmpty", func(t *testing.T) {
247+
t.Parallel()
248+
content := "ARG VARIANT\nFROM mcr.microsoft.com/devcontainers/python:1-${VARIANT}"
249+
ref, err := devcontainer.ImageFromDockerfile(content, nil)
250+
require.NoError(t, err)
251+
require.Equal(t, "mcr.microsoft.com/devcontainers/python:1-", ref.Name())
252+
})
253+
}
254+
255+
func TestUserFromDockerfile_BuildArgs(t *testing.T) {
256+
t.Parallel()
257+
258+
t.Run("SubstitutesARGInFROM", func(t *testing.T) {
259+
t.Parallel()
260+
registry := registrytest.New(t)
261+
image, err := partial.UncompressedToImage(emptyImage{configFile: &v1.ConfigFile{
262+
Config: v1.Config{
263+
User: "testuser",
264+
},
265+
}})
266+
require.NoError(t, err)
267+
ref := strings.TrimPrefix(registry, "http://") + "/coder/test:latest"
268+
parsed, err := name.ParseReference(ref)
269+
require.NoError(t, err)
270+
err = remote.Write(parsed, image)
271+
require.NoError(t, err)
272+
273+
// Dockerfile uses ARG without default for the image ref.
274+
content := fmt.Sprintf("ARG TAG\nFROM %s/coder/test:${TAG}", strings.TrimPrefix(registry, "http://"))
275+
user, err := devcontainer.UserFromDockerfile(content, map[string]string{"TAG": "latest"})
276+
require.NoError(t, err)
277+
require.Equal(t, "testuser", user)
278+
})
243279
}
244280

245281
func TestUserFrom(t *testing.T) {

envbuilder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
493493
defer cleanupBuildContext()
494494
if runtimeData.Built && opts.SkipRebuild {
495495
endStage := startStage("🏗️ Skipping build because of cache...")
496-
imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent, buildParams.BuildArgs)
496+
imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent, devcontainer.BuildArgsMap(buildParams.BuildArgs))
497497
if err != nil {
498498
return nil, fmt.Errorf("image from dockerfile: %w", err)
499499
}

0 commit comments

Comments
 (0)