Skip to content

Commit 897ba81

Browse files
authored
Merge branch 'develop' into ssl-config-dotnet
2 parents dab0069 + 1cfac50 commit 897ba81

File tree

21 files changed

+535
-61
lines changed

21 files changed

+535
-61
lines changed

docs/api/wait_strategies.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ _ = Wait.ForUnixContainer()
77
.UntilInternalTcpPortIsAvailable(80)
88
.UntilFileExists("/tmp/foo")
99
.UntilFileExists("/tmp/bar")
10-
.UntilOperationIsSucceeded(() => true, 1)
1110
.AddCustomWaitStrategy(new MyCustomWaitStrategy());
1211
```
1312

@@ -20,6 +19,17 @@ _ = Wait.ForUnixContainer()
2019

2120
Besides configuring the wait strategy, cancelling a container start can always be done utilizing a [CancellationToken](create_docker_container.md#canceling-a-container-start).
2221

22+
## Wait strategy modes
23+
24+
Wait strategy modes define how Testcontainers for .NET handles container readiness checks. By default, wait strategies assume the container remains running throughout the startup. If a container exits unexpectedly during startup, Testcontainers for .NET will throw a `ContainerNotRunningException` containing the exit code and logs.
25+
26+
Some containers are intended to stop after completing short-lived tasks like migrations or setup scripts. In these cases, the container exit is expected, not a failure. Use `WaitStrategyMode.OneShot` to treat a normal exit as successful rather than throwing an exception.
27+
28+
```csharp
29+
_ = Wait.ForUnixContainer()
30+
.UntilMessageIsLogged("Migration completed", o => o.WithMode(WaitStrategyMode.OneShot));
31+
```
32+
2333
## Wait until an HTTP(S) endpoint is available
2434

2535
You can choose to wait for an HTTP(S) endpoint to return a particular HTTP response status code or to match a predicate. The default configuration tries to access the HTTP endpoint running inside the container. Chose `ForPort(ushort)` or `ForPath(string)` to adjust the endpoint or `UsingTls()` to switch to HTTPS. When using `UsingTls()` port 443 is used as a default. If your container exposes a different HTTPS port, make sure that the correct waiting port is configured accordingly.

docs/custom_configuration/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ Once configured, Testcontainers will rewrite Docker Hub image names by adding th
8181
For example, the image:
8282

8383
```
84-
testcontainers/helloworld:1.2.0
84+
testcontainers/helloworld:1.3.0
8585
```
8686

8787
will automatically become:
8888

8989
```
90-
registry.mycompany.com/mirror/testcontainers/helloworld:1.2.0
90+
registry.mycompany.com/mirror/testcontainers/helloworld:1.3.0
9191
```
9292

9393
## Enable logging

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ dotnet add package Testcontainers
77
```csharp title="Run the Hello World container"
88
// Create a new instance of a container.
99
var container = new ContainerBuilder()
10-
// Set the image for the container to "testcontainers/helloworld:1.2.0".
11-
.WithImage("testcontainers/helloworld:1.2.0")
10+
// Set the image for the container to "testcontainers/helloworld:1.3.0".
11+
.WithImage("testcontainers/helloworld:1.3.0")
1212
// Bind port 8080 of the container to a random port on the host.
1313
.WithPortBinding(8080, true)
1414
// Wait until the HTTP endpoint of the container is available.

src/Testcontainers.MongoDb/MongoDbBuilder.cs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public MongoDbBuilder WithPassword(string password)
6565
}
6666

6767
/// <summary>
68-
/// Initialize MongoDB as a single-node replica set.
68+
/// Initializes MongoDB as a single-node replica set.
6969
/// </summary>
7070
/// <param name="replicaSetName">The replica set name.</param>
7171
/// <returns>A configured instance of <see cref="MongoDbBuilder" />.</returns>
@@ -97,7 +97,7 @@ public override MongoDbContainer Build()
9797
}
9898
else
9999
{
100-
waitUntil = new WaitInitiateReplicaSet(DockerResourceConfiguration);
100+
waitUntil = new WaitInitiateReplicaSet();
101101
}
102102

103103
// If the user does not provide a custom waiting strategy, append the default MongoDb waiting strategy.
@@ -112,7 +112,8 @@ protected override MongoDbBuilder Init()
112112
.WithImage(MongoDbImage)
113113
.WithPortBinding(MongoDbPort, true)
114114
.WithUsername(DefaultUsername)
115-
.WithPassword(DefaultPassword);
115+
.WithPassword(DefaultPassword)
116+
.WithStartupCallback(InitiateReplicaSetAsync);
116117
}
117118

