diff --git a/docs/api/create_docker_network.md b/docs/api/create_docker_network.md index 0b3d85faf..c8619c3df 100644 --- a/docs/api/create_docker_network.md +++ b/docs/api/create_docker_network.md @@ -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/). diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index 510e66a5e..093f706f6 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -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; @@ -373,6 +374,26 @@ await UnsafeUnpauseAsync(ct) .ConfigureAwait(false); } + /// + public async Task ConnectAsync(string network, CancellationToken ct = default) + { + using var disposable = await AcquireLockAsync(ct) + .ConfigureAwait(false); + + await UnsafeConnectAsync(network, ct) + .ConfigureAwait(false); + } + + /// + public async Task ConnectAsync(INetwork network, CancellationToken ct = default) + { + using var disposable = await AcquireLockAsync(ct) + .ConfigureAwait(false); + + await UnsafeConnectAsync(network.Name, ct) + .ConfigureAwait(false); + } + /// public Task CopyAsync(byte[] fileContent, string filePath, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default) { @@ -688,6 +709,28 @@ await _client.UnpauseAsync(_container.ID, ct) Unpaused?.Invoke(this, EventArgs.Empty); } + /// + /// Connects the container to an existing network. + /// + /// + /// Only the public members and are thread-safe for now. + /// + /// The network name. + /// Cancellation token. + /// Task that completes when the container has been connected to the network. + 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); + } + /// protected override bool Exists() { diff --git a/src/Testcontainers/Containers/IContainer.cs b/src/Testcontainers/Containers/IContainer.cs index 04eb6a5a7..99cb797e6 100644 --- a/src/Testcontainers/Containers/IContainer.cs +++ b/src/Testcontainers/Containers/IContainer.cs @@ -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; @@ -188,7 +189,7 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable /// /// The container port. /// Returns the public assigned host port. - /// Container has not been created. + /// Container has not been created, or no mapped port was found. ushort GetMappedPublicPort(string containerPort); /// @@ -252,6 +253,28 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable /// Thrown when a Testcontainers task gets canceled. Task UnpauseAsync(CancellationToken ct = default); + /// + /// Connects the running container to an existing network. + /// + /// The existing network to connect to. + /// Cancellation token. + /// Task that completes when the container has been connected to the network. + /// Container has not been created. + /// Thrown when a Docker API call gets canceled. + /// Thrown when a Testcontainers task gets canceled. + Task ConnectAsync(string network, CancellationToken ct = default); + + /// + /// Connects the running container to an existing network. + /// + /// The existing network to connect to. + /// Cancellation token. + /// Task that completes when the container has been connected to the network. + /// Container has not been created. + /// Thrown when a Docker API call gets canceled. + /// Thrown when a Testcontainers task gets canceled. + Task ConnectAsync(INetwork network, CancellationToken ct = default); + /// /// Copies a test host file to the container. /// @@ -261,7 +284,7 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable /// The group ID to set for the copied file or directory. Defaults to 0 (root). /// The POSIX file mode permission. /// Cancellation token. - /// + /// A task that completes when the byte array content has been copied. Task CopyAsync(byte[] fileContent, string filePath, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default); /// diff --git a/src/Testcontainers/Images/MatchImage.cs b/src/Testcontainers/Images/MatchImage.cs index 681fe4330..688e12b03 100644 --- a/src/Testcontainers/Images/MatchImage.cs +++ b/src/Testcontainers/Images/MatchImage.cs @@ -44,7 +44,7 @@ static ReferenceRegex() } private ReferenceRegex() - : base(Pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(1)) + : base(Pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(5)) { } diff --git a/tests/Testcontainers.Platform.Linux.Tests/NetworkConnectTest.cs b/tests/Testcontainers.Platform.Linux.Tests/NetworkConnectTest.cs new file mode 100644 index 000000000..2c10f0a47 --- /dev/null +++ b/tests/Testcontainers.Platform.Linux.Tests/NetworkConnectTest.cs @@ -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); + } +} \ No newline at end of file