Skip to content

Commit 7669b5e

Browse files
authored
feat: Support getting all mapped ports (#1485)
1 parent a35862d commit 7669b5e

File tree

4 files changed

+118
-9
lines changed

4 files changed

+118
-9
lines changed

src/Testcontainers/Containers/DockerContainer.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,13 @@ public long HealthCheckFailingStreak
252252
}
253253
}
254254

255+
/// <inheritdoc />
256+
public ushort GetMappedPublicPort()
257+
{
258+
using var enumerator = GetMappedPublicPorts().Values.GetEnumerator();
259+
return enumerator.MoveNext() ? enumerator.Current : throw new InvalidOperationException("No mapped port found.");
260+
}
261+
255262
/// <inheritdoc />
256263
public ushort GetMappedPublicPort(int containerPort)
257264
{
@@ -265,16 +272,40 @@ public ushort GetMappedPublicPort(string containerPort)
265272

266273
var qualifiedContainerPort = ContainerConfigurationConverter.GetQualifiedPort(containerPort);
267274

268-
if (_container.NetworkSettings.Ports.TryGetValue(qualifiedContainerPort, out var portBindings) && ushort.TryParse(portBindings[0].HostPort, out var publicPort))
275+
if (_container.NetworkSettings.Ports.TryGetValue(qualifiedContainerPort, out var portBindings) && ushort.TryParse(portBindings[0].HostPort, out var hostPort))
269276
{
270-
return publicPort;
277+
return hostPort;
271278
}
272279
else
273280
{
274281
throw new InvalidOperationException($"Exposed port {qualifiedContainerPort} is not mapped.");
275282
}
276283
}
277284

285+
/// <inheritdoc />
286+
public IReadOnlyDictionary<ushort, ushort> GetMappedPublicPorts()
287+
{
288+
ThrowIfResourceNotFound();
289+
290+
return _container.NetworkSettings.Ports
291+
.Where(
292+
kvp =>
293+
{
294+
return kvp.Key.Contains('/') && kvp.Value != null && kvp.Value.Count > 0;
295+
})
296+
.ToDictionary(
297+
kvp =>
298+
{
299+
var containerPort = kvp.Key.Substring(0, kvp.Key.IndexOf('/'));
300+
return ushort.Parse(containerPort);
301+
},
302+
kvp =>
303+
{
304+
var hostPort = kvp.Value[0].HostPort;
305+
return ushort.Parse(hostPort);
306+
});
307+
}
308+
278309
/// <inheritdoc />
279310
public Task<long> GetExitCodeAsync(CancellationToken ct = default)
280311
{

src/Testcontainers/Containers/IContainer.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ public interface IContainer : IAsyncDisposable
163163
/// </summary>
164164
long HealthCheckFailingStreak { get; }
165165

166+
/// <summary>
167+
/// Resolves the first public assigned host port.
168+
/// </summary>
169+
/// <returns>Returns the first public assigned host port.</returns>
170+
ushort GetMappedPublicPort();
171+
166172
/// <summary>
167173
/// Resolves the public assigned host port.
168174
/// </summary>
@@ -171,7 +177,7 @@ public interface IContainer : IAsyncDisposable
171177
/// </remarks>
172178
/// <param name="containerPort">The container port.</param>
173179
/// <returns>Returns the public assigned host port.</returns>
174-
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
180+
/// <exception cref="InvalidOperationException">Container has not been created, or no mapped port was found.</exception>
175181
ushort GetMappedPublicPort(int containerPort);
176182

177183
/// <summary>
@@ -185,6 +191,13 @@ public interface IContainer : IAsyncDisposable
185191
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
186192
ushort GetMappedPublicPort(string containerPort);
187193

194+
/// <summary>
195+
/// Resolves all public assigned host ports.
196+
/// </summary>
197+
/// <returns>Returns all public assigned host ports.</returns>
198+
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
199+
IReadOnlyDictionary<ushort, ushort> GetMappedPublicPorts();
200+
188201
/// <summary>
189202
/// Gets the container exit code.
190203
/// </summary>
@@ -284,15 +297,15 @@ public interface IContainer : IAsyncDisposable
284297
/// </summary>
285298
/// <param name="filePath">An absolute path or a name value within the container.</param>
286299
/// <param name="ct">Cancellation token.</param>
287-
/// <returns>Task that completes when the file has been read.</returns>
300+
/// <returns>A task that completes when the file has been read.</returns>
288301
Task<byte[]> ReadFileAsync(string filePath, CancellationToken ct = default);
289302

290303
/// <summary>
291304
/// Executes a command in the container.
292305
/// </summary>
293306
/// <param name="command">Shell command.</param>
294307
/// <param name="ct">Cancellation token.</param>
295-
/// <returns>Task that completes when the shell command has been executed.</returns>
308+
/// <returns>A task that completes when the shell command has been executed.</returns>
296309
Task<ExecResult> ExecAsync(IList<string> command, CancellationToken ct = default);
297310
}
298311
}

