From 4f4b6ad7aec28ebebcb371c4cffae0a922cdaae9 Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Mon, 22 Dec 2025 15:23:57 +0100
Subject: [PATCH 1/6] feat: Add Platform property to IImage interface
---
.../Clients/DockerImageOperations.cs | 1 +
src/Testcontainers/Images/DockerImage.cs | 32 ++++-
.../Images/DockerfileArchive.cs | 136 ++++++++++++++++--
.../Images/FutureDockerImage.cs | 10 ++
src/Testcontainers/Images/IImage.cs | 11 ++
src/Testcontainers/Images/IImageExtensions.cs | 2 +-
.../Assets/pullBaseImages/Dockerfile | 7 +-
.../Fixtures/Containers/Unix/DockerMTls.cs | 1 -
.../Containers/Unix/DockerTlsFixture.cs | 1 -
.../Fixtures/Images/DockerImageFixture.cs | 4 +-
.../Images/DockerImageFixtureSerializable.cs | 4 +-
.../Fixtures/Images/HealthCheckFixture.cs | 2 +
...ockerRegistryAuthenticationProviderTest.cs | 2 +-
.../Unit/Images/ImageFromDockerfileTest.cs | 20 +--
.../Unit/Images/TestcontainersImageTest.cs | 14 ++
15 files changed, 214 insertions(+), 33 deletions(-)
diff --git a/src/Testcontainers/Clients/DockerImageOperations.cs b/src/Testcontainers/Clients/DockerImageOperations.cs
index 3fdad710c..53d04d77f 100644
--- a/src/Testcontainers/Clients/DockerImageOperations.cs
+++ b/src/Testcontainers/Clients/DockerImageOperations.cs
@@ -60,6 +60,7 @@ public async Task CreateAsync(IImage image, IDockerRegistryAuthenticationConfigu
var createParameters = new ImagesCreateParameters
{
FromImage = image.FullName,
+ Platform = image.Platform,
};
var authConfig = new AuthConfig
diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs
index d41a4c1d7..ec3629ceb 100644
--- a/src/Testcontainers/Images/DockerImage.cs
+++ b/src/Testcontainers/Images/DockerImage.cs
@@ -31,12 +31,15 @@ public sealed class DockerImage : IImage
[CanBeNull]
private readonly string _digest;
+ [CanBeNull]
+ private readonly string _platform;
+
///
/// Initializes a new instance of the class.
///
/// The image.
public DockerImage(IImage image)
- : this(image.Repository, image.Registry, image.Tag, image.Digest)
+ : this(image.Repository, image.Registry, image.Tag, image.Digest, image.Platform)
{
}
@@ -50,6 +53,25 @@ public DockerImage(string image)
{
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The supported format for is <os>|<arch>|<os>/<arch>[/<variant>].
+ /// You can provide the operating system, the architecture, or both.
+ /// For more details and examples, see containerd/platforms.
+ ///
+ /// The image.
+ /// The platform.
+ /// fedora/httpd:version1.0 where fedora/httpd is the repository and version1.0 the tag.
+ public DockerImage(
+ string image,
+ string platform)
+ : this(GetDockerImage(image))
+ {
+ _platform = TrimOrDefault(platform);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -57,6 +79,7 @@ public DockerImage(string image)
/// The registry.
/// The tag.
/// The digest.
+ /// The platform.
/// The Docker Hub image name prefix.
/// fedora/httpd:version1.0 where fedora/httpd is the repository and version1.0 the tag.
public DockerImage(
@@ -64,12 +87,14 @@ public DockerImage(
string registry = null,
string tag = null,
string digest = null,
+ string platform = null,
string hubImageNamePrefix = null)
: this(
TrimOrDefault(repository),
TrimOrDefault(registry),
TrimOrDefault(tag, tag == null && digest == null ? LatestTag : null),
TrimOrDefault(digest),
+ TrimOrDefault(platform),
hubImageNamePrefix == null ? [] : hubImageNamePrefix.Trim(TrimChars).Split(SlashChar, 2, StringSplitOptions.RemoveEmptyEntries))
{
}
@@ -79,6 +104,7 @@ private DockerImage(
string registry,
string tag,
string digest,
+ string platform,
string[] substitutions)
{
_ = Guard.Argument(repository, nameof(repository))
@@ -109,6 +135,7 @@ private DockerImage(
_tag = tag;
_digest = digest;
+ _platform = platform;
}
///
@@ -123,6 +150,9 @@ private DockerImage(
///
public string Digest => _digest;
+ ///
+ public string Platform => _platform;
+
///
public string FullName
{
diff --git a/src/Testcontainers/Images/DockerfileArchive.cs b/src/Testcontainers/Images/DockerfileArchive.cs
index e2f1e1e23..e4b44b065 100644
--- a/src/Testcontainers/Images/DockerfileArchive.cs
+++ b/src/Testcontainers/Images/DockerfileArchive.cs
@@ -123,12 +123,14 @@ private DockerfileArchive(
/// An of .
public IEnumerable GetBaseImages()
{
- const string imageGroup = "image";
-
const string nameGroup = "name";
const string valueGroup = "value";
+ const string argGroup = "arg";
+
+ const string imageGroup = "image";
+
var lines = File.ReadAllLines(_dockerfile.FullName)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrEmpty(line))
@@ -160,13 +162,16 @@ public IEnumerable GetBaseImages()
.ToArray();
var images = fromMatches
- .Select(match => match.Groups[imageGroup])
- .Select(match => match.Value)
- .Select(line => ReplaceVariables(line, args))
- .Where(line => !line.Any(char.IsUpper))
- .Where(value => !stages.Contains(value))
- .Distinct()
- .Select(value => new DockerImage(value))
+ .Select(match => (Arg: match.Groups[argGroup], Image: match.Groups[imageGroup]))
+ .Select(item => (Arg: ReplaceVariables(item.Arg.Value, args), Image: ReplaceVariables(item.Image.Value, args)))
+ .Where(item => !item.Image.Any(char.IsUpper))
+ .Where(item => !stages.Contains(item.Image))
+ .Select(item =>
+ {
+ var fromArgs = ParseFromArgs(item.Arg).ToDictionary(arg => arg.Name, arg => arg.Value);
+ _ = fromArgs.TryGetValue("platform", out var platform);
+ return new DockerImage(item.Image, platform);
+ })
.ToArray();
return images;
@@ -213,11 +218,11 @@ await AddAsync(absoluteFilePath, relativeFilePath, tarOutputStream)
.ConfigureAwait(false);
}
- var dockerfileDirectoryLength = _dockerfileDirectory.FullName
+ var dockerfileDirectoryLength = _dockerfileDirectory.FullName
.TrimEnd(Path.DirectorySeparatorChar).Length + 1;
var dockerfileRelativeFilePath = _dockerfile.FullName
- .Substring(dockerfileDirectoryLength );
+ .Substring(dockerfileDirectoryLength);
var dockerfileNormalizedRelativeFilePath = Unix.Instance.NormalizePath(dockerfileRelativeFilePath);
@@ -306,23 +311,124 @@ private static int GetUnixFileMode(string filePath)
/// corresponding build argument if present; otherwise, the default value in the
/// Dockerfile is preserved.
///
- /// The image string from a Dockerfile FROM statement.
+ /// The line from a Dockerfile FROM statement.
/// A dictionary containing variable names as keys and their replacement values as values.
/// A new image string where placeholders are replaced with their corresponding values.
- private static string ReplaceVariables(string image, IDictionary variables)
+ private static string ReplaceVariables(string line, IDictionary variables)
{
const string nameGroup = "name";
if (variables.Count == 0)
{
- return image;
+ return line;
}
- return VariablePattern.Replace(image, match =>
+ return VariablePattern.Replace(line, match =>
{
var name = match.Groups[nameGroup].Value;
return variables.TryGetValue(name, out var value) ? value : match.Value;
});
}
+
+ ///
+ /// Parses a FROM statement arg string into flag and value pairs.
+ ///
+ ///
+ /// This method parses a string containing FROM statement style flags,
+ /// respecting quoted values. Both double quotes (") and single
+ /// quotes (') are supported. Whitespaces outside of quotes are
+ /// treated as separators.
+ ///
+ /// For example, the line:
+ ///
+ /// --pull=always --platform="linux/amd64"
+ ///
+ /// becomes:
+ ///
+ /// -
+ ///
+ /// (pull, always)
+ ///
+ ///
+ /// -
+ ///
+ /// (platform, linux/amd64)
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// The FROM statement arg string containing flags and optional values.
+ ///
+ ///
+ /// A sequence of (Name, Value) tuples.
+ ///
+ ///
+ /// Thrown if a quoted value is missing a closing quote.
+ ///
+ private static IEnumerable<(string Name, string Value)> ParseFromArgs(string line)
+ {
+ if (string.IsNullOrEmpty(line))
+ {
+ yield break;
+ }
+
+ char? quote = null;
+
+ var start = 0;
+
+ for (var i = 0; i < line.Length; i++)
+ {
+ var c = line[i];
+
+ if ((c == '"' || c == '\'') && (quote == null || quote == c))
+ {
+ quote = quote == null ? c : null;
+ }
+
+ if (quote != null || !char.IsWhiteSpace(c))
+ {
+ continue;
+ }
+
+ if (i > start)
+ {
+ yield return ParseFlag(line.Substring(start, i - start));
+ }
+
+ start = i + 1;
+ }
+
+ if (quote != null)
+ {
+ throw new FormatException($"Unmatched {quote} quote starting at position {start - 1} in line: '{line}'.");
+ }
+
+ if (line.Length > start)
+ {
+ yield return ParseFlag(line.Substring(start));
+ }
+ }
+
+ ///
+ /// Splits a single flag token into a flag name and an optional value.
+ ///
+ /// A single flag token, optionally containing an equals sign and value.
+ /// A tuple containing the flag name and its value, or null if no value is specified.
+ private static (string Name, string Value) ParseFlag(string flag)
+ {
+ var trimmed = flag.TrimStart('-');
+ var eqIndex = trimmed.IndexOf('=');
+ if (eqIndex == -1)
+ {
+ return (trimmed, null);
+ }
+ else
+ {
+ var name = trimmed.Substring(0, eqIndex);
+ var value = trimmed.Substring(eqIndex + 1).Trim(' ', '"', '\'');
+ return (name, value);
+ }
+ }
}
}
diff --git a/src/Testcontainers/Images/FutureDockerImage.cs b/src/Testcontainers/Images/FutureDockerImage.cs
index 32a44ee96..6c2f7dab4 100644
--- a/src/Testcontainers/Images/FutureDockerImage.cs
+++ b/src/Testcontainers/Images/FutureDockerImage.cs
@@ -68,6 +68,16 @@ public string Digest
}
}
+ ///
+ public string Platform
+ {
+ get
+ {
+ ThrowIfResourceNotFound();
+ return _configuration.Image.Platform;
+ }
+ }
+
///
public string FullName
{
diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs
index 8f35495f7..182156b61 100644
--- a/src/Testcontainers/Images/IImage.cs
+++ b/src/Testcontainers/Images/IImage.cs
@@ -33,6 +33,17 @@ public interface IImage
[CanBeNull]
string Digest { get; }
+ ///
+ /// Gets the platform.
+ ///
+ ///
+ /// The supported format is <os>|<arch>|<os>/<arch>[/<variant>].
+ /// You can provide the operating system, the architecture, or both.
+ /// For more details and examples, see containerd/platforms.
+ ///
+ [CanBeNull]
+ string Platform { get; }
+
///
/// Gets the full image name.
///
diff --git a/src/Testcontainers/Images/IImageExtensions.cs b/src/Testcontainers/Images/IImageExtensions.cs
index 55067ae80..b31c8e013 100644
--- a/src/Testcontainers/Images/IImageExtensions.cs
+++ b/src/Testcontainers/Images/IImageExtensions.cs
@@ -27,7 +27,7 @@ public static IImage ApplyHubImageNamePrefix(this IImage image)
return image;
}
- return new DockerImage(image.Repository, image.Registry, image.Tag, image.Digest, TestcontainersSettings.HubImageNamePrefix);
+ return new DockerImage(image.Repository, image.Registry, image.Tag, image.Digest, image.Platform, TestcontainersSettings.HubImageNamePrefix);
}
}
}
diff --git a/tests/Testcontainers.Tests/Assets/pullBaseImages/Dockerfile b/tests/Testcontainers.Tests/Assets/pullBaseImages/Dockerfile
index ff431cd45..6d04bc65e 100644
--- a/tests/Testcontainers.Tests/Assets/pullBaseImages/Dockerfile
+++ b/tests/Testcontainers.Tests/Assets/pullBaseImages/Dockerfile
@@ -1,4 +1,6 @@
ARG REPO=mcr.microsoft.com/dotnet/aspnet
+ARG PLATFORM=linux/arm64
+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime
FROM build
@@ -8,7 +10,10 @@ FROM ${REPO}:8.0-noble
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
# https://github.com/testcontainers/testcontainers-dotnet/issues/993.
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
+FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
+FROM --platform=$PLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
+FROM --platform="linux/arm/v6" mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
+FROM --platform='linux/arm/v7' mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
# https://github.com/testcontainers/testcontainers-dotnet/issues/1030.
FROM mcr.microsoft.com/dotnet/sdk:$SDK_VERSION_8_0 AS build_sdk_8_0
diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerMTls.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerMTls.cs
index 8921710df..44dd34268 100644
--- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerMTls.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerMTls.cs
@@ -2,7 +2,6 @@ namespace DotNet.Testcontainers.Tests.Fixtures
{
using System.Collections.Generic;
using DotNet.Testcontainers.Builders;
- using DotNet.Testcontainers.Images;
public abstract class DockerMTls : ProtectDockerDaemonSocket
{
diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerTlsFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerTlsFixture.cs
index aee4d2981..1693aa47e 100644
--- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerTlsFixture.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/DockerTlsFixture.cs
@@ -2,7 +2,6 @@ namespace DotNet.Testcontainers.Tests.Fixtures
{
using System.Collections.Generic;
using DotNet.Testcontainers.Builders;
- using DotNet.Testcontainers.Images;
using JetBrains.Annotations;
[UsedImplicitly]
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
index 4497fd8fd..e4a37ea45 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixture.cs
@@ -37,8 +37,8 @@ public DockerImageFixture()
Add(new DockerImageFixtureSerializable(new DockerImage(FedoraHttpd, PortSeparatorRegistry, CustomTag1, null)), $"{PortSeparatorRegistry}/{FedoraHttpd}:{CustomTag1}", $"{PortSeparatorRegistry}/{FedoraHttpd}:{CustomTag1}");
Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, SemVerTag, Digest)), $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}@{Digest}", $"{DotSeparatorRegistry}/{FooBarBaz}:{SemVerTag}@{Digest}");
Add(new DockerImageFixtureSerializable(new DockerImage(FooBarBaz, DotSeparatorRegistry, null, Digest)), $"{DotSeparatorRegistry}/{FooBarBaz}@{Digest}", $"{DotSeparatorRegistry}/{FooBarBaz}@{Digest}");
- Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null, HubImageNamePrefixImplicitLibrary)), $"{HubImageNamePrefixImplicitLibrary}/{BarBaz}", $"{HubImageNamePrefixImplicitLibrary}/{BarBaz}:{LatestTag}");
- Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null, HubImageNamePrefixExplicitLibrary)), $"{HubImageNamePrefixExplicitLibrary}/{BarBaz}", $"{HubImageNamePrefixExplicitLibrary}/{BarBaz}:{LatestTag}");
+ Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null, null, HubImageNamePrefixImplicitLibrary)), $"{HubImageNamePrefixImplicitLibrary}/{BarBaz}", $"{HubImageNamePrefixImplicitLibrary}/{BarBaz}:{LatestTag}");
+ Add(new DockerImageFixtureSerializable(new DockerImage(BarBaz, null, null, null, null, HubImageNamePrefixExplicitLibrary)), $"{HubImageNamePrefixExplicitLibrary}/{BarBaz}", $"{HubImageNamePrefixExplicitLibrary}/{BarBaz}:{LatestTag}");
}
}
}
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
index ebe799359..2176f5a5c 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/DockerImageFixtureSerializable.cs
@@ -22,7 +22,8 @@ public void Deserialize(IXunitSerializationInfo info)
var registry = info.GetValue("Registry");
var tag = info.GetValue("Tag");
var digest = info.GetValue("Digest");
- Image = new DockerImage(repository, registry, tag, digest);
+ var platform = info.GetValue("Platform");
+ Image = new DockerImage(repository, registry, tag, digest, platform);
}
public void Serialize(IXunitSerializationInfo info)
@@ -31,6 +32,7 @@ public void Serialize(IXunitSerializationInfo info)
info.AddValue("Registry", Image.Registry);
info.AddValue("Tag", Image.Tag);
info.AddValue("Digest", Image.Digest);
+ info.AddValue("Platform", Image.Platform);
}
}
}
diff --git a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
index d99399573..f27984fa6 100644
--- a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
+++ b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs
@@ -23,6 +23,8 @@ public sealed class HealthCheckFixture : IImage, IAsyncLifetime
public string Digest => _image.Digest;
+ public string Platform => _image.Platform;
+
public string FullName => _image.FullName;
public string GetHostname()
diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
index 855d190c0..51dbc0358 100644
--- a/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerRegistryAuthenticationProviderTest.cs
@@ -48,7 +48,7 @@ public void GetHostnameFromDockerImage(string dockerImageName, string hostname)
public void GetHostnameFromHubImageNamePrefix(string repository, string tag)
{
const string hubImageNamePrefix = "myregistry.azurecr.io";
- IImage image = new DockerImage(repository, null, tag, null, hubImageNamePrefix);
+ IImage image = new DockerImage(repository, null, tag, null, null, hubImageNamePrefix);
Assert.Equal(hubImageNamePrefix, image.GetHostname());
}
diff --git a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
index 82c4d4ef8..1baada99d 100644
--- a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
@@ -4,7 +4,6 @@ namespace DotNet.Testcontainers.Tests.Unit
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
- using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
@@ -23,13 +22,16 @@ public void DockerfileArchiveGetBaseImages()
// Given
var expected = new[]
{
- "mcr.microsoft.com/dotnet/sdk:8.0",
- "mcr.microsoft.com/dotnet/runtime:8.0",
- "mcr.microsoft.com/dotnet/aspnet:8.0-jammy",
- "mcr.microsoft.com/dotnet/aspnet:8.0-noble",
- "mcr.microsoft.com/dotnet/aspnet:8.0-alpine",
- "mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0",
- "mcr.microsoft.com/dotnet/sdk:8.0.414",
+ new DockerImage("mcr.microsoft.com/dotnet/sdk:8.0"),
+ new DockerImage("mcr.microsoft.com/dotnet/runtime:8.0"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-jammy"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-noble"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-alpine"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/amd64"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm64"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm/v6"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm/v7"),
+ new DockerImage("mcr.microsoft.com/dotnet/sdk:8.0.414"),
};
IImage image = new DockerImage("localhost/testcontainers", Guid.NewGuid().ToString("D"), string.Empty);
@@ -44,7 +46,7 @@ public void DockerfileArchiveGetBaseImages()
var actual = dockerfileArchive.GetBaseImages();
// Then
- Assert.Equal(expected, actual.Select(baseImage => baseImage.FullName));
+ Assert.Equivalent(expected, actual);
}
[Fact]
diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
index 4dc349470..e79222527 100644
--- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
@@ -69,6 +69,20 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl
Assert.Equal(fullName, actual.FullName);
}
+ [Fact]
+ public void Platform_PlatformIsLinuxAmd64_ReturnsLinuxAmd64()
+ {
+ // Given
+ const string platform = "linux/amd64";
+ IImage dockerImage = new DockerImage("foo", platform);
+
+ // When
+ var result = dockerImage.Platform;
+
+ // Then
+ Assert.Equal(platform, result);
+ }
+
[Fact]
public void MatchLatestOrNightly_TagIsLatest_ReturnsTrue()
{
From c1b76c5982fd441a6ca62cf0e6a7d4c3fb85af4d Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Mon, 22 Dec 2025 16:29:46 +0100
Subject: [PATCH 2/6] docs: Add platform doc
---
docs/api/create_docker_container.md | 18 ++++++++++++++++++
src/Testcontainers/Images/DockerfileArchive.cs | 4 ++--
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/docs/api/create_docker_container.md b/docs/api/create_docker_container.md
index 653d4eee6..a2881ad80 100644
--- a/docs/api/create_docker_container.md
+++ b/docs/api/create_docker_container.md
@@ -2,6 +2,24 @@
Testcontainers' generic container support offers the greatest flexibility and makes it easy to use virtually any container image in the context of a temporary test environment. To interact or exchange data with a container, Testcontainers provides `ContainerBuilder` to configure and create the resource.
+## Configure container image
+
+Use `WithImage(...)` to specify the container image.
+
+The simplest overload accepts a `string`:
+
+```csharp
+_ = new ContainerBuilder()
+ .WithImage("postgres:15.1");
+```
+
+For platform-specific scenarios, `WithImage` also accepts an `IImage`. Using the `DockerImage` implementation, you can explicitly set the platform:
+
+```csharp
+_ = new ContainerBuilder()
+ .WithImage(new DockerImage("postgres:15.1", "linux/amd64"));
+```
+
## Configure container start
Both `ENTRYPOINT` and `CMD` allows you to configure an executable and parameters, that a container runs at the start. By default, a container will run whatever `ENTRYPOINT` or `CMD` is specified in the Docker container image. At least one of both configurations is necessary. The container builder implementation supports `WithEntrypoint(params string[])` and `WithCommand(params string[])` to set or override the executable. Ideally, the `ENTRYPOINT` should set the container's executable, whereas the `CMD` sets the default arguments for the `ENTRYPOINT`.
diff --git a/src/Testcontainers/Images/DockerfileArchive.cs b/src/Testcontainers/Images/DockerfileArchive.cs
index e4b44b065..00ce502be 100644
--- a/src/Testcontainers/Images/DockerfileArchive.cs
+++ b/src/Testcontainers/Images/DockerfileArchive.cs
@@ -401,7 +401,7 @@ private static string ReplaceVariables(string line, IDictionary
if (quote != null)
{
- throw new FormatException($"Unmatched {quote} quote starting at position {start - 1} in line: '{line}'.");
+ throw new FormatException($"Unmatched {quote} quote in line: '{line}'.");
}
if (line.Length > start)
@@ -426,7 +426,7 @@ private static (string Name, string Value) ParseFlag(string flag)
else
{
var name = trimmed.Substring(0, eqIndex);
- var value = trimmed.Substring(eqIndex + 1).Trim(' ', '"', '\'');
+ var value = trimmed.Substring(eqIndex + 1).Trim().Trim('"', '\'');
return (name, value);
}
}
From 3dfe17a8284212cc3ac234b38b9a9c8a7776277e Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Tue, 23 Dec 2025 10:43:51 +0100
Subject: [PATCH 3/6] fix: Add Platform type
---
docs/api/create_docker_container.md | 12 ++++--
.../Clients/DockerContainerOperations.cs | 1 +
src/Testcontainers/Images/DockerImage.cs | 9 +----
.../Images/DockerfileArchive.cs | 21 +++++-----
src/Testcontainers/Images/IImage.cs | 5 +--
src/Testcontainers/Images/Platform.cs | 38 +++++++++++++++++++
.../Unit/Images/ImageFromDockerfileTest.cs | 8 ++--
7 files changed, 65 insertions(+), 29 deletions(-)
create mode 100644 src/Testcontainers/Images/Platform.cs
diff --git a/docs/api/create_docker_container.md b/docs/api/create_docker_container.md
index a2881ad80..08c87ff4a 100644
--- a/docs/api/create_docker_container.md
+++ b/docs/api/create_docker_container.md
@@ -4,7 +4,7 @@ Testcontainers' generic container support offers the greatest flexibility and ma
## Configure container image
-Use `WithImage(...)` to specify the container image.
+To specify the container image, use `WithImage(...)`.
The simplest overload accepts a `string`:
@@ -13,13 +13,19 @@ _ = new ContainerBuilder()
.WithImage("postgres:15.1");
```
-For platform-specific scenarios, `WithImage` also accepts an `IImage`. Using the `DockerImage` implementation, you can explicitly set the platform:
+For more advanced scenarios, `WithImage` also supports `IImage`, giving you more control over how the image is represented and its properties are resolved.
+
+If you need to target a specific platform, the `DockerImage` implementation provides an overload that lets you explicitly set the platform, such as `linux/amd64`.
```csharp
_ = new ContainerBuilder()
- .WithImage(new DockerImage("postgres:15.1", "linux/amd64"));
+ .WithImage(new DockerImage("postgres:15.1", new Platform("linux/amd64")));
```
+!!!tip
+
+ A specifier has the format `||/[/]`. The user can provide either the operating system or the architecture or both. For more details, [see containerd/platforms](https://github.com/containerd/platforms).
+
## Configure container start
Both `ENTRYPOINT` and `CMD` allows you to configure an executable and parameters, that a container runs at the start. By default, a container will run whatever `ENTRYPOINT` or `CMD` is specified in the Docker container image. At least one of both configurations is necessary. The container builder implementation supports `WithEntrypoint(params string[])` and `WithCommand(params string[])` to set or override the executable. Ideally, the `ENTRYPOINT` should set the container's executable, whereas the `CMD` sets the default arguments for the `ENTRYPOINT`.
diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs
index efc521678..23aacf026 100644
--- a/src/Testcontainers/Clients/DockerContainerOperations.cs
+++ b/src/Testcontainers/Clients/DockerContainerOperations.cs
@@ -202,6 +202,7 @@ public async Task RunAsync(IContainerConfiguration configuration, Cancel
var createParameters = new CreateContainerParameters
{
Image = configuration.Image.FullName,
+ Platform = configuration.Image.Platform,
Name = configuration.Name,
Hostname = configuration.Hostname,
WorkingDir = configuration.WorkingDirectory,
diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs
index ec3629ceb..ac2884d68 100644
--- a/src/Testcontainers/Images/DockerImage.cs
+++ b/src/Testcontainers/Images/DockerImage.cs
@@ -56,20 +56,15 @@ public DockerImage(string image)
///
/// Initializes a new instance of the class.
///
- ///
- /// The supported format for is <os>|<arch>|<os>/<arch>[/<variant>].
- /// You can provide the operating system, the architecture, or both.
- /// For more details and examples, see containerd/platforms.
- ///
/// The image.
/// The platform.
/// fedora/httpd:version1.0 where fedora/httpd is the repository and version1.0 the tag.
public DockerImage(
string image,
- string platform)
+ Platform platform)
: this(GetDockerImage(image))
{
- _platform = TrimOrDefault(platform);
+ _platform = platform.Value;
}
///
diff --git a/src/Testcontainers/Images/DockerfileArchive.cs b/src/Testcontainers/Images/DockerfileArchive.cs
index 00ce502be..dfb370957 100644
--- a/src/Testcontainers/Images/DockerfileArchive.cs
+++ b/src/Testcontainers/Images/DockerfileArchive.cs
@@ -170,7 +170,7 @@ public IEnumerable GetBaseImages()
{
var fromArgs = ParseFromArgs(item.Arg).ToDictionary(arg => arg.Name, arg => arg.Value);
_ = fromArgs.TryGetValue("platform", out var platform);
- return new DockerImage(item.Image, platform);
+ return new DockerImage(item.Image, new Platform(platform));
})
.ToArray();
@@ -339,11 +339,8 @@ private static string ReplaceVariables(string line, IDictionary
/// quotes (') are supported. Whitespaces outside of quotes are
/// treated as separators.
///
- /// For example, the line:
- ///
- /// --pull=always --platform="linux/amd64"
- ///
- /// becomes:
+ /// E.g., the line --pull=always --platform="linux/amd64" becomes:
+ ///
///
/// -
///
@@ -393,7 +390,7 @@ private static string ReplaceVariables(string line, IDictionary
if (i > start)
{
- yield return ParseFlag(line.Substring(start, i - start));
+ yield return ParseArg(line.Substring(start, i - start));
}
start = i + 1;
@@ -406,18 +403,18 @@ private static string ReplaceVariables(string line, IDictionary
if (line.Length > start)
{
- yield return ParseFlag(line.Substring(start));
+ yield return ParseArg(line.Substring(start));
}
}
///
- /// Splits a single flag token into a flag name and an optional value.
+ /// Splits a single arg into flag name and an optional value.
///
- /// A single flag token, optionally containing an equals sign and value.
+ /// A single arg, optionally containing an equals sign and value.
/// A tuple containing the flag name and its value, or null if no value is specified.
- private static (string Name, string Value) ParseFlag(string flag)
+ private static (string Name, string Value) ParseArg(string arg)
{
- var trimmed = flag.TrimStart('-');
+ var trimmed = arg.TrimStart('-');
var eqIndex = trimmed.IndexOf('=');
if (eqIndex == -1)
{
diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs
index 182156b61..ec0083f08 100644
--- a/src/Testcontainers/Images/IImage.cs
+++ b/src/Testcontainers/Images/IImage.cs
@@ -37,9 +37,8 @@ public interface IImage
/// Gets the platform.
///
///
- /// The supported format is <os>|<arch>|<os>/<arch>[/<variant>].
- /// You can provide the operating system, the architecture, or both.
- /// For more details and examples, see containerd/platforms.
+ /// The supported format for a platform value is:
+ /// <os>|<arch>|<os>/<arch>[/<variant>].
///
[CanBeNull]
string Platform { get; }
diff --git a/src/Testcontainers/Images/Platform.cs b/src/Testcontainers/Images/Platform.cs
new file mode 100644
index 000000000..df92f49c9
--- /dev/null
+++ b/src/Testcontainers/Images/Platform.cs
@@ -0,0 +1,38 @@
+namespace DotNet.Testcontainers.Images
+{
+ using JetBrains.Annotations;
+
+ ///
+ /// Represents a container platform identifier.
+ ///
+ ///
+ /// The supported format for a platform value is:
+ /// <os>|<arch>|<os>/<arch>[/<variant>].
+ ///
+ /// You can provide either the operating system or the architecture or both.
+ /// For more details, see containerd/platforms.
+ ///
+ [PublicAPI]
+ public readonly struct Platform
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The platform identifier.
+ [PublicAPI]
+ public Platform(string value)
+ {
+ }
+
+ ///
+ /// Gets the platform identifier.
+ ///
+ ///
+ /// A string representing the container platform in containerd/platforms format, or
+ /// null if no platform was specified.
+ ///
+ [PublicAPI]
+ [CanBeNull]
+ public string Value { get; }
+ }
+}
diff --git a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
index 1baada99d..95c55f961 100644
--- a/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/ImageFromDockerfileTest.cs
@@ -27,10 +27,10 @@ public void DockerfileArchiveGetBaseImages()
new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-jammy"),
new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-noble"),
new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-alpine"),
- new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/amd64"),
- new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm64"),
- new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm/v6"),
- new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", "linux/arm/v7"),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", new Platform("linux/amd64")),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", new Platform("linux/arm64")),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", new Platform("linux/arm/v6")),
+ new DockerImage("mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0", new Platform("linux/arm/v7")),
new DockerImage("mcr.microsoft.com/dotnet/sdk:8.0.414"),
};
From f2bf9d75981871988068ce060e91340695fa9b60 Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Tue, 23 Dec 2025 14:06:15 +0100
Subject: [PATCH 4/6] fix: Set Platform.Value
---
docs/api/create_docker_container.md | 4 ++--
src/Testcontainers/Images/DockerfileArchive.cs | 6 +++---
src/Testcontainers/Images/Platform.cs | 1 +
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/docs/api/create_docker_container.md b/docs/api/create_docker_container.md
index 08c87ff4a..496547dd4 100644
--- a/docs/api/create_docker_container.md
+++ b/docs/api/create_docker_container.md
@@ -15,7 +15,7 @@ _ = new ContainerBuilder()
For more advanced scenarios, `WithImage` also supports `IImage`, giving you more control over how the image is represented and its properties are resolved.
-If you need to target a specific platform, the `DockerImage` implementation provides an overload that lets you explicitly set the platform, such as `linux/amd64`.
+If you need to target a specific platform, the `DockerImage` implementation provides an overload that lets you explicitly set the platform, such as `linux/amd64`. By default, the container runtime uses the platform that matches the container host.
```csharp
_ = new ContainerBuilder()
@@ -24,7 +24,7 @@ _ = new ContainerBuilder()
!!!tip
- A specifier has the format `||/[/]`. The user can provide either the operating system or the architecture or both. For more details, [see containerd/platforms](https://github.com/containerd/platforms).
+ A specifier has the format `||/[/]`. The user can provide either the operating system or the architecture or both. For more details, see [containerd/platforms](https://github.com/containerd/platforms).
## Configure container start
diff --git a/src/Testcontainers/Images/DockerfileArchive.cs b/src/Testcontainers/Images/DockerfileArchive.cs
index dfb370957..79261fa2b 100644
--- a/src/Testcontainers/Images/DockerfileArchive.cs
+++ b/src/Testcontainers/Images/DockerfileArchive.cs
@@ -162,13 +162,13 @@ public IEnumerable GetBaseImages()
.ToArray();
var images = fromMatches
- .Select(match => (Arg: match.Groups[argGroup], Image: match.Groups[imageGroup]))
- .Select(item => (Arg: ReplaceVariables(item.Arg.Value, args), Image: ReplaceVariables(item.Image.Value, args)))
+ .Select(match => (FromArgs: match.Groups[argGroup], Image: match.Groups[imageGroup]))
+ .Select(item => (FromArgs: ReplaceVariables(item.FromArgs.Value, args), Image: ReplaceVariables(item.Image.Value, args)))
.Where(item => !item.Image.Any(char.IsUpper))
.Where(item => !stages.Contains(item.Image))
.Select(item =>
{
- var fromArgs = ParseFromArgs(item.Arg).ToDictionary(arg => arg.Name, arg => arg.Value);
+ var fromArgs = ParseFromArgs(item.FromArgs).ToDictionary(arg => arg.Name, arg => arg.Value);
_ = fromArgs.TryGetValue("platform", out var platform);
return new DockerImage(item.Image, new Platform(platform));
})
diff --git a/src/Testcontainers/Images/Platform.cs b/src/Testcontainers/Images/Platform.cs
index df92f49c9..ffedf0bfa 100644
--- a/src/Testcontainers/Images/Platform.cs
+++ b/src/Testcontainers/Images/Platform.cs
@@ -22,6 +22,7 @@ public readonly struct Platform
[PublicAPI]
public Platform(string value)
{
+ Value = value;
}
///
From eb674c88efa686a7688428265d73204b55325c35 Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Tue, 23 Dec 2025 15:09:34 +0100
Subject: [PATCH 5/6] fix: Use correct ctor
---
.../Unit/Images/TestcontainersImageTest.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
index e79222527..d371bfaea 100644
--- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
@@ -73,14 +73,14 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl
public void Platform_PlatformIsLinuxAmd64_ReturnsLinuxAmd64()
{
// Given
- const string platform = "linux/amd64";
- IImage dockerImage = new DockerImage("foo", platform);
+ const string linuxAmd64 = "linux/amd64";
+ IImage dockerImage = new DockerImage("foo", new Platform(linuxAmd64));
// When
var result = dockerImage.Platform;
// Then
- Assert.Equal(platform, result);
+ Assert.Equal(linuxAmd64, result);
}
[Fact]
From 09ea9c16efe5afd68ad089421603b8e3ca464e0a Mon Sep 17 00:00:00 2001
From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
Date: Tue, 23 Dec 2025 17:17:41 +0100
Subject: [PATCH 6/6] chore: Apply suggestions
---
src/Testcontainers/Images/Platform.cs | 2 +-
.../Unit/Images/TestcontainersImageTest.cs | 24 +++++++++++++++----
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/Testcontainers/Images/Platform.cs b/src/Testcontainers/Images/Platform.cs
index ffedf0bfa..ace0ea4ef 100644
--- a/src/Testcontainers/Images/Platform.cs
+++ b/src/Testcontainers/Images/Platform.cs
@@ -1,4 +1,4 @@
-namespace DotNet.Testcontainers.Images
+namespace DotNet.Testcontainers.Images
{
using JetBrains.Annotations;
diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
index d371bfaea..cf1cd9e45 100644
--- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
+++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs
@@ -70,17 +70,33 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl
}
[Fact]
- public void Platform_PlatformIsLinuxAmd64_ReturnsLinuxAmd64()
+ public void Platform_NoPlatformSpecified_ReturnsNull()
{
// Given
- const string linuxAmd64 = "linux/amd64";
- IImage dockerImage = new DockerImage("foo", new Platform(linuxAmd64));
+ IImage dockerImage = new DockerImage("foo");
// When
var result = dockerImage.Platform;
// Then
- Assert.Equal(linuxAmd64, result);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("linux/amd64")]
+ [InlineData("linux/arm64")]
+ [InlineData("linux/arm/v6")]
+ [InlineData("linux/arm/v7")]
+ public void Platform_PlatformSpecified_ReturnsPlatform(string platform)
+ {
+ // Given
+ IImage dockerImage = new DockerImage("foo", new Platform(platform));
+
+ // When
+ var result = dockerImage.Platform;
+
+ // Then
+ Assert.Equal(platform, result);
}
[Fact]