Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/api/create_docker_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

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

To specify the container image, use `WithImage(...)`.

The simplest overload accepts a `string`:

```csharp
_ = new ContainerBuilder()
.WithImage("postgres:15.1");
```

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`. By default, the container runtime uses the platform that matches the container host.

```csharp
_ = new ContainerBuilder()
.WithImage(new DockerImage("postgres:15.1", new Platform("linux/amd64")));
```

!!!tip

A specifier has the format `<os>|<arch>|<os>/<arch>[/<variant>]`. 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`.
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers/Clients/DockerContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public async Task<string> 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,
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers/Clients/DockerImageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src/Testcontainers/Images/DockerImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ public sealed class DockerImage : IImage
[CanBeNull]
private readonly string _digest;

[CanBeNull]
private readonly string _platform;

/// <summary>
/// Initializes a new instance of the <see cref="DockerImage" /> class.
/// </summary>
/// <param name="image">The image.</param>
public DockerImage(IImage image)
: this(image.Repository, image.Registry, image.Tag, image.Digest)
: this(image.Repository, image.Registry, image.Tag, image.Digest, image.Platform)
{
}

Expand All @@ -50,26 +53,43 @@ public DockerImage(string image)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerImage" /> class.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="platform">The platform.</param>
/// <example><c>fedora/httpd:version1.0</c> where <c>fedora/httpd</c> is the repository and <c>version1.0</c> the tag.</example>
public DockerImage(
string image,
Platform platform)
: this(GetDockerImage(image))
{
_platform = platform.Value;
}
Comment thread
HofmeisterAn marked this conversation as resolved.

/// <summary>
/// Initializes a new instance of the <see cref="DockerImage" /> class.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="registry">The registry.</param>
/// <param name="tag">The tag.</param>
/// <param name="digest">The digest.</param>
/// <param name="platform">The platform.</param>
/// <param name="hubImageNamePrefix">The Docker Hub image name prefix.</param>
/// <example><c>fedora/httpd:version1.0</c> where <c>fedora/httpd</c> is the repository and <c>version1.0</c> the tag.</example>
public DockerImage(
string repository,
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))
{
}
Expand All @@ -79,6 +99,7 @@ private DockerImage(
string registry,
string tag,
string digest,
string platform,
string[] substitutions)
{
_ = Guard.Argument(repository, nameof(repository))
Expand Down Expand Up @@ -109,6 +130,7 @@ private DockerImage(

_tag = tag;
_digest = digest;
_platform = platform;
}

/// <inheritdoc />
Expand All @@ -123,6 +145,9 @@ private DockerImage(
/// <inheritdoc />
public string Digest => _digest;

/// <inheritdoc />
public string Platform => _platform;

/// <inheritdoc />
public string FullName
{
Expand Down
133 changes: 118 additions & 15 deletions src/Testcontainers/Images/DockerfileArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ private DockerfileArchive(
/// <returns>An <see cref="IEnumerable{T}" /> of <see cref="IImage" />.</returns>
public IEnumerable<IImage> 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))
Expand Down Expand Up @@ -160,13 +162,16 @@ public IEnumerable<IImage> 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 => (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.FromArgs).ToDictionary(arg => arg.Name, arg => arg.Value);
_ = fromArgs.TryGetValue("platform", out var platform);
return new DockerImage(item.Image, new Platform(platform));
})
.ToArray();

return images;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -306,23 +311,121 @@ private static int GetUnixFileMode(string filePath)
/// corresponding build argument if present; otherwise, the default value in the
/// Dockerfile is preserved.
/// </summary>
/// <param name="image">The image string from a Dockerfile <c>FROM</c> statement.</param>
/// <param name="line">The line from a Dockerfile <c>FROM</c> statement.</param>
/// <param name="variables">A dictionary containing variable names as keys and their replacement values as values.</param>
/// <returns>A new image string where placeholders are replaced with their corresponding values.</returns>
private static string ReplaceVariables(string image, IDictionary<string, string> variables)
private static string ReplaceVariables(string line, IDictionary<string, string> 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;
});
}

