Skip to content

Commit

Permalink
[#242] #IMPLEMENT 'assemblyName: DotNet.Testcontainers; function: Res…
Browse files Browse the repository at this point in the history
…ourceReaper'

{Add ResourceReaper.}
  • Loading branch information
PSanetra committed Dec 21, 2021
1 parent 4186d34 commit 0cfdb4c
Show file tree
Hide file tree
Showing 26 changed files with 664 additions and 69 deletions.
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "5.0.100",
"version": "6.0.100",
"rollForward": "latestPatch"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace DotNet.Testcontainers.Tests.Unit.Containers.Unix
{
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
Expand Down Expand Up @@ -412,6 +413,62 @@ public async Task CopyFileToRunningContainer()
Assert.Equal(0, await testcontainer.ExecAsync(new[] { "/bin/sh", "-c", $"stat {dayOfWeekFilePath} | grep 0600" }));
}
}

[Fact]
public async Task AutoRemoveFalseShouldNotRemoveContainer()
{
// Given
var testcontainersBuilder = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("alpine")
.WithAutoRemove(false)
.WithCommand("/bin/sh", "-c", "tail -f /dev/null");

// When
// Then
await using (IDockerContainer testcontainer = testcontainersBuilder.Build())
{
await testcontainer.StartAsync();
await testcontainer.StopAsync();

var dockerPsStdOut = await DockerPsAqNoTrunc();

Assert.Contains(testcontainer.Id, dockerPsStdOut);
}
}

[Fact]
public async Task AutoRemoveTrueShouldRemoveContainer()
{
// Given
var testcontainersBuilder = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("alpine")
.WithAutoRemove(true)
.WithCommand("/bin/sh", "-c", "tail -f /dev/null");

// When
// Then
await using (IDockerContainer testcontainer = testcontainersBuilder.Build())
{
await testcontainer.StartAsync();
var containerId = testcontainer.Id;

await testcontainer.StopAsync();

var dockerPsStdOut = await DockerPsAqNoTrunc();

Assert.DoesNotContain(containerId, dockerPsStdOut);
}
}

private static async Task<string> DockerPsAqNoTrunc()
{
using var dockerPs = new Process { StartInfo = { FileName = "docker", Arguments = "ps -aq --no-trunc" } };
dockerPs.StartInfo.RedirectStandardOutput = true;
Assert.True(dockerPs.Start());

var dockerPsStdOut = await dockerPs.StandardOutput.ReadToEndAsync();
return dockerPsStdOut;
}
}
}
}
118 changes: 118 additions & 0 deletions src/DotNet.Testcontainers.Tests/Unit/Services/ResourceReaperTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
namespace DotNet.Testcontainers.Tests.Unit.Services
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Networks.Builders;
using Testcontainers.Containers.Builders;
using Testcontainers.Containers.Modules;
using Testcontainers.Images.Builders;
using Testcontainers.Services;
using Xunit;

public class ResourceReaperTest
{
[Fact]
public async Task ShouldReapContainerWhenDisposingResourceReaper()
{
// Given
await using (var resourceReaper = await ResourceReaper.StartNew())
{
var testcontainersBuilder = new TestcontainersBuilder<TestcontainersContainer>()
.WithResourceReaperSessionId(resourceReaper.SessionId)
.WithImage("alpine")
.WithCommand("/bin/sh", "-c", "tail -f /dev/null");

await using (var testcontainer = testcontainersBuilder.Build())
{
await testcontainer.StartAsync();
var containerId = testcontainer.Id;

// When
await resourceReaper.DisposeAsync();

// Then
var dockerPsStdOut = await DockerPsAqNoTrunc();

Assert.DoesNotContain(containerId, dockerPsStdOut);
}
}
}

[Fact]
public async Task ShouldReapImageWhenDisposingResourceReaper()
{
var imageName = $"testimage_{Guid.NewGuid().ToString("D")}";

// Given
await using (var resourceReaper = await ResourceReaper.StartNew())
{
await new ImageFromDockerfileBuilder()
.WithResourceReaperSessionId(resourceReaper.SessionId)
.WithName(imageName)
.WithDockerfileDirectory("Assets")
.Build();

Assert.Contains(imageName, await DockerImages());

// When
await resourceReaper.DisposeAsync();

// Then
Assert.DoesNotContain(imageName, await DockerImages());
}
}

[Fact]
public async Task ShouldReapNetworkWhenDisposingResourceReaper()
{
var networkName = $"testnetwork_{Guid.NewGuid().ToString("D")}";

// Given
await using (var resourceReaper = await ResourceReaper.StartNew())
{
await new TestcontainersNetworkBuilder()
.WithResourceReaperSessionId(resourceReaper.SessionId)
.WithName(networkName)
.Build()
.CreateAsync();

Assert.Contains(networkName, await DockerNetworks());

await resourceReaper.DisposeAsync();

Assert.DoesNotContain(networkName, await DockerNetworks());
}
}

private static async Task<string> DockerPsAqNoTrunc()
{
using var dockerPs = new Process { StartInfo = { FileName = "docker", Arguments = "images -aq --no-trunc" } };
dockerPs.StartInfo.RedirectStandardOutput = true;
Assert.True(dockerPs.Start());

var dockerPsStdOut = await dockerPs.StandardOutput.ReadToEndAsync();
return dockerPsStdOut;
}

private static async Task<string> DockerImages()
{
using var dockerImages = new Process { StartInfo = { FileName = "docker", Arguments = "images" } };
dockerImages.StartInfo.RedirectStandardOutput = true;
Assert.True(dockerImages.Start());

var dockerImagesStdOut = await dockerImages.StandardOutput.ReadToEndAsync();
return dockerImagesStdOut;
}

private static async Task<string> DockerNetworks()
{
using var dockerNetworks = new Process { StartInfo = { FileName = "docker", Arguments = "network ls" } };
dockerNetworks.StartInfo.RedirectStandardOutput = true;
Assert.True(dockerNetworks.Start());

var dockerNetworksStdOut = await dockerNetworks.StandardOutput.ReadToEndAsync();
return dockerNetworksStdOut;
}
}
}
3 changes: 2 additions & 1 deletion src/DotNet.Testcontainers/Clients/DefaultLabels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ namespace DotNet.Testcontainers.Clients
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Services;

