@@ -204,7 +204,7 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string,
204
204
// We should make a best-effort attempt to find the user.
205
205
// Features must be executed as root, so we need to swap back
206
206
// to the running user afterwards.
207
- params .User , err = UserFromDockerfile (params .DockerfileContent )
207
+ params .User , err = UserFromDockerfile (params .DockerfileContent , buildArgs )
208
208
if err != nil {
209
209
return nil , fmt .Errorf ("user from dockerfile: %w" , err )
210
210
}
@@ -308,12 +308,57 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir
308
308
309
309
// UserFromDockerfile inspects the contents of a provided Dockerfile
310
310
// and returns the user that will be used to run the container.
311
- func UserFromDockerfile (dockerfileContent string ) (user string , err error ) {
311
+ // Optionally accepts build args that may override default values in the Dockerfile.
312
+ func UserFromDockerfile (dockerfileContent string , buildArgs ... []string ) (user string , err error ) {
313
+ var args []string
314
+ if len (buildArgs ) > 0 {
315
+ args = buildArgs [0 ]
316
+ }
317
+
312
318
res , err := parser .Parse (strings .NewReader (dockerfileContent ))
313
319
if err != nil {
314
320
return "" , fmt .Errorf ("parse dockerfile: %w" , err )
315
321
}
316
322
323
+ // Parse build args and ARG instructions to build the substitution context
324
+ lexer := shell .NewLex ('\\' )
325
+
326
+ // Start with build args provided externally (e.g., from devcontainer.json)
327
+ argsCopy := make ([]string , len (args ))
328
+ copy (argsCopy , args )
329
+
330
+ // Parse build args into a map for easy lookup
331
+ buildArgsMap := make (map [string ]string )
332
+ for _ , arg := range args {
333
+ if parts := strings .SplitN (arg , "=" , 2 ); len (parts ) == 2 {
334
+ buildArgsMap [parts [0 ]] = parts [1 ]
335
+ }
336
+ }
337
+
338
+ // Process ARG instructions to add default values if not overridden
339
+ lines := strings .Split (dockerfileContent , "\n " )
340
+ for _ , line := range lines {
341
+ if arg , ok := strings .CutPrefix (line , "ARG " ); ok {
342
+ arg = strings .TrimSpace (arg )
343
+ if strings .Contains (arg , "=" ) {
344
+ parts := strings .SplitN (arg , "=" , 2 )
345
+ key , _ , err := lexer .ProcessWord (parts [0 ], shell .EnvsFromSlice (argsCopy ))
346
+ if err != nil {
347
+ return "" , fmt .Errorf ("processing %q: %w" , line , err )
348
+ }
349
+
350
+ // Only use the default value if no build arg was provided
351
+ if _ , exists := buildArgsMap [key ]; ! exists {
352
+ val , _ , err := lexer .ProcessWord (parts [1 ], shell .EnvsFromSlice (argsCopy ))
353
+ if err != nil {
354
+ return "" , fmt .Errorf ("processing %q: %w" , line , err )
355
+ }
356
+ argsCopy = append (argsCopy , key + "=" + val )
357
+ }
358
+ }
359
+ }
360
+ }
361
+
317
362
// Parse stages and user commands to determine the relevant user
318
363
// from the final stage.
319
364
var (
@@ -371,10 +416,16 @@ func UserFromDockerfile(dockerfileContent string) (user string, err error) {
371
416
}
372
417
373
418
// If we can't find a user command, try to find the user from
374
- // the image.
375
- ref , err := name .ParseReference (strings .TrimSpace (stage .BaseName ))
419
+ // the image. First, substitute any ARG variables in the image name.
420
+ imageRef := stage .BaseName
421
+ imageRef , _ , err := lexer .ProcessWord (imageRef , shell .EnvsFromSlice (argsCopy ))
376
422
if err != nil {
377
- return "" , fmt .Errorf ("parse image ref %q: %w" , stage .BaseName , err )
423
+ return "" , fmt .Errorf ("processing image ref %q: %w" , stage .BaseName , err )
424
+ }
425
+
426
+ ref , err := name .ParseReference (strings .TrimSpace (imageRef ))
427
+ if err != nil {
428
+ return "" , fmt .Errorf ("parse image ref %q: %w" , imageRef , err )
378
429
}
379
430
user , err := UserFromImage (ref )
380
431
if err != nil {
@@ -388,27 +439,50 @@ func UserFromDockerfile(dockerfileContent string) (user string, err error) {
388
439
389
440
// ImageFromDockerfile inspects the contents of a provided Dockerfile
390
441
// and returns the image that will be used to run the container.
391
- func ImageFromDockerfile ( dockerfileContent string ) (name. Reference , error ) {
392
- lexer := shell . NewLex ( '\\' )
442
+ // Optionally accepts build args that may override default values in the Dockerfile.
443
+ func ImageFromDockerfile ( dockerfileContent string , buildArgs ... [] string ) (name. Reference , error ) {
393
444
var args []string
445
+ if len (buildArgs ) > 0 {
446
+ args = buildArgs [0 ]
447
+ }
448
+
449
+ lexer := shell .NewLex ('\\' )
450
+
451
+ // Start with build args provided externally (e.g., from devcontainer.json)
452
+ // These have higher precedence than default values in ARG instructions
453
+ argsCopy := make ([]string , len (args ))
454
+ copy (argsCopy , args )
455
+
456
+ // Parse build args into a map for easy lookup
457
+ buildArgsMap := make (map [string ]string )
458
+ for _ , arg := range args {
459
+ if parts := strings .SplitN (arg , "=" , 2 ); len (parts ) == 2 {
460
+ buildArgsMap [parts [0 ]] = parts [1 ]
461
+ }
462
+ }
463
+
394
464
var imageRef string
395
465
lines := strings .Split (dockerfileContent , "\n " )
396
- // Iterate over lines in reverse
466
+ // Iterate over lines in reverse to find ARG declarations and FROM instruction
397
467
for i := len (lines ) - 1 ; i >= 0 ; i -- {
398
468
line := lines [i ]
399
469
if arg , ok := strings .CutPrefix (line , "ARG " ); ok {
400
470
arg = strings .TrimSpace (arg )
401
471
if strings .Contains (arg , "=" ) {
402
472
parts := strings .SplitN (arg , "=" , 2 )
403
- key , _ , err := lexer .ProcessWord (parts [0 ], shell .EnvsFromSlice (args ))
473
+ key , _ , err := lexer .ProcessWord (parts [0 ], shell .EnvsFromSlice (argsCopy ))
404
474
if err != nil {
405
475
return nil , fmt .Errorf ("processing %q: %w" , line , err )
406
476
}
407
- val , _ , err := lexer .ProcessWord (parts [1 ], shell .EnvsFromSlice (args ))
408
- if err != nil {
409
- return nil , fmt .Errorf ("processing %q: %w" , line , err )
477
+
478
+ // Only use the default value if no build arg was provided
479
+ if _ , exists := buildArgsMap [key ]; ! exists {
480
+ val , _ , err := lexer .ProcessWord (parts [1 ], shell .EnvsFromSlice (argsCopy ))
481
+ if err != nil {
482
+ return nil , fmt .Errorf ("processing %q: %w" , line , err )
483
+ }
484
+ argsCopy = append (argsCopy , key + "=" + val )
410
485
}
411
- args = append (args , key + "=" + val )
412
486
}
413
487
continue
414
488
}
@@ -421,7 +495,7 @@ func ImageFromDockerfile(dockerfileContent string) (name.Reference, error) {
421
495
if imageRef == "" {
422
496
return nil , fmt .Errorf ("no FROM directive found" )
423
497
}
424
- imageRef , _ , err := lexer .ProcessWord (imageRef , shell .EnvsFromSlice (args ))
498
+ imageRef , _ , err := lexer .ProcessWord (imageRef , shell .EnvsFromSlice (argsCopy ))
425
499
if err != nil {
426
500
return nil , fmt .Errorf ("processing %q: %w" , imageRef , err )
427
501
}
0 commit comments