Skip to content

Commit 9608bd9

Browse files
authored
feat: Add WithTarget(string) to image builder (#1534)
1 parent b933ae3 commit 9608bd9

File tree

9 files changed

+75
-4
lines changed

9 files changed

+75
-4
lines changed

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/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; }

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
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM scratch AS base
2+
FROM base AS build
3+
FROM build AS final

tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace DotNet.Testcontainers.Tests.Unit
1111
using DotNet.Testcontainers.Commons;
1212
using DotNet.Testcontainers.Images;
1313
using ICSharpCode.SharpZipLib.Tar;
14+
using Microsoft.Extensions.Logging;
1415
using Microsoft.Extensions.Logging.Abstractions;
1516
using Xunit;
1617

@@ -168,5 +169,42 @@ await imageFromDockerfileBuilder.CreateAsync(TestContext.Current.CancellationTok
168169
Assert.NotNull(imageFromDockerfileBuilder.FullName);
169170
Assert.Null(imageFromDockerfileBuilder.GetHostname());
170171
}
172+
173+
[Fact]
174+
public async Task BuildTargetBuildsUpToExpectedTarget()
175+
{
176+
// Given
177+
var logger = new TestLogger();
178+
179+
var imageFromDockerfileBuilder = new ImageFromDockerfileBuilder()
180+
.WithDockerfileDirectory("Assets/target")
181+
.WithTarget("build")
182+
.WithLogger(logger)
183+
.Build();
184+
185+
// When
186+
await imageFromDockerfileBuilder.CreateAsync(TestContext.Current.CancellationToken)
187+
.ConfigureAwait(true);
188+
189+
// Then
190+
Assert.Contains(logger.Logs, line => line.Contains("FROM scratch AS base"));
191+
Assert.Contains(logger.Logs, line => line.Contains("FROM base AS build"));
192+
Assert.DoesNotContain(logger.Logs, line => line.Contains("FROM build AS final"));
193+
}
194+
195+
private sealed class TestLogger : ILogger
196+
{
197+
public IList<string> Logs { get; }
198+
= new List<string>();
199+
200+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
201+
=> Logs.Add(formatter(state, exception));
202+
203+
public bool IsEnabled(LogLevel logLevel)
204+
=> true;
205+
206+
public IDisposable BeginScope<TState>(TState state) where TState : notnull
207+
=> null;
208+
}
171209
}
172210
}

0 commit comments

Comments
 (0)