diff --git a/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs b/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs index d8be6544a..eb6b39ec7 100644 --- a/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs +++ b/src/Testcontainers/Builders/IImageFromDockerfileBuilder`1.cs @@ -53,6 +53,15 @@ public interface IImageFromDockerfileBuilder [PublicAPI] TBuilderEntity WithDockerfileDirectory(CommonDirectoryPath commonDirectoryPath, string dockerfileDirectory); + /// + /// Sets the target build stage for the Docker image, allowing partial builds for + /// multi-stage Dockerfiles. + /// + /// The target build stage to use for the image build. + /// A configured instance of . + [PublicAPI] + TBuilderEntity WithTarget(string target); + /// /// Sets the image build policy. /// diff --git a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs index 7ddc6e779..b341c114c 100644 --- a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs +++ b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs @@ -83,6 +83,12 @@ public ImageFromDockerfileBuilder WithDockerfileDirectory(CommonDirectoryPath co return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(dockerfileDirectory: dockerfileDirectoryPath)); } + /// + public ImageFromDockerfileBuilder WithTarget(string target) + { + return Merge(DockerResourceConfiguration, new ImageFromDockerfileConfiguration(target: target)); + } + /// public ImageFromDockerfileBuilder WithImageBuildPolicy(Func imageBuildPolicy) { diff --git a/src/Testcontainers/Clients/DockerImageOperations.cs b/src/Testcontainers/Clients/DockerImageOperations.cs index 1b55f6b4a..3fdad710c 100644 --- a/src/Testcontainers/Clients/DockerImageOperations.cs +++ b/src/Testcontainers/Clients/DockerImageOperations.cs @@ -98,6 +98,7 @@ await DeleteAsync(image, ct) var buildParameters = new ImageBuildParameters { Dockerfile = configuration.Dockerfile, + Target = configuration.Target, Tags = new List { image.FullName }, BuildArgs = configuration.BuildArguments.ToDictionary(item => item.Key, item => item.Value), Labels = configuration.Labels.ToDictionary(item => item.Key, item => item.Value), diff --git a/src/Testcontainers/Clients/TraceProgress.cs b/src/Testcontainers/Clients/TraceProgress.cs index f03aa11c7..dae1fb17f 100644 --- a/src/Testcontainers/Clients/TraceProgress.cs +++ b/src/Testcontainers/Clients/TraceProgress.cs @@ -19,22 +19,22 @@ public void Report(JSONMessage value) if (!string.IsNullOrWhiteSpace(value.Status)) { - _logger.LogDebug(value.Status); + _logger.LogDebug(value.Status.TrimEnd()); } if (!string.IsNullOrWhiteSpace(value.Stream)) { - _logger.LogDebug(value.Stream); + _logger.LogDebug(value.Stream.TrimEnd()); } if (!string.IsNullOrWhiteSpace(value.ProgressMessage)) { - _logger.LogDebug(value.ProgressMessage); + _logger.LogDebug(value.ProgressMessage.TrimEnd()); } if (!string.IsNullOrWhiteSpace(value.ErrorMessage)) { - _logger.LogError(value.ErrorMessage); + _logger.LogError(value.ErrorMessage.TrimEnd()); } #pragma warning restore CA1848, CA2254 diff --git a/src/Testcontainers/Configurations/Images/IImageFromDockerfileConfiguration.cs b/src/Testcontainers/Configurations/Images/IImageFromDockerfileConfiguration.cs index eac754820..d97da133e 100644 --- a/src/Testcontainers/Configurations/Images/IImageFromDockerfileConfiguration.cs +++ b/src/Testcontainers/Configurations/Images/IImageFromDockerfileConfiguration.cs @@ -27,6 +27,11 @@ public interface IImageFromDockerfileConfiguration : IResourceConfiguration string DockerfileDirectory { get; } + /// + /// Gets the target. + /// + string Target { get; } + /// /// Gets the image. /// diff --git a/src/Testcontainers/Configurations/Images/ImageFromDockerfileConfiguration.cs b/src/Testcontainers/Configurations/Images/ImageFromDockerfileConfiguration.cs index a3c59509c..145ed26c8 100644 --- a/src/Testcontainers/Configurations/Images/ImageFromDockerfileConfiguration.cs +++ b/src/Testcontainers/Configurations/Images/ImageFromDockerfileConfiguration.cs @@ -17,6 +17,7 @@ internal sealed class ImageFromDockerfileConfiguration : ResourceConfiguration /// The Dockerfile. /// The Dockerfile directory. + /// The target. /// The image. /// The image build policy. /// A list of build arguments. @@ -24,6 +25,7 @@ internal sealed class ImageFromDockerfileConfiguration : ResourceConfiguration imageBuildPolicy = null, IReadOnlyDictionary buildArguments = null, @@ -31,6 +33,7 @@ public ImageFromDockerfileConfiguration( { Dockerfile = dockerfile; DockerfileDirectory = dockerfileDirectory; + Target = target; Image = image; ImageBuildPolicy = imageBuildPolicy; BuildArguments = buildArguments; @@ -65,6 +68,7 @@ public ImageFromDockerfileConfiguration(IImageFromDockerfileConfiguration oldVal { Dockerfile = BuildConfiguration.Combine(oldValue.Dockerfile, newValue.Dockerfile); DockerfileDirectory = BuildConfiguration.Combine(oldValue.DockerfileDirectory, newValue.DockerfileDirectory); + Target = BuildConfiguration.Combine(oldValue.Target, newValue.Target); Image = BuildConfiguration.Combine(oldValue.Image, newValue.Image); ImageBuildPolicy = BuildConfiguration.Combine(oldValue.ImageBuildPolicy, newValue.ImageBuildPolicy); BuildArguments = BuildConfiguration.Combine(oldValue.BuildArguments, newValue.BuildArguments); @@ -83,6 +87,10 @@ public ImageFromDockerfileConfiguration(IImageFromDockerfileConfiguration oldVal [JsonIgnore] public string DockerfileDirectory { get; } + /// + [JsonIgnore] + public string Target { get; } + /// [JsonIgnore] public IImage Image { get; } diff --git a/tests/Testcontainers.Tests/Assets/.dockerignore b/tests/Testcontainers.Tests/Assets/.dockerignore index d2e8921ac..71b2c46ad 100644 --- a/tests/Testcontainers.Tests/Assets/.dockerignore +++ b/tests/Testcontainers.Tests/Assets/.dockerignore @@ -4,4 +4,5 @@ credsStore healthWaitStrategy pullBaseImages scratch +target **/*.md diff --git a/tests/Testcontainers.Tests/Assets/target/Dockerfile b/tests/Testcontainers.Tests/Assets/target/Dockerfile new file mode 100644 index 000000000..f84eb0be7 --- /dev/null +++ b/tests/Testcontainers.Tests/Assets/target/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch AS base +FROM base AS build +FROM build AS final diff --git a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs index 3c86ec877..b450d88db 100644 --- a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs @@ -11,6 +11,7 @@ namespace DotNet.Testcontainers.Tests.Unit using DotNet.Testcontainers.Commons; using DotNet.Testcontainers.Images; using ICSharpCode.SharpZipLib.Tar; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -168,5 +169,42 @@ await imageFromDockerfileBuilder.CreateAsync(TestContext.Current.CancellationTok Assert.NotNull(imageFromDockerfileBuilder.FullName); Assert.Null(imageFromDockerfileBuilder.GetHostname()); } + + [Fact] + public async Task BuildTargetBuildsUpToExpectedTarget() + { + // Given + var logger = new TestLogger(); + + var imageFromDockerfileBuilder = new ImageFromDockerfileBuilder() + .WithDockerfileDirectory("Assets/target") + .WithTarget("build") + .WithLogger(logger) + .Build(); + + // When + await imageFromDockerfileBuilder.CreateAsync(TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + Assert.Contains(logger.Logs, line => line.Contains("FROM scratch AS base")); + Assert.Contains(logger.Logs, line => line.Contains("FROM base AS build")); + Assert.DoesNotContain(logger.Logs, line => line.Contains("FROM build AS final")); + } + + private sealed class TestLogger : ILogger + { + public IList Logs { get; } + = new List(); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + => Logs.Add(formatter(state, exception)); + + public bool IsEnabled(LogLevel logLevel) + => true; + + public IDisposable BeginScope(TState state) where TState : notnull + => null; + } } }