Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pausing and unpausing container #1315

Merged
merged 7 commits into from
Jan 8, 2025
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
12 changes: 12 additions & 0 deletions src/Testcontainers/Clients/DockerContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ public Task StopAsync(string id, CancellationToken ct = default)
return DockerClient.Containers.StopContainerAsync(id, new ContainerStopParameters { WaitBeforeKillSeconds = 15 }, ct);
}

public Task PauseAsync(string id, CancellationToken ct = default)
{
Logger.PauseDockerContainer(id);
return DockerClient.Containers.PauseContainerAsync(id, ct);
}

public Task UnpauseAsync(string id, CancellationToken ct = default)
{
Logger.UnpauseDockerContainer(id);
return DockerClient.Containers.UnpauseContainerAsync(id, ct);
}

public Task RemoveAsync(string id, CancellationToken ct = default)
{
Logger.DeleteDockerContainer(id);
Expand Down
4 changes: 4 additions & 0 deletions src/Testcontainers/Clients/IDockerContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ internal interface IDockerContainerOperations : IHasListOperations<ContainerList

Task StopAsync(string id, CancellationToken ct = default);

Task PauseAsync(string id, CancellationToken ct = default);

Task UnpauseAsync(string id, CancellationToken ct = default);

Task RemoveAsync(string id, CancellationToken ct = default);

Task ExtractArchiveToContainerAsync(string id, string path, TarOutputMemoryStream tarStream, CancellationToken ct = default);
Expand Down
16 changes: 16 additions & 0 deletions src/Testcontainers/Clients/ITestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ internal interface ITestcontainersClient
/// <returns>Task that completes when the container has been stopped.</returns>
Task StopAsync(string id, CancellationToken ct = default);

/// <summary>
/// Pauses the container.
/// </summary>
/// <param name="id">The container id.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been paused.</returns>
Task PauseAsync(string id, CancellationToken ct = default);

/// <summary>
/// Unpauses the container.
/// </summary>
/// <param name="id">The container id.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been unpaused.</returns>
Task UnpauseAsync(string id, CancellationToken ct = default);

/// <summary>
/// Removes the container.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/Testcontainers/Clients/TestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ await Container.StopAsync(id, ct)
}
}

