Skip to content

Commit

Permalink
Merge branch 'develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
HofmeisterAn authored Aug 29, 2024
2 parents 0ffe192 + fffd384 commit 0fb255d
Show file tree
Hide file tree
Showing 20 changed files with 381 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .cake-scripts/version.cake
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#addin nuget:?package=Cake.Git&version=2.0.0
#addin nuget:?package=Cake.Git&version=3.0.0

internal sealed class BuildInformation
{
Expand Down
2 changes: 1 addition & 1 deletion build.cake
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#tool nuget:?package=dotnet-sonarscanner&version=5.15.0
#tool nuget:?package=dotnet-sonarscanner&version=7.1.1

#addin nuget:?package=Cake.Sonar&version=1.1.32

Expand Down
8 changes: 8 additions & 0 deletions docs/modules/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ public sealed class MongoDbContainerTest : IAsyncLifetime
```

To execute the tests, use the command `dotnet test` from a terminal.

## MongoDb Replica Set

By default, MongoDB runs as a standalone instance. If your tests require a MongoDB replica set, use the code below which will initialize it as a single-node replica set:

```csharp
MongoDbContainer _mongoDbContainer = new MongoDbBuilder().WithReplicaSet().Build();
```
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.200",
"rollForward": "latestPatch"
"rollForward": "latestMinor"
}
}
22 changes: 18 additions & 4 deletions src/Testcontainers.Keycloak/KeycloakBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public sealed class KeycloakBuilder : ContainerBuilder<KeycloakBuilder, Keycloak

public const ushort KeycloakPort = 8080;

public const ushort KeycloakHealthPort = 9000;

public const string DefaultUsername = "admin";

public const string DefaultPassword = "admin";
Expand Down Expand Up @@ -60,7 +62,20 @@ public KeycloakBuilder WithPassword(string password)
public override KeycloakContainer Build()
{
Validate();
return new KeycloakContainer(DockerResourceConfiguration);

Predicate<System.Version> predicate = v => v.Major >= 25;

var image = DockerResourceConfiguration.Image;

// https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints.
var isMajorVersionGreaterOrEqual25 = image.MatchLatestOrNightly() || image.MatchVersion(predicate);

var waitStrategy = Wait.ForUnixContainer()
.UntilHttpRequestIsSucceeded(request =>
request.ForPath("/health/ready").ForPort(isMajorVersionGreaterOrEqual25 ? KeycloakHealthPort : KeycloakPort));

var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy);
return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration);
}

/// <inheritdoc />
Expand All @@ -70,11 +85,10 @@ protected override KeycloakBuilder Init()
.WithImage(KeycloakImage)
.WithCommand("start-dev")
.WithPortBinding(KeycloakPort, true)
.WithPortBinding(KeycloakHealthPort, true)
.WithUsername(DefaultUsername)
.WithPassword(DefaultPassword)
.WithEnvironment("KC_HEALTH_ENABLED", "true")
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
request.ForPath("/health/ready").ForPort(KeycloakPort)));
.WithEnvironment("KC_HEALTH_ENABLED", "true");
}

/// <inheritdoc />
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Keycloak/Usings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global using System;
global using System.Linq;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers;
global using DotNet.Testcontainers.Builders;
Expand Down
75 changes: 69 additions & 6 deletions src/Testcontainers.MongoDb/MongoDbBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ public sealed class MongoDbBuilder : ContainerBuilder<MongoDbBuilder, MongoDbCon

public const string DefaultPassword = "mongo";

private const string InitKeyFileScriptFilePath = "/docker-entrypoint-initdb.d/01-init-keyfile.sh";

private const string KeyFileFilePath = "/tmp/mongodb-keyfile";

/// <summary>
/// Initializes a new instance of the <see cref="MongoDbBuilder" /> class.
/// </summary>
Expand Down Expand Up @@ -60,15 +64,44 @@ public MongoDbBuilder WithPassword(string password)
.WithEnvironment("MONGO_INITDB_ROOT_PASSWORD", initDbRootPassword);
}

/// <summary>
/// Initialize MongoDB as a single-node replica set.
/// </summary>
/// <param name="replicaSetName">The replica set name.</param>
/// <returns>A configured instance of <see cref="MongoDbBuilder" />.</returns>
public MongoDbBuilder WithReplicaSet(string replicaSetName = "rs0")
{
var initKeyFileScript = new StringWriter();
initKeyFileScript.NewLine = "\n";
initKeyFileScript.WriteLine("#!/bin/bash");
initKeyFileScript.WriteLine("openssl rand -base64 32 > \"" + KeyFileFilePath + "\"");
initKeyFileScript.WriteLine("chmod 600 \"" + KeyFileFilePath + "\"");

return Merge(DockerResourceConfiguration, new MongoDbConfiguration(replicaSetName: replicaSetName))
.WithCommand("--replSet", replicaSetName, "--keyFile", KeyFileFilePath, "--bind_ip_all")
.WithResourceMapping(Encoding.Default.GetBytes(initKeyFileScript.ToString()), InitKeyFileScriptFilePath, Unix.FileMode755);
}

/// <inheritdoc />
public override MongoDbContainer Build()
{
Validate();

// The wait strategy relies on the configuration of MongoDb. If credentials are
// provided, the log message "Waiting for connections" appears twice.
IWaitUntil waitUntil;

if (string.IsNullOrEmpty(DockerResourceConfiguration.ReplicaSetName))
{
// The wait strategy relies on the configuration of MongoDb. If credentials are
// provided, the log message "Waiting for connections" appears twice.
waitUntil = new WaitIndicateReadiness(DockerResourceConfiguration);
}
else
{
waitUntil = new WaitInitiateReplicaSet(DockerResourceConfiguration);
}

// If the user does not provide a custom waiting strategy, append the default MongoDb waiting strategy.
var mongoDbBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
var mongoDbBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(waitUntil));
return new MongoDbContainer(mongoDbBuilder.DockerResourceConfiguration);
}

Expand Down Expand Up @@ -118,17 +151,17 @@ protected override MongoDbBuilder Merge(MongoDbConfiguration oldValue, MongoDbCo
}

/// <inheritdoc cref="IWaitUntil" />
private sealed class WaitUntil : IWaitUntil
private sealed class WaitIndicateReadiness : IWaitUntil
{
private static readonly string[] LineEndings = { "\r\n", "\n" };

private readonly int _count;

/// <summary>
/// Initializes a new instance of the <see cref="WaitUntil" /> class.
/// Initializes a new instance of the <see cref="WaitIndicateReadiness" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WaitUntil(MongoDbConfiguration configuration)
public WaitIndicateReadiness(MongoDbConfiguration configuration)
{
_count = string.IsNullOrEmpty(configuration.Username) && string.IsNullOrEmpty(configuration.Password) ? 1 : 2;
}
Expand All @@ -145,4 +178,34 @@ public async Task<bool> UntilAsync(IContainer container)
.Count(line => line.Contains("Waiting for connections")));
}
}

/// <inheritdoc cref="IWaitUntil" />
private sealed class WaitInitiateReplicaSet : IWaitUntil
{
private readonly string _scriptContent;

/// <summary>
/// Initializes a new instance of the <see cref="WaitInitiateReplicaSet" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WaitInitiateReplicaSet(MongoDbConfiguration configuration)
{
_scriptContent = $"try{{rs.status().ok}}catch(e){{rs.initiate({{'_id':'{configuration.ReplicaSetName}',members:[{{'_id':1,'host':'127.0.0.1:27017'}}]}}).ok}}";
}

/// <inheritdoc />
public Task<bool> UntilAsync(IContainer container)
{
return UntilAsync(container as MongoDbContainer);
}

/// <inheritdoc cref="IWaitUntil.UntilAsync" />
private async Task<bool> UntilAsync(MongoDbContainer container)
{
var execResult = await container.ExecScriptAsync(_scriptContent)
.ConfigureAwait(false);

return 0L.Equals(execResult.ExitCode);
}
}
}
14 changes: 13 additions & 1 deletion src/Testcontainers.MongoDb/MongoDbConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ public sealed class MongoDbConfiguration : ContainerConfiguration
/// </summary>
/// <param name="username">The MongoDb username.</param>
/// <param name="password">The MongoDb password.</param>
/// <param name="replicaSetName">The replica set name.</param>
public MongoDbConfiguration(
string username = null,
string password = null)
string password = null,
string replicaSetName = null)
{
Username = username;
Password = password;
ReplicaSetName = replicaSetName;
}

/// <summary>
Expand Down Expand Up @@ -57,6 +60,7 @@ public MongoDbConfiguration(MongoDbConfiguration oldValue, MongoDbConfiguration
{
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
ReplicaSetName = BuildConfiguration.Combine(oldValue.ReplicaSetName, newValue.ReplicaSetName);
}

/// <summary>
Expand All @@ -68,4 +72,12 @@ public MongoDbConfiguration(MongoDbConfiguration oldValue, MongoDbConfiguration
/// Gets the MongoDb password.
/// </summary>
public string Password { get; }

/// <summary>
/// Gets the replica set name.
/// </summary>
/// <remarks>
/// If specified, the container will be started as a single-node replica set.
/// </remarks>
public string ReplicaSetName { get; }
}
19 changes: 10 additions & 9 deletions src/Testcontainers/Builders/CommonDirectoryPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ public static CommonDirectoryPath GetSolutionDirectory([CallerFilePath, NotNull]
}

/// <summary>
/// Resolves the first CSharp project file upwards the directory tree.
/// Resolves the first CSharp, FSharp or Visual Basic project file upwards the directory tree.
/// </summary>
/// <remarks>
/// Start node is the caller file path directory. End node is the root directory.
/// </remarks>
/// <param name="filePath">The caller file path.</param>
/// <returns>The first CSharp project file upwards the directory tree.</returns>
/// <exception cref="DirectoryNotFoundException">Thrown when the CSharp project file was not found upwards the directory tree.</exception>
/// <returns>The first CSharp, FSharp or Visual Basic project file upwards the directory tree.</returns>
/// <exception cref="DirectoryNotFoundException">Thrown when no CSharp, FSharp or Visual Basic project file was found upwards the directory tree.</exception>
[PublicAPI]
public static CommonDirectoryPath GetProjectDirectory([CallerFilePath, NotNull] string filePath = "")
{
return new CommonDirectoryPath(GetDirectoryPath(Path.GetDirectoryName(filePath), "*.csproj"));
return new CommonDirectoryPath(GetDirectoryPath(Path.GetDirectoryName(filePath), "*.csproj", "*.fsproj", "*.vbproj"));
}

/// <summary>
Expand All @@ -105,19 +105,20 @@ public static CommonDirectoryPath GetCallerFileDirectory([CallerFilePath, NotNul
return new CommonDirectoryPath(Path.GetDirectoryName(filePath));
}

private static string GetDirectoryPath(string path, string searchPattern)
private static string GetDirectoryPath(string path, params string[] searchPatterns)
{
return GetDirectoryPath(Directory.Exists(path) ? new DirectoryInfo(path) : null, searchPattern);
return GetDirectoryPath(Directory.Exists(path) ? new DirectoryInfo(path) : null, searchPatterns);
}

private static string GetDirectoryPath(DirectoryInfo path, string searchPattern)
private static string GetDirectoryPath(DirectoryInfo path, params string[] searchPatterns)
{
if (path != null)
{
return path.EnumerateFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly).Any() ? path.FullName : GetDirectoryPath(path.Parent, searchPattern);
var paths = searchPatterns.SelectMany(searchPattern => path.EnumerateFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly)).Any();
return paths ? path.FullName : GetDirectoryPath(path.Parent, searchPatterns);
}

var message = $"Cannot find '{searchPattern}' and resolve the base directory in the directory tree.";
var message = $"Cannot find '{string.Join(", ", searchPatterns)}' and resolve the base directory in the directory tree.";
throw new DirectoryNotFoundException(message);
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/Testcontainers/Clients/DockerApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,19 @@ await RuntimeInitialized.WaitAsync(ct)
runtimeInfo.AppendLine(dockerInfo.OperatingSystem);

runtimeInfo.Append(" Total Memory: ");
runtimeInfo.AppendFormat(CultureInfo.InvariantCulture, "{0:F} {1}", dockerInfo.MemTotal / Math.Pow(1024, byteUnits.Length), byteUnits[byteUnits.Length - 1]);
runtimeInfo.AppendLine(String.Format(CultureInfo.InvariantCulture, "{0:F} {1}", dockerInfo.MemTotal / Math.Pow(1024, byteUnits.Length), byteUnits[byteUnits.Length - 1]));

var labels = dockerInfo.Labels;
if (labels != null && labels.Count > 0)
{
runtimeInfo.AppendLine(" Labels: ");

foreach (var label in labels)
{
runtimeInfo.Append(" ");
runtimeInfo.AppendLine(label);
}
}
Logger.LogInformation("{RuntimeInfo}", runtimeInfo);
}
catch(Exception e)
Expand Down
35 changes: 35 additions & 0 deletions src/Testcontainers/Images/DockerImage.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace DotNet.Testcontainers.Images
{
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;

/// <inheritdoc cref="IImage" />
Expand All @@ -10,6 +12,8 @@ public sealed class DockerImage : IImage
{
private const string LatestTag = "latest";

private const string NightlyTag = "nightly";

private static readonly Func<string, IImage> GetDockerImage = MatchImage.Match;

private static readonly char[] TrimChars = { ' ', ':', '/' };
Expand Down Expand Up @@ -107,6 +111,37 @@ public DockerImage(
/// <inheritdoc />
public string GetHostname() => _lazyHostname.Value;

/// <inheritdoc />
public bool MatchLatestOrNightly()
{
return MatchVersion((string tag) => LatestTag.Equals(tag) || NightlyTag.Equals(tag));
}

/// <inheritdoc />
public bool MatchVersion(Predicate<string> predicate)
{
return predicate(Tag);
}

/// <inheritdoc />
public bool MatchVersion(Predicate<Version> predicate)
{
var versionMatch = Regex.Match(Tag, "^(\\d+)(\\.\\d+)?(\\.\\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1));

if (!versionMatch.Success)
{
return false;
}

if (Version.TryParse(versionMatch.Value, out var version))
{
return predicate(version);
}

// If the Regex matches and Version.TryParse(string?, out Version?) fails then it means it is a major version only (i.e. without any dot separator)
return predicate(new Version(int.Parse(versionMatch.Groups[1].Value, NumberStyles.None), 0));
}

private static string TrimOrDefault(string value, string defaultValue = default)
{
return string.IsNullOrEmpty(value) ? defaultValue : value.Trim(TrimChars);
Expand Down
19 changes: 19 additions & 0 deletions src/Testcontainers/Images/FutureDockerImage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace DotNet.Testcontainers.Images
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
Expand Down Expand Up @@ -74,6 +75,24 @@ public string GetHostname()
return _configuration.Image.GetHostname();
}

/// <inheritdoc />
public bool MatchLatestOrNightly()
{
return _configuration.Image.MatchLatestOrNightly();
}

/// <inheritdoc />
public bool MatchVersion(Predicate<string> predicate)
{
return _configuration.Image.MatchVersion(predicate);
}

/// <inheritdoc />
public bool MatchVersion(Predicate<System.Version> predicate)
{
return _configuration.Image.MatchVersion(predicate);
}

/// <inheritdoc />
public async Task CreateAsync(CancellationToken ct = default)
{
Expand Down
Loading

0 comments on commit 0fb255d

Please sign in to comment.