Skip to content

Commit 23ef5a2

Browse files
authored
fix: Do not pre pull Dockerfile build stages that do not correspond to base images (#979)
1 parent 7808ac4 commit 23ef5a2

File tree

9 files changed

+105
-36
lines changed

9 files changed

+105
-36
lines changed

.devcontainer/devcontainer.json

+11-9
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@
1414
"moby": true
1515
},
1616
"ghcr.io/devcontainers/features/dotnet:1": {
17-
"version": "6.0.405",
17+
"version": "6.0.413",
1818
"installUsingApt": false
1919
}
2020
},
21-
"extensions": [
22-
"formulahendry.dotnet-test-explorer",
23-
"ms-azuretools.vscode-docker",
24-
"ms-dotnettools.csharp"
25-
],
26-
"settings": {
27-
"omnisharp.path": "latest" // https://github.com/OmniSharp/omnisharp-vscode/issues/5410#issuecomment-1284531542.
21+
"customizations": {
22+
"extensions": [
23+
"formulahendry.dotnet-test-explorer",
24+
"ms-azuretools.vscode-docker",
25+
"ms-dotnettools.csharp"
26+
],
27+
"settings": {
28+
"omnisharp.path": "latest" // https://github.com/OmniSharp/omnisharp-vscode/issues/5410#issuecomment-1284531542.
29+
}
2830
},
29-
"postCreateCommand": ["git", "lfs", "pull"],
31+
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git lfs checkout",
3032
"postStartCommand": ["dotnet", "build"]
3133
}

src/Testcontainers/Clients/DockerImageOperations.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,10 @@ public Task DeleteAsync(IImage image, CancellationToken ct = default)
8888
return Docker.Images.DeleteImageAsync(image.FullName, new ImageDeleteParameters { Force = true }, ct);
8989
}
9090

91-
public async Task<string> BuildAsync(IImageFromDockerfileConfiguration configuration, CancellationToken ct = default)
91+
public async Task<string> BuildAsync(IImageFromDockerfileConfiguration configuration, ITarArchive dockerfileArchive, CancellationToken ct = default)
9292
{
9393
var image = configuration.Image;
9494

95-
ITarArchive dockerfileArchive = new DockerfileArchive(configuration.DockerfileDirectory, configuration.Dockerfile, image, _logger);
96-
9795
var imageExists = await ExistsWithNameAsync(image.FullName, ct)
9896
.ConfigureAwait(false);
9997

src/Testcontainers/Clients/IDockerImageOperations.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ internal interface IDockerImageOperations : IHasListOperations<ImagesListRespons
1212

1313
Task DeleteAsync(IImage image, CancellationToken ct = default);
1414

15-
Task<string> BuildAsync(IImageFromDockerfileConfiguration configuration, CancellationToken ct = default);
15+
Task<string> BuildAsync(IImageFromDockerfileConfiguration configuration, ITarArchive dockerfileArchive, CancellationToken ct = default);
1616
}
1717
}

src/Testcontainers/Clients/TestcontainersClient.cs

+13-19
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace DotNet.Testcontainers.Clients
66
using System.Linq;
77
using System.Reflection;
88
using System.Text;
9-
using System.Text.RegularExpressions;
109
using System.Threading;
1110
using System.Threading.Tasks;
1211
using Docker.DotNet;
@@ -33,10 +32,10 @@ internal sealed class TestcontainersClient : ITestcontainersClient
3332

3433
private static readonly string OSRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());
3534

36-
private static readonly Regex FromLinePattern = new Regex("FROM (?<arg>--[^\\s]+\\s)*(?<image>[^\\s]+).*", RegexOptions.None, TimeSpan.FromSeconds(1));
37-
3835
private readonly DockerRegistryAuthenticationProvider _registryAuthenticationProvider;
3936

37+
private readonly ILogger _logger;
38+
4039
/// <summary>
4140
/// Initializes a new instance of the <see cref="TestcontainersClient" /> class.
4241
/// </summary>
@@ -50,7 +49,8 @@ public TestcontainersClient(Guid sessionId, IDockerEndpointAuthenticationConfigu
5049
new DockerNetworkOperations(sessionId, dockerEndpointAuthConfig, logger),
5150
new DockerVolumeOperations(sessionId, dockerEndpointAuthConfig, logger),
5251
new DockerSystemOperations(sessionId, dockerEndpointAuthConfig, logger),
53-
new DockerRegistryAuthenticationProvider(logger))
52+
new DockerRegistryAuthenticationProvider(logger),
53+
logger)
5454
{
5555
}
5656

