Skip to content

Commit

Permalink
feat: Add docker compose support (testcontainers#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
nkz-soft committed Jan 2, 2024
1 parent 5ca4b6b commit eb986f4
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 2 deletions.
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DockerCompose", "src\Testcontainers.DockerCompose\Testcontainers.DockerCompose.csproj", "{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DockerCompose.Tests", "tests\Testcontainers.DockerCompose.Tests\Testcontainers.DockerCompose.Tests.csproj", "{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -532,6 +536,14 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Release|Any CPU.Build.0 = Release|Any CPU
{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -619,5 +631,7 @@ Global
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{D77017F1-9E38-4B06-8CEB-9B3D98B6497C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions src/Testcontainers.DockerCompose/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
123 changes: 123 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
namespace Testcontainers.DockerCompose;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class DockerComposeBuilder : ContainerBuilder<DockerComposeBuilder, DockerComposeContainer, DockerComposeConfiguration>
{
private const string NoComposeFile = "No docker compose file have been provided.";

//Docker Compose is included as part of this image.
public const string DockerComposeImage = "docker:24-cli";

public const string DockerSocketPath = "/var/run/docker.sock";

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeBuilder" /> class.
/// </summary>
public DockerComposeBuilder()
: this(new DockerComposeConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private DockerComposeBuilder(DockerComposeConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override DockerComposeConfiguration DockerResourceConfiguration { get; }

/// <inheritdoc />
public override DockerComposeContainer Build()
{
Validate();

return new DockerComposeContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
}

/// <summary>
/// Sets the compose file.
/// </summary>
/// <param name="composeFile">The compose file.</param>
/// <returns></returns>
public DockerComposeBuilder WithComposeFile(string composeFile)
{
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
(composeFile: composeFile));
}

/// <summary>
/// If true use a local Docker Compose binary instead of a container.
/// </summary>
/// <param name="localCompose"></param>
/// <returns></returns>
public DockerComposeBuilder WithLocalCompose(bool localCompose)
{
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
(localCompose: localCompose));
}

/// <inheritdoc />
protected override DockerComposeBuilder Init()
{
return base.Init()
.WithImage(DockerComposeImage)
.WithEntrypoint(CommonCommands.SleepInfinity)
.WithBindMount(DockerSocketPath, DockerSocketPath, AccessMode.ReadWrite)
.WithStartupCallback(ConfigureDockerComposeAsync);
}

/// <inheritdoc />
protected override void Validate()
{
base.Validate();

_ = Guard.Argument(DockerResourceConfiguration.ComposeFile, nameof(DockerResourceConfiguration.ComposeFile))
.NotEmpty();
}

/// <inheritdoc />
protected override DockerComposeBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override DockerComposeBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override DockerComposeBuilder Merge(DockerComposeConfiguration oldValue, DockerComposeConfiguration newValue)
{
return new DockerComposeBuilder(new DockerComposeConfiguration(oldValue, newValue));
}

/// <summary>
/// Configures the compose container.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="ct">Cancellation token.</param>
private async Task ConfigureDockerComposeAsync(IContainer container, CancellationToken ct = default)
{
if (container is DockerComposeRemote dockerComposeContainer &&
!dockerComposeContainer.RuntimeConfiguration.LocalCompose )
{
var fileInfo = new FileInfo(dockerComposeContainer.RuntimeConfiguration.ComposeFile);
if (fileInfo.Exists)
{
await container.CopyAsync(fileInfo, ".", Unix.FileMode644, ct)
.ConfigureAwait(false);
}
await container.ExecAsync(new[] { "docker", "compose", "up", "-d" }, ct)
.ConfigureAwait(false);
}
}
}
71 changes: 71 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Testcontainers.DockerCompose;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class DockerComposeConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeConfiguration" /> class.
/// </summary>
/// <param name="composeFile">The fully qualified path to the compose file.</param>
/// <param name="localCompose">Whether the local compose will be used.</param>
public DockerComposeConfiguration(
string composeFile = null,
bool localCompose = false)
{
ComposeFile = composeFile;
LocalCompose = localCompose;
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DockerComposeConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DockerComposeConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DockerComposeConfiguration(DockerComposeConfiguration resourceConfiguration)
: this(new DockerComposeConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public DockerComposeConfiguration(DockerComposeConfiguration oldValue, DockerComposeConfiguration newValue)
: base(oldValue, newValue)
{
ComposeFile = BuildConfiguration.Combine(oldValue.ComposeFile, newValue.ComposeFile);
LocalCompose = BuildConfiguration.Combine(oldValue.LocalCompose, newValue.LocalCompose);
}

/// <summary>
/// Gets the path to the compose file.
/// </summary>
public string ComposeFile { get; }

/// <summary>
/// Indicates whether local compose is enabled.
/// </summary>
public bool LocalCompose { get; }
}
30 changes: 30 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Testcontainers.DockerCompose;

public class DockerComposeContainer : DockerContainer
{
private readonly IContainer _proxyContainer;

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeContainer" /> class.
/// </summary>
/// <param name="configuration"></param>
/// <param name="logger"></param>
public DockerComposeContainer(DockerComposeConfiguration configuration, ILogger logger) : base(configuration, logger)
{
_proxyContainer = configuration.LocalCompose
? new DockerComposeLocal(configuration, logger)
: new DockerComposeRemote(configuration, logger);
}

/// <inheritdoc />
public override async Task StartAsync(CancellationToken ct = default)
{
await _proxyContainer.StartAsync(ct);
}

/// <inheritdoc />
public override async Task StopAsync(CancellationToken ct = default)
{
await _proxyContainer.StopAsync(ct);
}
}
40 changes: 40 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeLocal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Testcontainers.DockerCompose;

[PublicAPI]
public sealed class DockerComposeLocal : DockerContainer
{
private readonly string _dockerComposeBinary =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "docker-compose.exe" : "docker-compose";

/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeRemote" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public DockerComposeLocal(DockerComposeConfiguration configuration, ILogger logger) : base(configuration, logger)
{
}

/// <summary>
/// Gets the runtime configuration.
/// </summary>
public DockerComposeConfiguration RuntimeConfiguration => _configuration as DockerComposeConfiguration;

/// <inheritdoc />
public override async Task StartAsync(CancellationToken ct = default)
{
await Cli.Wrap(_dockerComposeBinary)
.WithArguments(new[] {"up", "-d"})
.WithWorkingDirectory(Path.GetDirectoryName(RuntimeConfiguration.ComposeFile)!)
.ExecuteBufferedAsync();
}

/// <inheritdoc />
public override async Task StopAsync(CancellationToken ct = default)
{
await Cli.Wrap(_dockerComposeBinary)
.WithArguments(new[] {"down"})
.WithWorkingDirectory(Path.GetDirectoryName(RuntimeConfiguration.ComposeFile)!)
.ExecuteBufferedAsync();
}
}
29 changes: 29 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeRemote.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Testcontainers.DockerCompose;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public class DockerComposeRemote : DockerContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="DockerComposeRemote" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public DockerComposeRemote(DockerComposeConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
}

/// <summary>
/// Gets the runtime configuration.
/// </summary>
public DockerComposeConfiguration RuntimeConfiguration => _configuration as DockerComposeConfiguration;

/// <inheritdoc />
public override async Task StopAsync(CancellationToken ct = default)
{
await ExecAsync(new[] { "docker", "compose", "down"}, ct)
.ConfigureAwait(false);
await base.StopAsync(ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
<ProjectReference Include="..\..\tests\Testcontainers.Commons\Testcontainers.Commons.csproj" />
</ItemGroup>
</Project>
18 changes: 18 additions & 0 deletions src/Testcontainers.DockerCompose/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Threading;
global using System.Threading.Tasks;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using JetBrains.Annotations;
global using Microsoft.Extensions.Logging;
global using System.Diagnostics;
global using System.Linq;
global using System.Runtime.InteropServices;
global using CliWrap;
global using CliWrap.Buffered;
global using DotNet.Testcontainers.Commons;
4 changes: 2 additions & 2 deletions src/Testcontainers/Containers/DockerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public class DockerContainer : Resource, IContainer

private readonly ITestcontainersClient _client;

private readonly IContainerConfiguration _configuration;
protected ContainerInspectResponse _container = new ContainerInspectResponse();

private ContainerInspectResponse _container = new ContainerInspectResponse();
protected readonly IContainerConfiguration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="DockerContainer" /> class.
Expand Down
1 change: 1 addition & 0 deletions tests/Testcontainers.DockerCompose.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
Loading

0 comments on commit eb986f4

Please sign in to comment.