Skip to content

Commit f6400da

Browse files
authored
Merge branch 'develop' into ssl-config-dotnet
2 parents 955edb3 + 874a006 commit f6400da

File tree

13 files changed

+186
-30
lines changed

13 files changed

+186
-30
lines changed

src/Testcontainers.Keycloak/KeycloakBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public override KeycloakContainer Build()
8989
var isMajorVersionGreaterOrEqual25 = image.MatchLatestOrNightly() || image.MatchVersion(predicate);
9090

9191
var waitStrategy = Wait.ForUnixContainer()
92+
.UntilMessageIsLogged("Added user '[^']+' to realm '[^']+'|Created temporary admin user with username \\S+")
9293
.UntilHttpRequestIsSucceeded(request =>
9394
request.ForPath("/health/ready").ForPort(isMajorVersionGreaterOrEqual25 ? KeycloakHealthPort : KeycloakPort));
9495

src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ public interface IImageFromDockerfileBuilder<out TBuilderEntity>
5353
[PublicAPI]
5454
TBuilderEntity WithDockerfileDirectory(CommonDirectoryPath commonDirectoryPath, string dockerfileDirectory);
5555

56+
/// <summary>
57+
/// Sets the target build stage for the Docker image, allowing partial builds for
58+
/// multi-stage Dockerfiles.
59+
/// </summary>
60+
/// <param name="target">The target build stage to use for the image build.</param>
61+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
62+
[PublicAPI]
63+
TBuilderEntity WithTarget(string target);
64+
5665
/// <summary>
5766
/// Sets the image build policy.
5867
/// </summary>

src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ public ImageFromDockerfileBuilder WithDockerfileDirectory(CommonDirectoryPath co
8383
return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(dockerfileDirectory: dockerfileDirectoryPath));
8484
}
8585