/// <inheritdoc />
public async Task PauseAsync(string id, CancellationToken ct = default)
{
if (await Container.ExistsWithIdAsync(id, ct)
.ConfigureAwait(false))
{
await Container.PauseAsync(id, ct)
.ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task UnpauseAsync(string id, CancellationToken ct = default)
{
if (await Container.ExistsWithIdAsync(id, ct)
.ConfigureAwait(false))
{
await Container.UnpauseAsync(id, ct)
.ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task RemoveAsync(string id, CancellationToken ct = default)
{
Expand Down
98 changes: 97 additions & 1 deletion src/Testcontainers/Containers/DockerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace DotNet.Testcontainers.Containers
[PublicAPI]
public class DockerContainer : Resource, IContainer
{
private const TestcontainersStates ContainerHasBeenCreatedStates = TestcontainersStates.Created | TestcontainersStates.Running | TestcontainersStates.Exited;
private const TestcontainersStates ContainerHasBeenCreatedStates = TestcontainersStates.Created | TestcontainersStates.Running | TestcontainersStates.Paused | TestcontainersStates.Exited;

private const TestcontainersHealthStatus ContainerHasHealthCheck = TestcontainersHealthStatus.Starting | TestcontainersHealthStatus.Healthy | TestcontainersHealthStatus.Unhealthy;

Expand Down Expand Up @@ -48,6 +48,12 @@ public DockerContainer(IContainerConfiguration configuration)
/// <inheritdoc />
public event EventHandler Stopping;

/// <inheritdoc />
public event EventHandler Pausing;

/// <inheritdoc />
public event EventHandler Unpausing;

/// <inheritdoc />
public event EventHandler Created;

Expand All @@ -57,6 +63,12 @@ public DockerContainer(IContainerConfiguration configuration)
/// <inheritdoc />
public event EventHandler Stopped;

/// <inheritdoc />
public event EventHandler Paused;

/// <inheritdoc />
public event EventHandler Unpaused;

/// <inheritdoc />
public DateTime CreatedTime { get; private set; }

Expand All @@ -66,6 +78,12 @@ public DockerContainer(IContainerConfiguration configuration)
/// <inheritdoc />
public DateTime StoppedTime { get; private set; }

/// <inheritdoc />
public DateTime PausedTime { get; private set; }

/// <inheritdoc />
public DateTime UnpausedTime { get; private set; }

/// <inheritdoc />
public ILogger Logger
{
Expand Down Expand Up @@ -294,6 +312,26 @@ await UnsafeStopAsync(ct)
.ConfigureAwait(false);
}

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

await UnsafePauseAsync(ct)
.ConfigureAwait(false);
}

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

await UnsafeUnpauseAsync(ct)
.ConfigureAwait(false);
}

/// <inheritdoc />
public Task CopyAsync(byte[] fileContent, string filePath, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default)
{
Expand Down Expand Up @@ -522,6 +560,64 @@ await _client.StopAsync(_container.ID, ct)
Stopped?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Pauses the container.
/// </summary>
/// <remarks>
/// Only the public members <see cref="PauseAsync" /> and <see cref="UnpauseAsync" /> are thread-safe for now.
/// </remarks>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been paused.</returns>
protected virtual async Task UnsafePauseAsync(CancellationToken ct = default)
{
ThrowIfLockNotAcquired();

if (!Exists())
{
return;
}

Pausing?.Invoke(this, EventArgs.Empty);

await _client.PauseAsync(_container.ID, ct)
.ConfigureAwait(false);

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

PausedTime = DateTime.UtcNow;
Paused?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Unpauses the container.
/// </summary>
/// <remarks>
/// Only the public members <see cref="PauseAsync" /> and <see cref="UnpauseAsync" /> are thread-safe for now.
/// </remarks>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been unpaused.</returns>
protected virtual async Task UnsafeUnpauseAsync(CancellationToken ct = default)
{
ThrowIfLockNotAcquired();

if (!Exists())
{
return;
}

Unpausing?.Invoke(this, EventArgs.Empty);

await _client.UnpauseAsync(_container.ID, ct)
.ConfigureAwait(false);

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

UnpausedTime = DateTime.UtcNow;
Unpaused?.Invoke(this, EventArgs.Empty);
}

/// <inheritdoc />
protected override bool Exists()
{
Expand Down
52 changes: 52 additions & 0 deletions src/Testcontainers/Containers/IContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ public interface IContainer : IAsyncDisposable
[CanBeNull]
event EventHandler Stopping;

/// <summary>
/// Subscribes to the pausing event.
/// </summary>
[CanBeNull]
event EventHandler Pausing;

/// <summary>
/// Subscribes to the unpausing event.
/// </summary>
[CanBeNull]
event EventHandler Unpausing;

/// <summary>
/// Subscribes to the created event.
/// </summary>
Expand All @@ -52,6 +64,18 @@ public interface IContainer : IAsyncDisposable
[CanBeNull]
event EventHandler Stopped;

/// <summary>
/// Subscribes to the paused event.
/// </summary>
[CanBeNull]
event EventHandler Paused;

/// <summary>
/// Subscribes to the unpaused event.
/// </summary>
[CanBeNull]
event EventHandler Unpaused;

/// <summary>
/// Gets the created timestamp.
/// </summary>
Expand All @@ -67,6 +91,16 @@ public interface IContainer : IAsyncDisposable
/// </summary>
DateTime StoppedTime { get; }

/// <summary>
/// Gets the paused timestamp.
/// </summary>
DateTime PausedTime { get; }

/// <summary>
/// Gets the unpaused timestamp.
/// </summary>
DateTime UnpausedTime { get; }

/// <summary>
/// Gets the logger.
/// </summary>
Expand Down Expand Up @@ -187,6 +221,24 @@ public interface IContainer : IAsyncDisposable
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task StopAsync(CancellationToken ct = default);

/// <summary>
/// Pauses the container.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been paused.</returns>
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task PauseAsync(CancellationToken ct = default);

/// <summary>
/// Unpauses the container.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the container has been unpaused.</returns>
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
Task UnpauseAsync(CancellationToken ct = default);

/// <summary>
/// Copies a test host file to the container.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/Testcontainers/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ private static readonly Action<ILogger, string, Exception> _StartDockerContainer
private static readonly Action<ILogger, string, Exception> _StopDockerContainer
= LoggerMessage.Define<string>(LogLevel.Information, default, "Stop Docker container {Id}");

private static readonly Action<ILogger, string, Exception> _PauseDockerContainer
= LoggerMessage.Define<string>(LogLevel.Information, default, "Pause Docker container {Id}");

private static readonly Action<ILogger, string, Exception> _UnpauseDockerContainer
= LoggerMessage.Define<string>(LogLevel.Information, default, "Unpause Docker container {Id}");

private static readonly Action<ILogger, string, Exception> _DeleteDockerContainer
= LoggerMessage.Define<string>(LogLevel.Information, default, "Delete Docker container {Id}");

Expand Down Expand Up @@ -132,6 +138,16 @@ public static void StopDockerContainer(this ILogger logger, string id)
_StopDockerContainer(logger, TruncId(id), null);
}

public static void PauseDockerContainer(this ILogger logger, string id)
{
_PauseDockerContainer(logger, TruncId(id), null);
}

public static void UnpauseDockerContainer(this ILogger logger, string id)
{
_UnpauseDockerContainer(logger, TruncId(id), null);
}

public static void DeleteDockerContainer(this ILogger logger, string id)
{
_DeleteDockerContainer(logger, TruncId(id), null);
Expand Down
37 changes: 37 additions & 0 deletions tests/Testcontainers.Platform.Linux.Tests/PauseUnpauseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Testcontainers.Tests;

public sealed class PauseUnpauseTest : IAsyncLifetime
{
private readonly IContainer _container = new ContainerBuilder()
.WithImage(CommonImages.Alpine)
.WithCommand(CommonCommands.SleepInfinity)
.Build();

public Task InitializeAsync()
{
return _container.StartAsync();
}

public Task DisposeAsync()
{
return _container.DisposeAsync().AsTask();
}

[Fact]
public async Task PausesAndUnpausesContainerSuccessfully()
{
await _container.PauseAsync()
.ConfigureAwait(true);
Assert.Equal(TestcontainersStates.Paused, _container.State);

await _container.UnpauseAsync()
.ConfigureAwait(true);
Assert.Equal(TestcontainersStates.Running, _container.State);
}

[Fact]
public Task UnpausingRunningContainerThrowsDockerApiException()
{
return Assert.ThrowsAsync<DockerApiException>(() => _container.UnpauseAsync());
}
}
1 change: 1 addition & 0 deletions tests/Testcontainers.Platform.Linux.Tests/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Docker.DotNet;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers;
global using DotNet.Testcontainers.Builders;
Expand Down