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 docker compose support (#122) #1082

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@
<PackageVersion Include="RavenDB.Client" Version="5.4.100"/>
<PackageVersion Include="Selenium.WebDriver" Version="4.8.1"/>
<PackageVersion Include="StackExchange.Redis" Version="2.6.90"/>
<PackageVersion Include="CliWrap" Version="3.6.4"/>
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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 @@ -556,6 +560,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 @@ -647,5 +659,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
64 changes: 64 additions & 0 deletions src/Testcontainers.DockerCompose/DockerCompose.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Testcontainers.DockerCompose;

/// <inheritdoc cref="DockerContainer" />
internal abstract class DockerCompose : DockerContainer
{
private static readonly IList<string> AppLineArgs = new[] { "docker", "compose"};

private static readonly IList<string> StartCommandLineArgs = new[] { "up", "-d" };
private static readonly IList<string> StopCommandLineArgs = new[] { "down" };

private static readonly IList<string> RemoveImagesArgs = new[] { "--rmi" };
private static readonly IList<string> FilesArgs = new[] { "-f" };

/// <summary>
/// Initializes a new instance of the <see cref="DockerCompose" /> class.
/// </summary>
protected DockerCompose(IContainerConfiguration configuration, ILogger logger) : base(configuration, logger)
{
}

/// <summary>
/// Gets the runtime configuration.
/// </summary>
public DockerComposeConfiguration RuntimeConfiguration => _configuration as DockerComposeConfiguration;
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Builds a command line to start the docker compose
/// </summary>
public IList<string> BuildStartCommandLine()
{
return BuildConfiguration.Combine(BuildIncludeFileCommand(), StartCommandLineArgs).ToList();
}

/// <summary>
/// Builds a command line to stop the docker compose
/// </summary>
public IList<string> BuildStopCommandLine()
{
var removeImagesArgs = RuntimeConfiguration.RemoveImages switch
{
RemoveImages.All => [RemoveImages.All.ToString().ToLower()],
RemoveImages.Local => [RemoveImages.Local.ToString().ToLower()],
_ => Array.Empty<string>(),
};

var stopCommand = BuildConfiguration.Combine(BuildIncludeFileCommand(),
StopCommandLineArgs.AsEnumerable());

return removeImagesArgs.Length > 0
? BuildConfiguration.Combine(
BuildConfiguration.Combine(
stopCommand, RemoveImagesArgs.AsEnumerable()),
removeImagesArgs)
.ToList()
: stopCommand.ToList();
}

private IEnumerable<string> BuildIncludeFileCommand()
{
return BuildConfiguration.Combine(
BuildConfiguration.Combine(AppLineArgs.AsEnumerable(), FilesArgs.AsEnumerable()),
new[] { Path.GetFileName(RuntimeConfiguration.ComposeFile) });
}
}
147 changes: 147 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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>A configured instance of <see cref="DockerComposeBuilder" />.</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">Whether the local compose will be used.</param>
/// <returns>A configured instance of <see cref="DockerComposeBuilder" />.</returns>
public DockerComposeBuilder WithLocalCompose(bool localCompose)
{
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
(localCompose: localCompose));
}

/// <summary>
/// Adds options to the docker-compose command, e.g. docker-compose --compatibility.
/// </summary>
/// <param name="options">Options for the docker-compose command.</param>
/// <returns>A configured instance of <see cref="DockerComposeBuilder" />.</returns>
public DockerComposeBuilder WithOptions(params string[] options) {
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
(options: options));
}

/// <summary>
/// Remove images after containers shutdown.
/// </summary>
/// <param name="removeImages"></param>
/// <returns>A configured instance of <see cref="DockerComposeBuilder" />.</returns>
public DockerComposeBuilder WithRemoveImages(RemoveImages removeImages) {
return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
(removeImages: removeImages));
}
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved


/// <inheritdoc />
protected override DockerComposeBuilder Init()
{
return base.Init()
.WithImage(DockerComposeImage)
.WithEntrypoint(CommonCommands.SleepInfinity)
.WithBindMount(DockerSocketPath, DockerSocketPath, AccessMode.ReadWrite)
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved
.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)
{
throw new FileNotFoundException(NoComposeFile, fileInfo.Name);
}
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved

await container.CopyAsync(fileInfo, ".", Unix.FileMode644, ct)
.ConfigureAwait(false);
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved

await container.ExecAsync(dockerComposeContainer.BuildStartCommandLine(), ct)
.ConfigureAwait(false);
nkz-soft marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
88 changes: 88 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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>
/// <param name="options">Options for the docker-compose command.</param>
/// <param name="removeImages">Options for remove images.</param>
public DockerComposeConfiguration(
string composeFile = null,
bool localCompose = false,
IEnumerable<string> options = null,
RemoveImages removeImages = RemoveImages.None)
{
ComposeFile = composeFile;
LocalCompose = localCompose;
Options = options ?? Array.Empty<string>();
RemoveImages = removeImages;
}

/// <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);
RemoveImages = BuildConfiguration.Combine(oldValue.RemoveImages, newValue.RemoveImages);
}

/// <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; }

/// <summary>
/// Options for the docker-compose command.
/// </summary>
public IEnumerable<string> Options { get; } = Array.Empty<string>();

/// <summary>
/// Options for remove images.
/// </summary>
public RemoveImages RemoveImages { get; } = RemoveImages.None;
}
31 changes: 31 additions & 0 deletions src/Testcontainers.DockerCompose/DockerComposeContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Testcontainers.DockerCompose;

[PublicAPI]
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);
}
}
Loading
Loading