Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add reuse support #1051

Merged
Merged
Show file tree
Hide file tree
Changes from 9 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
8 changes: 8 additions & 0 deletions src/Testcontainers/Builders/AbstractBuilder`4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public TBuilderEntity WithCleanUp(bool cleanUp)
return WithResourceReaperSessionId(TestcontainersSettings.ResourceReaperEnabled && cleanUp ? ResourceReaper.DefaultSessionId : Guid.Empty);
}

/// <inheritdoc />
public TBuilderEntity WithReuse(bool reuse)
{
return Clone(new ResourceConfiguration<TCreateResourceEntity>(reuse: reuse));
}

/// <inheritdoc />
public TBuilderEntity WithLabel(string name, string value)
{
Expand Down Expand Up @@ -129,6 +135,8 @@ protected virtual void Validate()
const string message = "Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured. You can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. For more information, visit:\nhttps://dotnet.testcontainers.org/custom_configuration/";
_ = Guard.Argument(DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IResourceConfiguration<TCreateResourceEntity>.DockerEndpointAuthConfig))
.ThrowIf(argument => argument.Value == null, argument => new ArgumentException(message, argument.Name));

// TODO: Validate WithReuse(), WithAutoRemove() and WithCleanUp() combinations.
}

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Testcontainers/Builders/IAbstractBuilder`3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ public interface IAbstractBuilder<out TBuilderEntity, out TResourceEntity, out T
[PublicAPI]
TBuilderEntity WithCleanUp(bool cleanUp);

/// <summary>
/// Reuses an existing Docker resource.
/// </summary>
/// <remarks>
/// If reuse is enabled, Testcontainers will label the resource with a hash value
/// according to the respective build/resource configuration. When Testcontainers finds a
/// matching resource, it will reuse this resource instead of creating a new one. Enabling
/// reuse will disable the resource reaper, meaning the resource will not be cleaned up
/// after the tests are finished.
/// </remarks>
/// <param name="reuse">Determines whether to reuse an existing resource configuration or not.</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
[PublicAPI]
TBuilderEntity WithReuse(bool reuse);

/// <summary>
/// Adds user-defined metadata to the Docker resource.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Testcontainers/Clients/DockerContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ public async Task<string> RunAsync(IContainerConfiguration configuration, Cancel
NetworkingConfig = networkingConfig,
};

if (configuration.Reuse.HasValue && configuration.Reuse.Value)
{
createParameters.Labels.Add(TestcontainersClient.TestcontainersReuseHashLabel, configuration.GetReuseHash());
}