118119
/// <inheritdoc />
@@ -150,6 +151,38 @@ protected override MongoDbBuilder Merge(MongoDbConfiguration oldValue, MongoDbCo
150151
return new MongoDbBuilder(new MongoDbConfiguration(oldValue, newValue));
151152
}
152153

154+
/// <summary>
155+
/// Initiates the MongoDB replica set.
156+
/// </summary>
157+
/// <param name="container">The container instance.</param>
158+
/// <param name="configuration">The container configuration.</param>
159+
/// <param name="ct">Cancellation token.</param>
160+
/// <returns>Task that completes when the replica set initiation has been executed.</returns>
161+
private static async Task InitiateReplicaSetAsync(MongoDbContainer container, MongoDbConfiguration configuration, CancellationToken ct)
162+
{
163+
if (string.IsNullOrEmpty(configuration.ReplicaSetName))
164+
{
165+
return;
166+
}
167+
168+
// This is a simple workaround to use the default options, which can be configured
169+
// with custom configurations as needed.
170+
var options = new WaitStrategy();
171+
172+
var scriptContent = $"var r=rs.initiate({{_id:\"{configuration.ReplicaSetName}\",members:[{{_id:0,host:\"127.0.0.1:27017\"}}]}});quit(r.ok===1?0:1);";
173+
174+
var initiate = async () =>
175+
{
176+
var execResult = await container.ExecScriptAsync(scriptContent, ct)
177+
.ConfigureAwait(false);
178+
179+
return 0L.Equals(execResult.ExitCode);
180+
};
181+
182+
await WaitStrategy.WaitUntilAsync(initiate, options.Interval, options.Timeout, options.Retries, ct)
183+
.ConfigureAwait(false);
184+
}
185+
153186
/// <inheritdoc cref="IWaitUntil" />
154187
private sealed class WaitIndicateReadiness : IWaitUntil
155188
{
@@ -182,16 +215,7 @@ public async Task<bool> UntilAsync(IContainer container)
182215
/// <inheritdoc cref="IWaitUntil" />
183216
private sealed class WaitInitiateReplicaSet : IWaitUntil
184217
{
185-
private readonly string _scriptContent;
186-
187-
/// <summary>
188-
/// Initializes a new instance of the <see cref="WaitInitiateReplicaSet" /> class.
189-
/// </summary>
190-
/// <param name="configuration">The container configuration.</param>
191-
public WaitInitiateReplicaSet(MongoDbConfiguration configuration)
192-
{
193-
_scriptContent = $"try{{rs.status()}}catch(e){{rs.initiate({{_id:'{configuration.ReplicaSetName}',members:[{{_id:0,host:'127.0.0.1:27017'}}]}});throw e;}}";
194-
}
218+
private const string ScriptContent = "var r=db.runCommand({hello:1}).isWritablePrimary;quit(r===true?0:1);";
195219

196220
/// <inheritdoc />
197221
public Task<bool> UntilAsync(IContainer container)
@@ -200,9 +224,9 @@ public Task<bool> UntilAsync(IContainer container)
200224
}
201225

202226
/// <inheritdoc cref="IWaitUntil.UntilAsync" />
203-
private async Task<bool> UntilAsync(MongoDbContainer container)
227+
private static async Task<bool> UntilAsync(MongoDbContainer container)
204228
{
205-
var execResult = await container.ExecScriptAsync(_scriptContent)
229+
var execResult = await container.ExecScriptAsync(ScriptContent)
206230
.ConfigureAwait(false);
207231

208232
return 0L.Equals(execResult.ExitCode);

src/Testcontainers.Papercut/PapercutBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ namespace Testcontainers.Papercut;
44
[PublicAPI]
55
public sealed class PapercutBuilder : ContainerBuilder<PapercutBuilder, PapercutContainer, PapercutConfiguration>
66
{
7-
public const string PapercutImage = "changemakerstudiosus/papercut-smtp:latest";
7+
public const string PapercutImage = "changemakerstudiosus/papercut-smtp:7.0";
88

9-
public const ushort SmtpPort = 25;
9+
public const ushort SmtpPort = 2525;
1010

11-
public const ushort HttpPort = 80;
11+
public const ushort HttpPort = 8080;
1212

1313
/// <summary>
1414
/// Initializes a new instance of the <see cref="PapercutBuilder" /> class.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Text.Json;
4+
5+
internal static class DefaultJsonSerializerOptions
6+
{
7+
static DefaultJsonSerializerOptions()
8+
{
9+
Instance.Converters.Add(new JsonOrderedKeysConverter());
10+
}
11+
12+
public static JsonSerializerOptions Instance { get; }
13+
= new JsonSerializerOptions();
14+
}
15+
}
Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,24 @@
11
namespace DotNet.Testcontainers.Configurations
22
{
3-
using System;
43
using System.Collections.Generic;
54
using System.Linq;
65
using System.Text.Json;
7-
using System.Text.Json.Serialization;
86
using DotNet.Testcontainers.Clients;
97
using DotNet.Testcontainers.Containers;
108

11-
internal sealed class JsonIgnoreRuntimeResourceLabels : JsonConverter<IReadOnlyDictionary<string, string>>
9+
internal sealed class JsonIgnoreRuntimeResourceLabels : JsonOrderedKeysConverter
1210
{
13-
private static readonly ISet<string> IgnoreLabels = new HashSet<string> { ResourceReaper.ResourceReaperSessionLabel, TestcontainersClient.TestcontainersVersionLabel, TestcontainersClient.TestcontainersSessionIdLabel };
14-
15-
public override bool CanConvert(Type typeToConvert)
16-
{
17-
return typeof(IEnumerable<KeyValuePair<string, string>>).IsAssignableFrom(typeToConvert);
18-
}
19-
20-
public override IReadOnlyDictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11+
private static readonly ISet<string> IgnoreLabels = new HashSet<string>
2112
{
22-
return JsonSerializer.Deserialize<IReadOnlyDictionary<string, string>>(ref reader);
23-
}
13+
ResourceReaper.ResourceReaperSessionLabel,
14+
TestcontainersClient.TestcontainersVersionLabel,
15+
TestcontainersClient.TestcontainersSessionIdLabel,
16+
};
2417

2518
public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<string, string> value, JsonSerializerOptions options)
2619
{
2720
var labels = value.Where(label => !IgnoreLabels.Contains(label.Key)).ToDictionary(label => label.Key, label => label.Value);
28-
29-
writer.WriteStartObject();
30-
31-
foreach (var label in labels)
32-
{
33-
writer.WriteString(label.Key, label.Value);
34-
}
35-
36-
writer.WriteEndObject();
21+
base.Write(writer, labels, options);
3722
}
3823
}
3924
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
9+
internal class JsonOrderedKeysConverter : JsonConverter<IReadOnlyDictionary<string, string>>
10+
{
11+
public override bool CanConvert(Type typeToConvert)
12+
{
13+
return typeof(IEnumerable<KeyValuePair<string, string>>).IsAssignableFrom(typeToConvert);
14+
}
15+
16+
public override IReadOnlyDictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
17+
{
18+
return JsonSerializer.Deserialize<IReadOnlyDictionary<string, string>>(ref reader);
19+
}
20+
21+
public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<string, string> value, JsonSerializerOptions options)
22+
{
23+
writer.WriteStartObject();
24+
25+
foreach (var item in value.OrderBy(item => item.Key))
26+
{
27+
writer.WriteString(item.Key, item.Value);
28+
}
29+
30+
writer.WriteEndObject();
31+
}
32+
}
33+
}

src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ protected ResourceConfiguration(IResourceConfiguration<TCreateResourceEntity> ol
8888
/// <inheritdoc />
8989
public virtual string GetReuseHash()
9090
{
91-
var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType());
91+
var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), DefaultJsonSerializerOptions.Instance);
9292

9393
#if NET6_0_OR_GREATER
9494
return Convert.ToBase64String(SHA1.HashData(jsonUtf8Bytes));

src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ namespace DotNet.Testcontainers.Configurations
99
[PublicAPI]
1010
public interface IWaitStrategy
1111
{
12+
/// <summary>
13+
/// Sets the wait strategy mode.
14+
/// </summary>
15+
/// <param name="mode">The wait strategy mode.</param>
16+
/// <returns>The updated instance of the wait strategy.</returns>
17+
IWaitStrategy WithMode(WaitStrategyMode mode);
18+
1219
/// <summary>
1320
/// Sets the number of retries for the wait strategy.
1421
/// </summary>

0 commit comments

Comments
 (0)