@@ -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 )
207+ params .User , err = UserFromDockerfile (params .DockerfileContent , buildArgs )
208208 if err != nil {
209209 return nil , fmt .Errorf ("user from dockerfile: %w" , err )
210210 }
@@ -306,14 +306,75 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir
306306 return strings .Join (lines , "\n " ), featureContexts , err
307307}
308308
309+ // buildArgsWithDefaults merges external build args with ARG defaults from a Dockerfile.
310+ // External args take precedence over Dockerfile defaults.
311+ func buildArgsWithDefaults (dockerfileContent string , externalArgs []string ) ([]string , error ) {
312+ lexer := shell .NewLex ('\\' )
313+
314+ // Start with external args (these have highest precedence)
315+ result := make ([]string , len (externalArgs ))
316+ copy (result , externalArgs )
317+
318+ // Build a set of externally-provided arg names for quick lookup
319+ externalArgNames := make (map [string ]struct {})
320+ for _ , arg := range externalArgs {
321+ if parts := strings .SplitN (arg , "=" , 2 ); len (parts ) == 2 {
322+ externalArgNames [parts [0 ]] = struct {}{}
323+ }
324+ }
325+
326+ // Process ARG instructions to add default values if not overridden
327+ for _ , line := range strings .Split (dockerfileContent , "\n " ) {
328+ arg , ok := strings .CutPrefix (line , "ARG " )
329+ if ! ok {
330+ continue
331+ }
332+ arg = strings .TrimSpace (arg )
333+ if ! strings .Contains (arg , "=" ) {
334+ continue
335+ }
336+
337+ parts := strings .SplitN (arg , "=" , 2 )
338+ key , _ , err := lexer .ProcessWord (parts [0 ], shell .EnvsFromSlice (result ))
339+ if err != nil {
340+ return nil , fmt .Errorf ("processing %q: %w" , line , err )
341+ }
342+
343+ // Only use the default value if no external arg was provided
344+ if _ , exists := externalArgNames [key ]; exists {
345+ continue
346+ }
347+
348+ val , _ , err := lexer .ProcessWord (parts [1 ], shell .EnvsFromSlice (result ))
349+ if err != nil {
350+ return nil , fmt .Errorf ("processing %q: %w" , line , err )
351+ }
352+ result = append (result , key + "=" + val )
353+ }
354+
355+ return result , nil
356+ }
357+
309358// UserFromDockerfile inspects the contents of a provided Dockerfile
310359// and returns the user that will be used to run the container.
311- func UserFromDockerfile (dockerfileContent string ) (user string , err error ) {
360+ // Optionally accepts build args that may override default values in the Dockerfile.
361+ func UserFromDockerfile (dockerfileContent string , buildArgs ... []string ) (user string , err error ) {
362+ var args []string
363+ if len (buildArgs ) > 0 {
364+ args = buildArgs [0 ]
365+ }
366+
312367 res , err := parser .Parse (strings .NewReader (dockerfileContent ))
313368 if err != nil {
314369 return "" , fmt .Errorf ("parse dockerfile: %w" , err )
315370 }
316371
372+ resolvedArgs , err := buildArgsWithDefaults (dockerfileContent , args )
373+ if err != nil {
374+ return "" , err
375+ }
376+ lexer := shell .NewLex ('\\' )
377+
317378 // Parse stages and user commands to determine the relevant user
318379 // from the final stage.
319380 var (
@@ -371,10 +432,16 @@ func UserFromDockerfile(dockerfileContent string) (user string, err error) {
371432 }
372433
373434 // 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 ))
435+ // the image. First, substitute any ARG variables in the image name.
436+ imageRef := stage .BaseName
437+ imageRef , _ , err := lexer .ProcessWord (imageRef , shell .EnvsFromSlice (resolvedArgs ))
438+ if err != nil {
439+ return "" , fmt .Errorf ("processing image ref %q: %w" , stage .BaseName , err )
440+ }
441+
442+ ref , err := name .ParseReference (strings .TrimSpace (imageRef ))
376443 if err != nil {
377- return "" , fmt .Errorf ("parse image ref %q: %w" , stage . BaseName , err )
444+ return "" , fmt .Errorf ("parse image ref %q: %w" , imageRef , err )
378445 }
379446 user , err := UserFromImage (ref )
380447 if err != nil {
@@ -388,40 +455,32 @@ func UserFromDockerfile(dockerfileContent string) (user string, err error) {
388455
389456// ImageFromDockerfile inspects the contents of a provided Dockerfile
390457// and returns the image that will be used to run the container.
391- func ImageFromDockerfile ( dockerfileContent string ) (name. Reference , error ) {
392- lexer := shell . NewLex ( '\\' )
458+ // Optionally accepts build args that may override default values in the Dockerfile.
459+ func ImageFromDockerfile ( dockerfileContent string , buildArgs ... [] string ) (name. Reference , error ) {
393460 var args []string
461+ if len (buildArgs ) > 0 {
462+ args = buildArgs [0 ]
463+ }
464+
465+ resolvedArgs , err := buildArgsWithDefaults (dockerfileContent , args )
466+ if err != nil {
467+ return nil , err
468+ }
469+
470+ // Find the FROM instruction
394471 var imageRef string
395- lines := strings .Split (dockerfileContent , "\n " )
396- // Iterate over lines in reverse
397- for i := len (lines ) - 1 ; i >= 0 ; i -- {
398- line := lines [i ]
399- if arg , ok := strings .CutPrefix (line , "ARG " ); ok {
400- arg = strings .TrimSpace (arg )
401- if strings .Contains (arg , "=" ) {
402- parts := strings .SplitN (arg , "=" , 2 )
403- key , _ , err := lexer .ProcessWord (parts [0 ], shell .EnvsFromSlice (args ))
404- if err != nil {
405- return nil , fmt .Errorf ("processing %q: %w" , line , err )
406- }
407- val , _ , err := lexer .ProcessWord (parts [1 ], shell .EnvsFromSlice (args ))
408- if err != nil {
409- return nil , fmt .Errorf ("processing %q: %w" , line , err )
410- }
411- args = append (args , key + "=" + val )
412- }
413- continue
414- }
415- if imageRef == "" {
416- if fromArgs , ok := strings .CutPrefix (line , "FROM " ); ok {
417- imageRef = fromArgs
418- }
472+ for _ , line := range strings .Split (dockerfileContent , "\n " ) {
473+ if fromArgs , ok := strings .CutPrefix (line , "FROM " ); ok {
474+ imageRef = fromArgs
475+ break
419476 }
420477 }
421478 if imageRef == "" {
422479 return nil , fmt .Errorf ("no FROM directive found" )
423480 }
424- imageRef , _ , err := lexer .ProcessWord (imageRef , shell .EnvsFromSlice (args ))
481+
482+ lexer := shell .NewLex ('\\' )
483+ imageRef , _ , err = lexer .ProcessWord (imageRef , shell .EnvsFromSlice (resolvedArgs ))
425484 if err != nil {
426485 return nil , fmt .Errorf ("processing %q: %w" , imageRef , err )
427486 }
0 commit comments