if (configuration.ParameterModifiers != null)
{
foreach (var parameterModifier in configuration.ParameterModifiers)
Expand Down
5 changes: 5 additions & 0 deletions src/Testcontainers/Clients/DockerNetworkOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public async Task<string> CreateAsync(INetworkConfiguration configuration, Cance
Labels = configuration.Labels.ToDictionary(item => item.Key, item => item.Value),
};

if (configuration.Reuse.HasValue && configuration.Reuse.Value)
{
createParameters.Labels.Add(TestcontainersClient.TestcontainersReuseHashLabel, configuration.GetReuseHash());
}

if (configuration.ParameterModifiers != null)
{
foreach (var parameterModifier in configuration.ParameterModifiers)
Expand Down
5 changes: 5 additions & 0 deletions src/Testcontainers/Clients/DockerVolumeOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public async Task<string> CreateAsync(IVolumeConfiguration configuration, Cancel
Labels = configuration.Labels.ToDictionary(item => item.Key, item => item.Value),
};

if (configuration.Reuse.HasValue && configuration.Reuse.Value)
{
createParameters.Labels.Add(TestcontainersClient.TestcontainersReuseHashLabel, configuration.GetReuseHash());
}

if (configuration.ParameterModifiers != null)
{
foreach (var parameterModifier in configuration.ParameterModifiers)
Expand Down
26 changes: 25 additions & 1 deletion src/Testcontainers/Clients/FilterByProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ namespace DotNet.Testcontainers.Clients
{
using System.Collections.Concurrent;
using System.Collections.Generic;
using DotNet.Testcontainers.Configurations;

internal sealed class FilterByProperty : ConcurrentDictionary<string, IDictionary<string, bool>>
public class FilterByProperty : ConcurrentDictionary<string, IDictionary<string, bool>>
{
public FilterByProperty Add(string property, string value)
{
Expand All @@ -12,4 +13,27 @@ public FilterByProperty Add(string property, string value)
return this;
}
}

public sealed class FilterByReuseHash : FilterByProperty
{
public FilterByReuseHash(IContainerConfiguration resourceConfiguration)
: this(resourceConfiguration.GetReuseHash())
{
}

public FilterByReuseHash(INetworkConfiguration resourceConfiguration)
: this(resourceConfiguration.GetReuseHash())
{
}

public FilterByReuseHash(IVolumeConfiguration resourceConfiguration)
: this(resourceConfiguration.GetReuseHash())
{
}

private FilterByReuseHash(string hash)
{
Add("label", string.Join("=", TestcontainersClient.TestcontainersReuseHashLabel, hash));
}
}
}
2 changes: 2 additions & 0 deletions src/Testcontainers/Clients/TestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal sealed class TestcontainersClient : ITestcontainersClient

public const string TestcontainersSessionIdLabel = TestcontainersLabel + ".session-id";

public const string TestcontainersReuseHashLabel = TestcontainersLabel + ".reuse-hash";

public static readonly string Version = typeof(TestcontainersClient).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;

private static readonly string OSRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public interface IResourceConfiguration<in TCreateResourceEntity>
/// </summary>
Guid SessionId { get; }

/// <summary>
/// Gets a value indicating whether to reuse an existing resource configuration or not.
/// </summary>
bool? Reuse { get; }

/// <summary>
/// Gets the Docker endpoint authentication configuration.
/// </summary>
Expand All @@ -30,5 +35,10 @@ public interface IResourceConfiguration<in TCreateResourceEntity>
/// Gets a list of low level modifications of the Docker.DotNet entity.
/// </summary>
IReadOnlyList<Action<TCreateResourceEntity>> ParameterModifiers { get; }

/// <summary>
/// Gets the reuse hash.
/// </summary>
string GetReuseHash();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using DotNet.Testcontainers.Clients;
using DotNet.Testcontainers.Containers;

internal sealed class JsonIgnoreRuntimeResourceLabels : JsonConverter<IReadOnlyDictionary<string, string>>
{
private static readonly ISet<string> IgnoreLabels = new HashSet<string> { ResourceReaper.ResourceReaperSessionLabel, TestcontainersClient.TestcontainersVersionLabel, TestcontainersClient.TestcontainersSessionIdLabel };

public override bool CanConvert(Type typeToConvert)
{
return typeof(IEnumerable<KeyValuePair<string, string>>).IsAssignableFrom(typeToConvert);
}

public override IReadOnlyDictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<IReadOnlyDictionary<string, string>>(ref reader);
}

public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<string, string> value, JsonSerializerOptions options)
{
var labels = value.Where(label => !IgnoreLabels.Contains(label.Key)).ToDictionary(label => label.Key, label => label.Value);

writer.WriteStartObject();

foreach (var label in labels)
{
writer.WriteString(label.Key, label.Value);
}

writer.WriteEndObject();
}
}
}
30 changes: 28 additions & 2 deletions src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;
Expand All @@ -16,15 +19,18 @@ public class ResourceConfiguration<TCreateResourceEntity> : IResourceConfigurati
/// <param name="dockerEndpointAuthenticationConfiguration">The Docker endpoint authentication configuration.</param>
/// <param name="labels">The test session id.</param>
/// <param name="parameterModifiers">A list of low level modifications of the Docker.DotNet entity.</param>
/// <param name="reuse">A value indicating whether to reuse an existing resource configuration or not.</param>
public ResourceConfiguration(
IDockerEndpointAuthenticationConfiguration dockerEndpointAuthenticationConfiguration = null,
IReadOnlyDictionary<string, string> labels = null,
IReadOnlyList<Action<TCreateResourceEntity>> parameterModifiers = null)
IReadOnlyList<Action<TCreateResourceEntity>> parameterModifiers = null,
bool? reuse = null)
{
SessionId = labels != null && labels.TryGetValue(ResourceReaper.ResourceReaperSessionLabel, out var resourceReaperSessionId) && Guid.TryParseExact(resourceReaperSessionId, "D", out var sessionId) ? sessionId : Guid.Empty;
DockerEndpointAuthConfig = dockerEndpointAuthenticationConfiguration;
Labels = labels;
ParameterModifiers = parameterModifiers;
Reuse = reuse;
}