86+
/// <inheritdoc />
87+
public ImageFromDockerfileBuilder WithTarget(string target)
88+
{
89+
return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(target: target));
90+
}
91+
8692
/// <inheritdoc />
8793
public ImageFromDockerfileBuilder WithImageBuildPolicy(Func<ImageInspectResponse, bool> imageBuildPolicy)
8894
{

src/Testcontainers/Clients/DockerImageOperations.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ await DeleteAsync(image, ct)
9898
var buildParameters = new ImageBuildParameters
9999
{
100100
Dockerfile = configuration.Dockerfile,
101+
Target = configuration.Target,
101102
Tags = new List<string> { image.FullName },
102103
BuildArgs = configuration.BuildArguments.ToDictionary(item => item.Key, item => item.Value),
103104
Labels = configuration.Labels.ToDictionary(item => item.Key, item => item.Value),

src/Testcontainers/Clients/TestcontainersClient.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,12 @@ public async Task<string> BuildAsync(IImageFromDockerfileConfiguration configura
371371

372372
if (configuration.ImageBuildPolicy(cachedImage))
373373
{
374-
var dockerfileArchive = new DockerfileArchive(configuration.DockerfileDirectory, configuration.Dockerfile, configuration.Image, _logger);
374+
var dockerfileArchive = new DockerfileArchive(
375+
configuration.DockerfileDirectory,
376+
configuration.Dockerfile,
377+
configuration.Image,
378+
configuration.BuildArguments,
379+
_logger);
375380

376381
var baseImages = dockerfileArchive.GetBaseImages().ToArray();
377382

src/Testcontainers/Clients/TraceProgress.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ public void Report(JSONMessage value)
1919

2020
if (!string.IsNullOrWhiteSpace(value.Status))
2121
{
22-
_logger.LogDebug(value.Status);
22+
_logger.LogDebug(value.Status.TrimEnd());
2323
}
2424

2525
if (!string.IsNullOrWhiteSpace(value.Stream))
2626
{
27-
_logger.LogDebug(value.Stream);
27+
_logger.LogDebug(value.Stream.TrimEnd());
2828
}
2929

3030
if (!string.IsNullOrWhiteSpace(value.ProgressMessage))
3131
{
32-
_logger.LogDebug(value.ProgressMessage);
32+
_logger.LogDebug(value.ProgressMessage.TrimEnd());
3333
}
3434

3535
if (!string.IsNullOrWhiteSpace(value.ErrorMessage))
3636
{
37-
_logger.LogError(value.ErrorMessage);
37+
_logger.LogError(value.ErrorMessage.TrimEnd());
3838
}
3939

4040
#pragma warning restore CA1848, CA2254

src/Testcontainers/Configurations/Images/IImageFromDockerfileConfiguration.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public interface IImageFromDockerfileConfiguration : IResourceConfiguration<Imag
2727
/// </summary>
2828
string DockerfileDirectory { get; }
2929

30+
/// <summary>
31+
/// Gets the target.
32+
/// </summary>
33+
string Target { get; }
34+
3035
/// <summary>
3136
/// Gets the image.
3237
/// </summary>

src/Testcontainers/Configurations/Images/ImageFromDockerfileConfiguration.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,23 @@ internal sealed class ImageFromDockerfileConfiguration : ResourceConfiguration<I
1717
/// </summary>
1818
/// <param name="dockerfile">The Dockerfile.</param>
1919
/// <param name="dockerfileDirectory">The Dockerfile directory.</param>
20+
/// <param name="target">The target.</param>
2021
/// <param name="image">The image.</param>
2122
/// <param name="imageBuildPolicy">The image build policy.</param>
2223
/// <param name="buildArguments">A list of build arguments.</param>
2324
/// <param name="deleteIfExists">A value indicating whether Testcontainers removes an existing image or not.</param>
2425
public ImageFromDockerfileConfiguration(
2526
string dockerfile = null,
2627
string dockerfileDirectory = null,
28+
string target = null,
2729
IImage image = null,
2830
Func<ImageInspectResponse, bool> imageBuildPolicy = null,
2931
IReadOnlyDictionary<string, string> buildArguments = null,
3032
bool? deleteIfExists = null)
3133
{
3234
Dockerfile = dockerfile;
3335
DockerfileDirectory = dockerfileDirectory;
36+
Target = target;
3437
Image = image;
3538
ImageBuildPolicy = imageBuildPolicy;
3639
BuildArguments = buildArguments;
@@ -65,6 +68,7 @@ public ImageFromDockerfileConfiguration(IImageFromDockerfileConfiguration oldVal
6568
{
6669
Dockerfile = BuildConfiguration.Combine(oldValue.Dockerfile, newValue.Dockerfile);
6770
DockerfileDirectory = BuildConfiguration.Combine(oldValue.DockerfileDirectory, newValue.DockerfileDirectory);
71+
Target = BuildConfiguration.Combine(oldValue.Target, newValue.Target);
6872
Image = BuildConfiguration.Combine(oldValue.Image, newValue.Image);
6973
ImageBuildPolicy = BuildConfiguration.Combine(oldValue.ImageBuildPolicy, newValue.ImageBuildPolicy);
7074
BuildArguments = BuildConfiguration.Combine(oldValue.BuildArguments, newValue.BuildArguments);
@@ -83,6 +87,10 @@ public ImageFromDockerfileConfiguration(IImageFromDockerfileConfiguration oldVal
8387
[JsonIgnore]
8488
public string DockerfileDirectory { get; }
8589

90+
/// <inheritdoc />
91+
[JsonIgnore]
92+
public string Target { get; }
93+
8694
/// <inheritdoc />
8795
[JsonIgnore]
8896
public IImage Image { get; }

src/Testcontainers/Images/DockerfileArchive.cs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

tests/Testcontainers.Tests/Assets/.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ credsStore
44
healthWaitStrategy
55
pullBaseImages
66
scratch
7+
target
78
**/*.md

0 commit comments

Comments
 (0)