/// <summary>
/// Parses a FROM statement arg string into flag and value pairs.
/// </summary>
/// <remarks>
/// This method parses a string containing FROM statement style flags,
/// respecting quoted values. Both double quotes (<c>"</c>) and single
/// quotes (<c>'</c>) are supported. Whitespaces outside of quotes are
/// treated as separators.
///
/// E.g., the line <c>--pull=always --platform="linux/amd64"</c> becomes:
///
/// <list type="bullet">
/// <item>
/// <description>
/// (<c>pull</c>, <c>always</c>)
/// </description>
/// </item>
/// <item>
/// <description>
/// (<c>platform</c>, <c>linux/amd64</c>)
/// </description>
/// </item>
/// </list>
/// </remarks>
/// <param name="line">
/// The FROM statement arg string containing flags and optional values.
/// </param>
/// <returns>
/// A sequence of (<c>Name</c>, <c>Value</c>) tuples.
/// </returns>
/// <exception cref="FormatException">
/// Thrown if a quoted value is missing a closing quote.
/// </exception>
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 ParseArg(line.Substring(start, i - start));
}

start = i + 1;
}

if (quote != null)
{
throw new FormatException($"Unmatched {quote} quote in line: '{line}'.");
}
Comment thread
HofmeisterAn marked this conversation as resolved.

if (line.Length > start)
{
yield return ParseArg(line.Substring(start));
}
}

/// <summary>
/// Splits a single arg into flag name and an optional value.
/// </summary>
/// <param name="arg">A single arg, optionally containing an equals sign and value.</param>
/// <returns>A tuple containing the flag name and its value, or <c>null</c> if no value is specified.</returns>
private static (string Name, string Value) ParseArg(string arg)
{
var trimmed = arg.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().Trim('"', '\'');
return (name, value);
}
}
}
}
10 changes: 10 additions & 0 deletions src/Testcontainers/Images/FutureDockerImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ public string Digest
}
}

/// <inheritdoc />
public string Platform
{
get
{
ThrowIfResourceNotFound();
return _configuration.Image.Platform;
}
}

/// <inheritdoc />
public string FullName
{
Expand Down
10 changes: 10 additions & 0 deletions src/Testcontainers/Images/IImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public interface IImage
[CanBeNull]
string Digest { get; }

/// <summary>
/// Gets the platform.
/// </summary>
/// <remarks>
/// The supported format for a platform value is:
/// <c>&lt;os&gt;|&lt;arch&gt;|&lt;os&gt;/&lt;arch&gt;[/&lt;variant&gt;]</c>.
/// </remarks>
[CanBeNull]
string Platform { get; }

/// <summary>
/// Gets the full image name.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers/Images/IImageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
39 changes: 39 additions & 0 deletions src/Testcontainers/Images/Platform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace DotNet.Testcontainers.Images
{
using JetBrains.Annotations;

/// <summary>
/// Represents a container platform identifier.
/// </summary>
/// <remarks>
/// The supported format for a platform value is:
/// <c>&lt;os&gt;|&lt;arch&gt;|&lt;os&gt;/&lt;arch&gt;[/&lt;variant&gt;]</c>.
///
/// You can provide either the operating system or the architecture or both.
/// For more details, see <see href="https://github.com/containerd/platforms">containerd/platforms</see>.
/// </remarks>
[PublicAPI]
public readonly struct Platform
{
/// <summary>
/// Initializes a new instance of the <see cref="Platform" /> struct.
/// </summary>
/// <param name="value">The platform identifier.</param>
[PublicAPI]
public Platform(string value)
{
Value = value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// Gets the platform identifier.
/// </summary>
/// <remarks>
/// A string representing the container platform in <c>containerd/platforms</c> format, or
/// <c>null</c> if no platform was specified.
/// </remarks>
[PublicAPI]
[CanBeNull]
public string Value { get; }
}
}
Loading