src/Testcontainers/Images/IgnoreFile.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,17 @@ public string Replace(string input)
185185
/// <inheritdoc />
186186
public string Replace(string input)
187187
{
188-
// Find last non recursive wildcard in pattern.
188+
// Find last non-recursive wildcard in pattern.
189189
var index = input.LastIndexOf("*", StringComparison.Ordinal);
190190

191-
// If last character is a non recursive wildcard, add the end of string regular expression.
191+
// If last character is a non-recursive wildcard, add the end of string regular expression.
192192
if (input.EndsWith("*", StringComparison.Ordinal) && index >= 0)
193193
{
194194
input = input.Remove(index, 1).Insert(index, $"{MatchAllExceptPathSeparator}?$");
195195
index = -1;
196196
}
197197

198-
// Replace the last non recursive wildcard with a match-zero-or-one quantifier regular expression.
198+
// Replace the last non-recursive wildcard with a match-zero-or-one quantifier regular expression.
199199
#if NETSTANDARD2_0
200200
if (input.Contains("*") && index >= 0)
201201
#else
@@ -205,7 +205,7 @@ public string Replace(string input)
205205
input = input.Remove(index, 1).Insert(index, $"{MatchAllExceptPathSeparator}?");
206206
}
207207

208-
// Replace remaining non recursive wildcards.
208+
// Replace remaining non-recursive wildcards.
209209
return input.Replace("*", MatchAllExceptPathSeparator);
210210
}
211211
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace Testcontainers.Tests;
2+
3+
public abstract class PortBindingTest : IAsyncLifetime
4+
{
5+
private readonly IContainer _container;
6+
7+
private PortBindingTest(ushort[] expectedPorts)
8+
{
9+
var containerBuilder = new ContainerBuilder().WithImage(CommonImages.Alpine).WithEntrypoint(CommonCommands.SleepInfinity);
10+
_container = expectedPorts.Aggregate(containerBuilder, (builder, port) => builder.WithPortBinding(port, true)).Build();
11+
}
12+
13+
public async ValueTask InitializeAsync()
14+
{
15+
await _container.StartAsync()
16+
.ConfigureAwait(false);
17+
}
18+
19+
public async ValueTask DisposeAsync()
20+
{
21+
await DisposeAsyncCore()
22+
.ConfigureAwait(false);
23+
24+
GC.SuppressFinalize(this);
25+
}
26+
27+
protected virtual ValueTask DisposeAsyncCore()
28+
{
29+
return _container.DisposeAsync();
30+
}
31+
32+
public abstract class MappedPublicPorts : PortBindingTest
33+
{
34+
private readonly ushort[] _expectedPorts;
35+
36+
protected MappedPublicPorts(ushort[] expectedPorts)
37+
: base(expectedPorts)
38+
{
39+
_expectedPorts = expectedPorts;
40+
}
41+
42+
[Fact]
43+
public void ShouldReturnExpectedPorts()
44+
{
45+
var actualPorts = _container.GetMappedPublicPorts();
46+
Assert.Equal(_expectedPorts, actualPorts.Keys);
47+
}
48+
49+
[Fact]
50+
public void ShouldThrowWhenNoPortsExist()
51+
{
52+
var exception = Record.Exception(() => _container.GetMappedPublicPort());
53+
Assert.Equal(exception == null, _expectedPorts.Length > 0);
54+
}
55+
}
56+
57+
public sealed class NoPortBindingTest()
58+
: MappedPublicPorts(Array.Empty<ushort>());
59+
60+
public sealed class SinglePortBindingTest()
61+
: MappedPublicPorts(new ushort[] { 8080 });
62+
63+
public sealed class MultiplePortBindingTest()
64+
: MappedPublicPorts(new ushort[] { 8080, 8081 });
65+
}

0 commit comments

Comments
 (0)