/// <summary>
Expand All @@ -44,21 +50,41 @@ protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> re
protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> oldValue, IResourceConfiguration<TCreateResourceEntity> newValue)
: this(
dockerEndpointAuthenticationConfiguration: BuildConfiguration.Combine(oldValue.DockerEndpointAuthConfig, newValue.DockerEndpointAuthConfig),
labels: BuildConfiguration.Combine(oldValue.Labels, newValue.Labels),
parameterModifiers: BuildConfiguration.Combine(oldValue.ParameterModifiers, newValue.ParameterModifiers),
labels: BuildConfiguration.Combine(oldValue.Labels, newValue.Labels))
reuse: (oldValue.Reuse.HasValue && oldValue.Reuse.Value) || (newValue.Reuse.HasValue && newValue.Reuse.Value))
{
}

/// <inheritdoc />
[JsonIgnore]
public Guid SessionId { get; }

/// <inheritdoc />
[JsonIgnore]
public bool? Reuse { get; }

/// <inheritdoc />
[JsonIgnore]
public IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig { get; }

/// <inheritdoc />
[JsonConverter(typeof(JsonIgnoreRuntimeResourceLabels))]
public IReadOnlyDictionary<string, string> Labels { get; }

/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<Action<TCreateResourceEntity>> ParameterModifiers { get; }

/// <inheritdoc />
public virtual string GetReuseHash()
{
var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType());

using (var sha1 = SHA1.Create())
{
return Convert.ToBase64String(sha1.ComputeHash(jsonUtf8Bytes));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
Expand Down Expand Up @@ -139,27 +140,34 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig
}

/// <inheritdoc />
[JsonIgnore]
public bool? AutoRemove { get; }

/// <inheritdoc />
[JsonIgnore]
public bool? Privileged { get; }

/// <inheritdoc />
public IImage Image { get; }

/// <inheritdoc />
[JsonIgnore]
public Func<ImageInspectResponse, bool> ImagePullPolicy { get; }

/// <inheritdoc />
[JsonIgnore]
public string Name { get; }

/// <inheritdoc />
[JsonIgnore]
public string Hostname { get; }

/// <inheritdoc />
[JsonIgnore]
public string MacAddress { get; }

/// <inheritdoc />
[JsonIgnore]
public string WorkingDirectory { get; }

/// <inheritdoc />
Expand All @@ -169,39 +177,51 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig
public IEnumerable<string> Command { get; }

/// <inheritdoc />
[JsonIgnore]
public IReadOnlyDictionary<string, string> Environments { get; }

/// <inheritdoc />
[JsonIgnore]
public IReadOnlyDictionary<string, string> ExposedPorts { get; }

/// <inheritdoc />
[JsonIgnore]
public IReadOnlyDictionary<string, string> PortBindings { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<IResourceMapping> ResourceMappings { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<IContainer> Containers { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<IMount> Mounts { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<INetwork> Networks { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<string> NetworkAliases { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<string> ExtraHosts { get; }

/// <inheritdoc />
[JsonIgnore]
public IOutputConsumer OutputConsumer { get; }

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<IWaitUntil> WaitStrategies { get; }

/// <inheritdoc />
[JsonIgnore]
public Func<IContainer, CancellationToken, Task> StartupCallback { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Images;
Expand Down Expand Up @@ -71,21 +72,27 @@ public ImageFromDockerfileConfiguration(IImageFromDockerfileConfiguration oldVal
}

/// <inheritdoc />
[JsonIgnore]
public bool? DeleteIfExists { get; }

/// <inheritdoc />
[JsonIgnore]
public string Dockerfile { get; }

/// <inheritdoc />
[JsonIgnore]
public string DockerfileDirectory { get; }

/// <inheritdoc />
[JsonIgnore]
public IImage Image { get; }

/// <inheritdoc />
[JsonIgnore]
public Func<ImageInspectResponse, bool> ImageBuildPolicy { get; }

/// <inheritdoc />
[JsonIgnore]
public IReadOnlyDictionary<string, string> BuildArguments { get; }
}
}
Loading