Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 31 additions & 0 deletions docs/api/create_docker_network.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ var execResult = await ultimateQuestionContainer.ExecAsync(new[] { "nc", MagicNu
Assert.Equal(MagicNumber, execResult.Stdout.Trim());
```

## Connecting a running container to an existing network

If a container is already running, use `IContainer.ConnectAsync(...)` to attach it to an existing network.

The network must already exist. You can reference it either by network name or by an `INetwork` instance:

```csharp
var network = new NetworkBuilder()
.WithName(Guid.NewGuid().ToString("D"))
.Build();

var container = new ContainerBuilder("alpine:3.20.0")
.WithEntrypoint("top")
.Build();

await network.CreateAsync()
.ConfigureAwait(false);

await container.StartAsync()
.ConfigureAwait(false);

await container.ConnectAsync(network)
.ConfigureAwait(false);

// Equivalent when only the network name is available:
await container.ConnectAsync(network.Name)
.ConfigureAwait(false);
```

Prefer `WithNetwork(...)` during container configuration whenever possible. Use `ConnectAsync(...)` when you explicitly need to attach a running container to an already existing network.

## Exposing container ports to the host

It is common to connect to a container from your test process running on your test host. To bind and expose a container port, use the `WithPortBinding(ushort, true)` container builder member. To retrieve the actual port at runtime, use the container `GetMappedPublicPort(ushort)` member. Further information on network configurations is included in our [best practices](https://dotnet.testcontainers.org/api/best_practices/).
Expand Down
43 changes: 43 additions & 0 deletions src/Testcontainers/Containers/DockerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace DotNet.Testcontainers.Containers
using DotNet.Testcontainers.Clients;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Images;
using DotNet.Testcontainers.Networks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -373,6 +374,26 @@ await UnsafeUnpauseAsync(ct)
.ConfigureAwait(false);
}

/// <inheritdoc />
public async Task ConnectAsync(string network, CancellationToken ct = default)
{
using var disposable = await AcquireLockAsync(ct)
.ConfigureAwait(false);

await UnsafeConnectAsync(network, ct)
.ConfigureAwait(false);
}

/// <inheritdoc />
public async Task ConnectAsync(INetwork network, CancellationToken ct = default)
{
using var disposable = await AcquireLockAsync(ct)
.ConfigureAwait(false);

await UnsafeConnectAsync(network.Name, ct)
.ConfigureAwait(false);
}

/// <inheritdoc />
public Task CopyAsync(byte[] fileContent, string filePath, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default)
{
Expand Down Expand Up @@ -688,6 +709,28 @@ await _client.UnpauseAsync(_container.ID, ct)
Unpaused?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Connects the container to an existing network.
/// </summary>
/// <remarks>
/// Only the public members <see cref="ConnectAsync(string, CancellationToken)" /> and <see cref="ConnectAsync(INetwork, CancellationToken)" /> are thread-safe for now.
/// </remarks>
/// <param name="network">The network name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been connected to the network.</returns>
protected virtual async Task UnsafeConnectAsync(string network, CancellationToken ct = default)
{
ThrowIfLockNotAcquired();

ThrowIfResourceNotFound();

await _client.Network.ConnectAsync(network, _container.ID, ct)
.ConfigureAwait(false);

_container = await _client.Container.ByIdAsync(_container.ID, ct)
.ConfigureAwait(false);
}

/// <inheritdoc />
protected override bool Exists()
{
Expand Down
27 changes: 25 additions & 2 deletions src/Testcontainers/Containers/IContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace DotNet.Testcontainers.Containers
using System.Threading.Tasks;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Images;
using DotNet.Testcontainers.Networks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -188,7 +189,7 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable
/// </remarks>
/// <param name="containerPort">The container port.</param>
/// <returns>Returns the public assigned host port.</returns>
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
/// <exception cref="InvalidOperationException">Container has not been created, or no mapped port was found.</exception>
ushort GetMappedPublicPort(string containerPort);

/// <summary>
Expand Down Expand Up @@ -252,6 +253,28 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task UnpauseAsync(CancellationToken ct = default);

/// <summary>
/// Connects the running container to an existing network.
/// </summary>
/// <param name="network">The existing network to connect to.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been connected to the network.</returns>
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task ConnectAsync(string network, CancellationToken ct = default);

/// <summary>
/// Connects the running container to an existing network.
/// </summary>
/// <param name="network">The existing network to connect to.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been connected to the network.</returns>
/// <exception cref="InvalidOperationException">Container has not been created.</exception>
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task ConnectAsync(INetwork network, CancellationToken ct = default);

/// <summary>
/// Copies a test host file to the container.
/// </summary>
Expand All @@ -261,7 +284,7 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable
/// <param name="gid">The group ID to set for the copied file or directory. Defaults to 0 (root).</param>
/// <param name="fileMode">The POSIX file mode permission.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns></returns>
/// <returns>A task that completes when the byte array content has been copied.</returns>
Task CopyAsync(byte[] fileContent, string filePath, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default);

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers/Images/MatchImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ static ReferenceRegex()
}

private ReferenceRegex()
: base(Pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(1))
: base(Pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(5))
{
}

Expand Down
45 changes: 45 additions & 0 deletions tests/Testcontainers.Platform.Linux.Tests/NetworkConnectTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Testcontainers.Tests;

public sealed class NetworkConnectTest
{
[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task ConnectsRunningContainerToExistingNetworks()
{
// Given
await using var networkByName = new NetworkBuilder()
.Build();

await using var networkByReference = new NetworkBuilder()
.Build();

await using var container = new ContainerBuilder(CommonImages.Alpine)
.WithCommand(CommonCommands.SleepInfinity)
.Build();

await networkByName.CreateAsync(TestContext.Current.CancellationToken)
.ConfigureAwait(true);

await networkByReference.CreateAsync(TestContext.Current.CancellationToken)
.ConfigureAwait(true);

await container.StartAsync(TestContext.Current.CancellationToken)
.ConfigureAwait(true);

using var dockerClient = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientBuilder(Guid.NewGuid()).Build();

// When
await container.ConnectAsync(networkByName.Name, TestContext.Current.CancellationToken)
.ConfigureAwait(true);

await container.ConnectAsync(networkByReference, TestContext.Current.CancellationToken)
.ConfigureAwait(true);

var response = await dockerClient.Containers.InspectContainerAsync(container.Id, TestContext.Current.CancellationToken)
.ConfigureAwait(true);

// Then
Assert.Contains(networkByName.Name, response.NetworkSettings.Networks.Keys);
Assert.Contains(networkByReference.Name, response.NetworkSettings.Networks.Keys);
}
}
Loading