@@ -17,14 +17,20 @@ namespace DotNet.Testcontainers.Images
1717 /// </summary>
1818 internal sealed class DockerfileArchive : ITarArchive
1919 {
20- private static readonly Regex FromLinePattern = new Regex ( "FROM (?<arg>--\\ S+\\ s)*(?<image>\\ S+).*" , RegexOptions . None , TimeSpan . FromSeconds ( 1 ) ) ;
20+ private static readonly Regex ArgLinePattern = new Regex ( "^ARG\\ s+(?<name>[A-Za-z_][A-Za-z0-9_]*)=(?:\" (?<value>[^\" ]*)\" |'(?<value>[^']*)'|(?<value>\\ S+))" , RegexOptions . IgnoreCase , TimeSpan . FromSeconds ( 1 ) ) ;
21+
22+ private static readonly Regex FromLinePattern = new Regex ( "^FROM\\ s+(?<arg>--\\ S+\\ s)*(?<image>\\ S+).*" , RegexOptions . IgnoreCase , TimeSpan . FromSeconds ( 1 ) ) ;
23+
24+ private static readonly Regex VariablePattern = new Regex ( "\\ $(\\ {(?<name>[A-Za-z_][A-Za-z0-9_]*)\\ }|(?<name>[A-Za-z_][A-Za-z0-9_]*))" , RegexOptions . None , TimeSpan . FromSeconds ( 1 ) ) ;
2125
2226 private readonly DirectoryInfo _dockerfileDirectory ;
2327
2428 private readonly FileInfo _dockerfile ;
2529
2630 private readonly IImage _image ;
2731
32+ private readonly IReadOnlyDictionary < string , string > _buildArguments ;
33+
2834 private readonly ILogger _logger ;
2935
3036 /// <summary>
@@ -33,10 +39,21 @@ internal sealed class DockerfileArchive : ITarArchive
3339 /// <param name="dockerfileDirectory">Directory to Docker configuration files.</param>
3440 /// <param name="dockerfile">Name of the Dockerfile, which is necessary to start the Docker build.</param>
3541 /// <param name="image">Docker image information to create the tar archive for.</param>
42+ /// <param name="buildArguments">Docker build arguments.</param>
3643 /// <param name="logger">The logger.</param>
3744 /// <exception cref="ArgumentException">Thrown when the Dockerfile directory does not exist or the directory does not contain a Dockerfile.</exception>
38- public DockerfileArchive ( string dockerfileDirectory , string dockerfile , IImage image , ILogger logger )
39- : this ( new DirectoryInfo ( dockerfileDirectory ) , new FileInfo ( dockerfile ) , image , logger )
45+ public DockerfileArchive (
46+ string dockerfileDirectory ,
47+ string dockerfile ,
48+ IImage image ,
49+ IReadOnlyDictionary < string , string > buildArguments ,
50+ ILogger logger )
51+ : this (
52+ new DirectoryInfo ( dockerfileDirectory ) ,
53+ new FileInfo ( dockerfile ) ,
54+ image ,
55+ buildArguments ,
56+ logger )
4057 {
4158 }
4259
@@ -46,9 +63,15 @@ public DockerfileArchive(string dockerfileDirectory, string dockerfile, IImage i
4663 /// <param name="dockerfileDirectory">Directory to Docker configuration files.</param>
4764 /// <param name="dockerfile">Name of the Dockerfile, which is necessary to start the Docker build.</param>
4865 /// <param name="image">Docker image information to create the tar archive for.</param>
66+ /// <param name="buildArguments">Docker build arguments.</param>
4967 /// <param name="logger">The logger.</param>
5068 /// <exception cref="ArgumentException">Thrown when the Dockerfile directory does not exist or the directory does not contain a Dockerfile.</exception>
51- public DockerfileArchive ( DirectoryInfo dockerfileDirectory , FileInfo dockerfile , IImage image , ILogger logger )
69+ public DockerfileArchive (
70+ DirectoryInfo dockerfileDirectory ,
71+ FileInfo dockerfile ,
72+ IImage image ,
73+ IReadOnlyDictionary < string , string > buildArguments ,
74+ ILogger logger )
5275 {
5376 if ( ! dockerfileDirectory . Exists )
5477 {
@@ -63,6 +86,7 @@ public DockerfileArchive(DirectoryInfo dockerfileDirectory, FileInfo dockerfile,
6386 _dockerfileDirectory = dockerfileDirectory ;
6487 _dockerfile = dockerfile ;
6588 _image = image ;
89+ _buildArguments = buildArguments ;
6690 _logger = logger ;
6791 }
6892
@@ -83,28 +107,44 @@ public IEnumerable<IImage> GetBaseImages()
83107 {
84108 const string imageGroup = "image" ;
85109
110+ const string nameGroup = "name" ;
111+
112+ const string valueGroup = "value" ;
113+
86114 var lines = File . ReadAllLines ( Path . Combine ( _dockerfileDirectory . FullName , _dockerfile . ToString ( ) ) )
87115 . Select ( line => line . Trim ( ) )
88116 . Where ( line => ! string . IsNullOrEmpty ( line ) )
89117 . Where ( line => ! line . StartsWith ( "#" , StringComparison . Ordinal ) )
118+ . ToArray ( ) ;
119+
120+ var argMatches = lines
121+ . Select ( line => ArgLinePattern . Match ( line ) )
122+ . Where ( match => match . Success )
123+ . ToArray ( ) ;
124+
125+ var fromMatches = lines
90126 . Select ( line => FromLinePattern . Match ( line ) )
91127 . Where ( match => match . Success )
92128 . ToArray ( ) ;
93129
94- var stages = lines
130+ var args = argMatches
131+ . Select ( match => new KeyValuePair < string , string > ( match . Groups [ nameGroup ] . Value , match . Groups [ valueGroup ] . Value ) )
132+ . Concat ( _buildArguments )
133+ . GroupBy ( kvp => kvp . Key )
134+ . ToDictionary ( group => group . Key , group => group . Last ( ) . Value ) ;
135+
136+ var stages = fromMatches
95137 . Select ( line => line . Value )
96138 . Select ( line => line . Split ( new [ ] { " AS " , " As " , " aS " , " as " } , StringSplitOptions . RemoveEmptyEntries ) )
97139 . Where ( substrings => substrings . Length > 1 )
98140 . Select ( substrings => substrings [ substrings . Length - 1 ] )
99141 . Distinct ( )
100142 . ToArray ( ) ;
101143
102- var images = lines
144+ var images = fromMatches
103145 . Select ( match => match . Groups [ imageGroup ] )
104146 . Select ( match => match . Value )
105- // Until now, we are unable to resolve variables within Dockerfiles. Ignore base
106- // images that utilize variables. Expect them to exist on the host.
107- . Where ( line => ! line . Contains ( '$' ) )
147+ . Select ( line => ReplaceVariables ( line , args ) )
108148 . Where ( line => ! line . Any ( char . IsUpper ) )
109149 . Where ( value => ! stages . Contains ( value ) )
110150 . Distinct ( )
@@ -205,5 +245,30 @@ private static int GetUnixFileMode(string filePath)
205245 _ = filePath ;
206246 return ( int ) Unix . FileMode755 ;
207247 }
248+
249+ /// <summary>
250+ /// Replaces placeholders in the Dockerfile <c>FROM</c> image string with the values
251+ /// provided in <paramref name="variables" />. Each placeholder is replaced with the
252+ /// corresponding build argument if present; otherwise, the default value in the
253+ /// Dockerfile is preserved.
254+ /// </summary>
255+ /// <param name="image">The image string from a Dockerfile <c>FROM</c> statement.</param>
256+ /// <param name="variables">A dictionary containing variable names as keys and their replacement values as values.</param>
257+ /// <returns>A new image string where placeholders are replaced with their corresponding values.</returns>
258+ private static string ReplaceVariables ( string image , IDictionary < string , string > variables )
259+ {
260+ const string nameGroup = "name" ;
261+
262+ if ( variables . Count == 0 )
263+ {
264+ return image ;
265+ }
266+
267+ return VariablePattern . Replace ( image , match =>
268+ {
269+ var name = match . Groups [ nameGroup ] . Value ;
270+ return variables . TryGetValue ( name , out var value ) ? value : match . Value ;
271+ } ) ;
272+ }
208273 }
209274}
0 commit comments