Skip to content

Commit

Permalink
[testcontainers#466] #IMPLEMENT 'assemblyName: DotNet.Testcontainers;…
Browse files Browse the repository at this point in the history
… function: Image Name Substitution'
  • Loading branch information
bohlenc committed Jun 1, 2022
1 parent ba7d87b commit 174ab93
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 1 deletion.
22 changes: 22 additions & 0 deletions src/DotNet.Testcontainers/Builders/IImageNameSubstitutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace DotNet.Testcontainers.Builders
{
using DotNet.Testcontainers.Images;

/// <summary>
/// Substitutes the fullname of a <see cref="IDockerImage"/>.
/// The nature of the substitution is up to the implementation.
/// </summary>
public interface IImageNameSubstitutor
{
/// <summary>
/// Applies a substitution to the fullname of an image.
/// </summary>
/// <param name="originalImage">
/// The image to modify.
/// </param>
/// <returns>
/// The modified image.
/// </returns>
IDockerImage ApplyTo(IDockerImage originalImage);
}
}
26 changes: 26 additions & 0 deletions src/DotNet.Testcontainers/Builders/ImageNameSubstitutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace DotNet.Testcontainers.Builders
{
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Images;

public class ImageNameSubstitutor
{
public static IImageNameSubstitutor Create()
{
if (!string.IsNullOrWhiteSpace(TestcontainersSettings.DockerHubImagePrefix))
{
return new PrefixingImageNameSubstitutor(TestcontainersSettings.DockerHubImagePrefix);
}

return new NoopImageSubstitutor();
}

public class NoopImageSubstitutor : IImageNameSubstitutor
{
public IDockerImage ApplyTo(IDockerImage originalImage)
{
return originalImage;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace DotNet.Testcontainers.Builders
{
using DotNet.Testcontainers.Images;

/// <summary>
/// Prepends a prefix to the fullname of a Docker image.
/// Applies only to images hosted on Docker Hub.
/// </summary>
/// <example>
/// With a configured prefix of "my.proxy.com":
/// docker:latest -> my.proxy.com/docker:latest
/// my.azurecr.io/docker:latest -> my.azurecr.io/docker:latest
/// </example>
public class PrefixingImageNameSubstitutor : IImageNameSubstitutor
{
/// <summary>
/// The prefix to prepend to the fullname of the image.
/// </summary>
private readonly string prefix;

public PrefixingImageNameSubstitutor(string prefix)
{
Guard.Argument(prefix, nameof(prefix))
.NotNull()
.NotWhitespace();

this.prefix = prefix;
}

/// <inheritdoc />>
public IDockerImage ApplyTo(IDockerImage originalImage)
{
var imageHostedOnDockerHub = originalImage.GetHostname() == null;
if (!imageHostedOnDockerHub)
{
return originalImage;
}

return new DockerImage(originalImage.Repository.Length == 0 ? this.prefix : $"{this.prefix}/{originalImage.Repository}", originalImage.Name, originalImage.Tag);
}
}
}
29 changes: 28 additions & 1 deletion src/DotNet.Testcontainers/Builders/TestcontainersBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,12 @@ public TDockerContainer Build()
Guard.Argument(this.DockerResourceConfiguration.Image, nameof(ITestcontainersConfiguration.Image))
.NotNull();

var finalizedDockerResourceConfiguration = this.FinalizeConfiguration();

#pragma warning disable S3011

// Create container instance.
var container = (TDockerContainer)Activator.CreateInstance(typeof(TDockerContainer), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.DockerResourceConfiguration, TestcontainersSettings.Logger }, null);
var container = (TDockerContainer)Activator.CreateInstance(typeof(TDockerContainer), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { finalizedDockerResourceConfiguration, TestcontainersSettings.Logger }, null);

#pragma warning restore S3011

Expand All @@ -289,6 +291,31 @@ public TDockerContainer Build()
return container;
}

private ITestcontainersConfiguration FinalizeConfiguration()
{
var substitutedImageName = ImageNameSubstitutor.Create().ApplyTo(this.DockerResourceConfiguration.Image);
return new TestcontainersConfiguration(
this.DockerResourceConfiguration.Endpoint,
this.DockerResourceConfiguration.DockerRegistryAuthConfig,
substitutedImageName,
this.DockerResourceConfiguration.Name,
this.DockerResourceConfiguration.Hostname,
this.DockerResourceConfiguration.WorkingDirectory,
this.DockerResourceConfiguration.Entrypoint,
this.DockerResourceConfiguration.Command,
this.DockerResourceConfiguration.Environments,
this.DockerResourceConfiguration.Labels,
this.DockerResourceConfiguration.ExposedPorts,
this.DockerResourceConfiguration.PortBindings,
this.DockerResourceConfiguration.Mounts,
this.DockerResourceConfiguration.Networks,
this.DockerResourceConfiguration.OutputConsumer,
this.DockerResourceConfiguration.WaitStrategies,
this.DockerResourceConfiguration.StartupCallback,
this.DockerResourceConfiguration.AutoRemove,
this.DockerResourceConfiguration.Privileged);
}

#pragma warning disable S107

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace DotNet.Testcontainers.Configurations
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Builders;
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
using DotNet.Testcontainers.Networks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public static class TestcontainersSettings
public static bool ResourceReaperEnabled { get; set; }
= true;

/// <summary>
/// Gets or sets a prefix to prepend to image names for images hosted on DockerHub.
/// </summary>
public static string DockerHubImagePrefix { get; set; }

/// <summary>
/// Gets or sets the logger.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public string Hostname
}
}

/// <inheritdoc />
public string ImageName
{
get
{
return this.configuration.Image.FullName;
}
}

/// <inheritdoc />
public TestcontainersState State
{
Expand Down
17 changes: 17 additions & 0 deletions src/DotNet.Testcontainers/Guard.String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,22 @@ public static ref readonly ArgumentInfo<string> NotEmpty(in this ArgumentInfo<st

return ref argument;
}

/// <summary>
/// Ensure that an argument string value is not all whitespace.
/// </summary>
/// <param name="argument">String argument to validate.</param>
/// <returns>Reference to the Guard object that validates the argument preconditions.</returns>
/// <exception cref="ArgumentException">Thrown when argument is all whitespace.</exception>
[DebuggerStepThrough]
public static ref readonly ArgumentInfo<string> NotWhitespace(in this ArgumentInfo<string> argument)
{
if (argument.Value.Trim().Length == 0)
{
throw new ArgumentException($"{argument.Name} can not be all whitespace.", argument.Name);
}

return ref argument;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace DotNet.Testcontainers.Tests.Unit.Builders;

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using Xunit;

public class ImageNameSubstitutorTest
{
[Fact]
public void CreatesNoopImageNameSubstitutorWhenNothingElseIsConfigured()
{
var substitutor = ImageNameSubstitutor.Create();

Assert.IsType<ImageNameSubstitutor.NoopImageSubstitutor>(substitutor);
}

[Fact]
public void CreatesPrefixingImageNameSubstitutorWhenDockerHubImagePrefixIsConfigured()
{
TestcontainersSettings.DockerHubImagePrefix = "my.proxy.com";

var substitutor = ImageNameSubstitutor.Create();

Assert.IsType<PrefixingImageNameSubstitutor>(substitutor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace DotNet.Testcontainers.Tests.Unit.Builders;

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Images;
using Xunit;

public class PrefixingImageNameSubstitutorTest
{

[Theory]
[InlineData("my.proxy.com", "bar", "my.proxy.com/bar:latest")]
[InlineData("my.proxy.com", "bar:latest", "my.proxy.com/bar:latest")]
[InlineData("my.proxy.com", "bar:1.0.0", "my.proxy.com/bar:1.0.0")]
[InlineData("my.proxy.com", "foo/bar:1.0.0", "my.proxy.com/foo/bar:1.0.0")]
[InlineData("my.proxy.com:443", "foo/bar:1.0.0", "my.proxy.com:443/foo/bar:1.0.0")]
[InlineData("my.proxy.com", "myregistry.azurecr.io/foo/bar:1.0.0", "myregistry.azurecr.io/foo/bar:1.0.0")]
[InlineData("my.proxy.com", "myregistry.azurecr.io:443/foo/bar:1.0.0", "myregistry.azurecr.io:443/foo/bar:1.0.0")]
public void ShouldAddPrefixForDockerHubImages(string prefix, string originalImageFullName, string expectedFullName)
{
var substitutor = new PrefixingImageNameSubstitutor(prefix);

var originalImage = new DockerImage(originalImageFullName);

var actual = substitutor.ApplyTo(originalImage);

Assert.Equal(expectedFullName, actual.FullName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DotNet.Testcontainers.Tests.Unit.Builders;

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using Xunit;

public class TestcontainerBuilderTest
{
[Fact]
public void BuildAppliesDoesNotChangeImageNameWhenNoSubstitutorIsConfigured()
{
TestcontainersSettings.DockerHubImagePrefix = null;

var container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("foo/bar:1.0.0")
.Build();

Assert.Equal("foo/bar:1.0.0", container.ImageName);
}

[Fact]
public void BuildAppliesPrexifingImageNameSubstitutorWhenDockerHubImagePrefixIsConfigured()
{
TestcontainersSettings.DockerHubImagePrefix = "my.proxy.com";

var container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("foo/bar:1.0.0")
.Build();

Assert.Equal("my.proxy.com/foo/bar:1.0.0", container.ImageName);
}
}
13 changes: 13 additions & 0 deletions tests/DotNet.Testcontainers.Tests/Unit/GuardTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ public void IfNotEmpty()
var exception = Record.Exception(() => Guard.Argument("Not Empty", nameof(this.IfNotEmpty)).NotEmpty());
Assert.Null(exception);
}

[Fact]
public void IfNotWhitespace()
{
var exception = Record.Exception(() => Guard.Argument("Not All Whitespace", nameof(this.IfNotWhitespace)).NotWhitespace());
Assert.Null(exception);
}
}

public sealed class ThrowArgumentException
Expand All @@ -75,6 +82,12 @@ public void IfNotEmpty()
{
Assert.Throws<ArgumentException>(() => Guard.Argument("Not Empty", nameof(this.IfNotEmpty)).Empty());
}

[Fact]
public void IfNotWhitespace()
{
Assert.Throws<ArgumentException>(() => Guard.Argument(" ", nameof(this.IfNotWhitespace)).NotWhitespace());
}
}
}
}
Expand Down

0 comments on commit 174ab93

Please sign in to comment.