@@ -60,9 +60,11 @@ private TestcontainersClient(
6060
IDockerNetworkOperations networkOperations,
6161
IDockerVolumeOperations volumeOperations,
6262
IDockerSystemOperations systemOperations,
63-
DockerRegistryAuthenticationProvider registryAuthenticationProvider)
63+
DockerRegistryAuthenticationProvider registryAuthenticationProvider,
64+
ILogger logger)
6465
{
6566
_registryAuthenticationProvider = registryAuthenticationProvider;
67+
_logger = logger;
6668
Container = containerOperations;
6769
Image = imageOperations;
6870
Network = networkOperations;
@@ -328,25 +330,17 @@ await Task.WhenAll(configuration.ResourceMappings.Values.Select(resourceMapping
328330
/// <inheritdoc />
329331
public async Task<string> BuildAsync(IImageFromDockerfileConfiguration configuration, CancellationToken ct = default)
330332
{
331-
var dockerfileFilePath = Path.Combine(configuration.DockerfileDirectory, configuration.Dockerfile);
332-
333333
var cachedImage = await Image.ByNameAsync(configuration.Image.FullName, ct)
334334
.ConfigureAwait(false);
335335

336-
if (File.Exists(dockerfileFilePath))
337-
{
338-
await Task.WhenAll(File.ReadAllLines(dockerfileFilePath)
339-
.Select(line => FromLinePattern.Match(line))
340-
.Where(match => match.Success)
341-
.Select(match => match.Groups["image"])
342-
.Select(group => group.Value)
343-
.Select(image => new DockerImage(image))
344-
.Select(image => PullImageAsync(image, ct)));
345-
}
346-
347336
if (configuration.ImageBuildPolicy(cachedImage))
348337
{
349-
_ = await Image.BuildAsync(configuration, ct)
338+
var dockerfileArchive = new DockerfileArchive(configuration.DockerfileDirectory, configuration.Dockerfile, configuration.Image, _logger);
339+
340+
await Task.WhenAll(dockerfileArchive.GetBaseImages().Select(image => PullImageAsync(image, ct)))
341+
.ConfigureAwait(false);
342+
343+
_ = await Image.BuildAsync(configuration, dockerfileArchive, ct)
350344
.ConfigureAwait(false);
351345
}
352346

src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public interface IWaitForContainerOS
103103
/// <summary>
104104
/// Returns a collection with all configured wait strategies.
105105
/// </summary>
106-
/// <returns>List with all configured wait strategies.</returns>
106+
/// <returns>Returns a list with all configured wait strategies.</returns>
107107
[PublicAPI]
108108
IEnumerable<IWaitUntil> Build();
109109
}

src/Testcontainers/Images/DockerfileArchive.cs

+50
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ 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));
21+
2022
private readonly DirectoryInfo _dockerfileDirectory;
2123

2224
private readonly FileInfo _dockerfile;
@@ -64,6 +66,54 @@ public DockerfileArchive(DirectoryInfo dockerfileDirectory, FileInfo dockerfile,
6466
_logger = logger;
6567
}
6668

69+
/// <summary>
70+
/// Gets a collection of base images.
71+
/// </summary>
72+
/// <remarks>
73+
/// This method reads the Dockerfile and collects a list of base images. It
74+
/// excludes stages that do not correspond to base images. For example, it will not include
75+
/// the second line from the following Dockerfile configuration:
76+
/// <code>
77+
/// FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
78+
/// FROM build
79+
/// </code>
80+
/// </remarks>
81+
/// <returns>An <see cref="IEnumerable{T}" /> of <see cref="IImage" />.</returns>
82+
public IEnumerable<IImage> GetBaseImages()
83+
{
84+
const string imageGroup = "image";
85+
86+
var lines = File.ReadAllLines(Path.Combine(_dockerfileDirectory.FullName, _dockerfile.ToString()))
87+
.Select(line => line.Trim())
88+
.Where(line => !string.IsNullOrEmpty(line))
89+
.Where(line => !line.StartsWith("#", StringComparison.Ordinal))
90+
.Select(line => FromLinePattern.Match(line))
91+
.Where(match => match.Success)
92+
// Until now, we are unable to resolve variables within Dockerfiles. Ignore base
93+
// images that utilize variables. Expect them to exist on the host.
94+
.Where(match => !match.Groups[imageGroup].Value.Contains('$'))
95+
.Where(match => !match.Groups[imageGroup].Value.Any(char.IsUpper))
96+
.ToArray();
97+
98+
var stages = lines
99+
.Select(line => line.Value)
100+
.Select(line => line.Split(new [] { " AS ", " As ", " aS ", " as " }, StringSplitOptions.RemoveEmptyEntries))
101+
.Where(substrings => substrings.Length > 1)
102+
.Select(substrings => substrings[substrings.Length - 1])
103+
.Distinct()
104+
.ToArray();
105+
106+
var images = lines
107+
.Select(match => match.Groups[imageGroup])
108+
.Select(group => group.Value)
109+
.Where(value => !stages.Contains(value))
110+
.Distinct()
111+
.Select(value => new DockerImage(value))
112+
.ToArray();
113+
114+
return images;
115+
}
116+
67117
/// <inheritdoc />
68118
public async Task<string> Tar(CancellationToken ct = default)
69119
{

tests/Testcontainers.Tests/Assets/.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ Dockerfile
22
credHelpers
33
credsStore
44
healthWaitStrategy
5+
pullBaseImages
56
**/*.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ARG REPO=mcr.microsoft.com/dotnet/aspnet
2+
FROM $REPO:6.0.21-jammy-amd64
3+
FROM ${REPO}:6.0.21-jammy-amd64
4+
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
5+
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS runtime
6+
FROM build
7+
FROM build AS publish
8+
FROM mcr.microsoft.com/dotnet/aspnet:6.0.21-jammy-amd64

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

+19-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Tests.Unit
33
using System;
44
using System.Collections.Generic;
55
using System.IO;
6+
using System.Linq;
67
using System.Text;
78
using System.Threading.Tasks;
89
using DotNet.Testcontainers.Builders;
@@ -14,17 +15,32 @@ namespace DotNet.Testcontainers.Tests.Unit
1415

1516
public sealed class ImageFromDockerfileTest
1617
{
18+
[Fact]
19+
public void DockerfileArchiveGetBaseImages()
20+
{
21+
// Given
22+
IImage image = new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty);
23+
24+
var dockerfileArchive = new DockerfileArchive("Assets//pullBaseImages/", "Dockerfile", image, NullLogger.Instance);
25+
26+
// When
27+
var baseImages = dockerfileArchive.GetBaseImages();
28+
29+
// Then
30+
Assert.Equal(3, baseImages.Count());
31+
}
32+
1733
[Fact]
1834
public async Task DockerfileArchiveTar()
1935
{
2036
// Given
21-
var image = new DockerImage("testcontainers", "test", "0.1.0");
37+
IImage image = new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty);
2238

2339
var expected = new SortedSet<string> { ".dockerignore", "Dockerfile", "setup/setup.sh" };
2440

2541
var actual = new SortedSet<string>();
2642

27-
var dockerfileArchive = new DockerfileArchive("Assets", "Dockerfile", image, NullLogger.Instance);
43+
var dockerfileArchive = new DockerfileArchive("Assets/", "Dockerfile", image, NullLogger.Instance);
2844

2945
var dockerfileArchiveFilePath = await dockerfileArchive.Tar()
3046
.ConfigureAwait(false);
@@ -91,7 +107,7 @@ public async Task BuildsDockerImage()
91107
var imageFromDockerfileBuilder = new ImageFromDockerfileBuilder()
92108
.WithName(tag1)
93109
.WithDockerfile("Dockerfile")
94-
.WithDockerfileDirectory("Assets")
110+
.WithDockerfileDirectory("Assets/")
95111
.WithDeleteIfExists(true)
96112
.WithCreateParameterModifier(parameterModifier => parameterModifier.Tags.Add(tag2.FullName))
97113
.Build();

0 commit comments

Comments
 (0)