internal sealed class DefaultLabels : ReadOnlyDictionary<string, string>
{
public DefaultLabels() : base(new Dictionary<string, string>
{
{ TestcontainersClient.TestcontainersLabel, "true" },
{ TestcontainersClient.TestcontainersCleanUpLabel, "true" }
{ ResourceReaper.ResourceReaperSessionLabel, ResourceReaper.DefaultSessionId.ToString("D") }
})
{
}
Expand Down
12 changes: 3 additions & 9 deletions src/DotNet.Testcontainers/Clients/DockerContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ public async Task<IEnumerable<ContainerListResponse>> GetAllAsync(CancellationTo
.ConfigureAwait(false)).ToArray();
}

public async Task<IEnumerable<ContainerListResponse>> GetOrphanedObjects(CancellationToken ct = default)
{
var filters = new FilterByProperty { { "label", $"{TestcontainersClient.TestcontainersCleanUpLabel}=true" }, { "status", "exited" } };
return (await this.Docker.Containers.ListContainersAsync(new ContainersListParameters { All = true, Filters = filters }, ct)
.ConfigureAwait(false)).ToArray();
}

public Task<ContainerListResponse> ByIdAsync(string id, CancellationToken ct = default)
{
return this.ByPropertyAsync("id", id, ct);
Expand Down Expand Up @@ -139,9 +132,10 @@ public async Task<string> RunAsync(ITestcontainersConfiguration configuration, C

var hostConfig = new HostConfig
{
// AutoRemove = configuration.CleanUp, TODO: Should we keep this? If the Docker daemon remove containers we're no longer able to call e. g. `CleanUp(true)` + `GetExitCode()`.
AutoRemove = configuration.AutoRemove ?? false,
Privileged = configuration.Privileged ?? false,
PortBindings = converter.PortBindings,
Mounts = converter.Mounts,
Mounts = converter.Mounts
};

var networkingConfig = new NetworkingConfig
Expand Down
16 changes: 8 additions & 8 deletions src/DotNet.Testcontainers/Clients/DockerImageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ public async Task<IEnumerable<ImagesListResponse>> GetAllAsync(CancellationToken
.ConfigureAwait(false)).ToArray();
}

public Task<IEnumerable<ImagesListResponse>> GetOrphanedObjects(CancellationToken ct = default)
{
IEnumerable<ImagesListResponse> images = Array.Empty<ImagesListResponse>();
return Task.FromResult(images);
}

public async Task<ImagesListResponse> ByIdAsync(string id, CancellationToken ct = default)
{
return (await this.GetAllAsync(ct)
Expand Down Expand Up @@ -96,8 +90,14 @@ await this.DeleteAsync(config.Image, ct)

using (var stream = new FileStream(dockerFileArchive.Tar(), FileMode.Open))
{
using (var image = await this.Docker.Images.BuildImageFromDockerfileAsync(stream, new ImageBuildParameters { Dockerfile = config.Dockerfile, Tags = new[] { config.Image.FullName } }, ct)
.ConfigureAwait(false))
var imageBuildParameters = new ImageBuildParameters
{
Dockerfile = config.Dockerfile,
Tags = new[] { config.Image.FullName },
Labels = config.Labels.ToDictionary(kv => kv.Key, kv => kv.Value)
};
using (var image = await this.Docker.Images.BuildImageFromDockerfileAsync(stream, imageBuildParameters, ct)
.ConfigureAwait(false))
{
// Read the image stream to the end, to avoid disposing before Docker has done it's job.
_ = await new StreamReader(image).ReadToEndAsync()
Expand Down
15 changes: 8 additions & 7 deletions src/DotNet.Testcontainers/Clients/DockerNetworkOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ public async Task<IEnumerable<NetworkResponse>> GetAllAsync(CancellationToken ct
.ConfigureAwait(false)).ToArray();
}

public Task<IEnumerable<NetworkResponse>> GetOrphanedObjects(CancellationToken ct = default)
{
IEnumerable<NetworkResponse> networks = Array.Empty<NetworkResponse>();
return Task.FromResult(networks);
}

public async Task<NetworkResponse> ByIdAsync(string id, CancellationToken ct = default)
{
return (await this.GetAllAsync(ct)
Expand Down Expand Up @@ -62,7 +56,14 @@ public async Task<bool> ExistsWithNameAsync(string name, CancellationToken ct =

public async Task<string> CreateAsync(ITestcontainersNetworkConfiguration configuration, CancellationToken ct = default)
{
var id = (await this.Docker.Networks.CreateNetworkAsync(new NetworksCreateParameters { Name = configuration.Name, Driver = configuration.Driver.Value }, ct)
var networksCreateParameters = new NetworksCreateParameters
{
Name = configuration.Name,
Driver = configuration.Driver.Value,
Labels = configuration.Labels.ToDictionary(kv => kv.Key, kv => kv.Value)
};

var id = (await this.Docker.Networks.CreateNetworkAsync(networksCreateParameters, ct)
.ConfigureAwait(false)).ID;

Logger.LogInformation("Network {id} created", id);
Expand Down
2 changes: 0 additions & 2 deletions src/DotNet.Testcontainers/Clients/IHasListOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ internal interface IHasListOperations<T>
{
Task<IEnumerable<T>> GetAllAsync(CancellationToken ct = default);

Task<IEnumerable<T>> GetOrphanedObjects(CancellationToken ct = default);

Task<T> ByIdAsync(string id, CancellationToken ct = default);

Task<T> ByNameAsync(string name, CancellationToken ct = default);
Expand Down
32 changes: 11 additions & 21 deletions src/DotNet.Testcontainers/Clients/TestcontainersClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ namespace DotNet.Testcontainers.Clients
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet;
Expand All @@ -14,15 +14,16 @@ namespace DotNet.Testcontainers.Clients
using DotNet.Testcontainers.Images.Configurations;
using DotNet.Testcontainers.Services;
using ICSharpCode.SharpZipLib.Tar;
using Microsoft.Extensions.Logging;

internal sealed class TestcontainersClient : ITestcontainersClient
{
public const string TestcontainersLabel = "dotnet.testcontainers";

public const string TestcontainersCleanUpLabel = TestcontainersLabel + ".cleanUp";
private static readonly ILogger<TestcontainersClient> Logger = TestcontainersHostService.GetLogger<TestcontainersClient>();

private readonly string osRootDirectory = Path.GetPathRoot(Directory.GetCurrentDirectory());

public const string TestcontainersLabel = "dotnet.testcontainers";

private readonly TestcontainersRegistryService registryService;

private readonly IDockerContainerOperations containers;
Expand Down Expand Up @@ -119,14 +120,9 @@ public async Task RemoveAsync(string id, CancellationToken ct = default)
await this.containers.RemoveAsync(id, ct)
.ConfigureAwait(false);
}
catch (DockerApiException e)
catch (DockerApiException e) when(e.StatusCode == HttpStatusCode.Conflict)
{
// The Docker daemon may already start the progress to removes the container (AutoRemove).
// https://docs.docker.com/engine/api/v1.41/#operation/ContainerCreate.
if (!e.Message.Contains($"removal of container {id} is already in progress"))
{
throw;
}
Logger.LogDebug(e, "Conflict while trying to remove Container. This may happen if the container is started with the AutoRemove option.");
}
}

Expand Down Expand Up @@ -176,11 +172,10 @@ await this.containers.ExtractArchiveToContainerAsync(id, "/", memStream, ct)

public async Task<string> RunAsync(ITestcontainersConfiguration configuration, CancellationToken ct = default)
{
// Killing or canceling the test process will prevent the cleanup.
// Remove labeled, orphaned containers from previous runs.
var removeOrphanedContainersTasks = (await this.containers.GetOrphanedObjects(ct)
.ConfigureAwait(false))
.Select(container => this.containers.RemoveAsync(container.ID, ct));
if (configuration.Labels.TryGetValue(ResourceReaper.ResourceReaperSessionLabel, out var resourceReaperSessionId) && !string.IsNullOrWhiteSpace(resourceReaperSessionId))
{
await ResourceReaper.GetOrStartDefaultAsync();
}

if (!await this.images.ExistsWithNameAsync(configuration.Image.FullName, ct)
.ConfigureAwait(false))
Expand All @@ -189,14 +184,9 @@ await this.images.CreateAsync(configuration.Image, configuration.AuthConfig, ct)
.ConfigureAwait(false);
}

await Task.WhenAll(removeOrphanedContainersTasks)
.ConfigureAwait(false);

var id = await this.containers.RunAsync(configuration, ct)
.ConfigureAwait(false);

this.registryService.Register(id, configuration.CleanUp);

return id;
}

Expand Down
Loading

0 comments on commit 0cfdb4c

Please sign in to comment.