Skip to content

Commit

Permalink
feat: Add Db2 module (#1237)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
kevin0x90 and HofmeisterAn authored Feb 14, 2025
1 parent 13dcbe4 commit 7715969
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
{ name: "Testcontainers.CosmosDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Couchbase", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.CouchDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Db2", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.DynamoDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Elasticsearch", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.EventStoreDb", runs-on: "ubuntu-22.04" },
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
<PackageVersion Include="Confluent.SchemaRegistry" Version="2.8.0"/>
<PackageVersion Include="Consul" Version="1.6.10.9"/>
<PackageVersion Include="CouchbaseNetClient" Version="3.6.4"/>
<PackageVersion Include="Net.IBM.Data.Db2-lnx" Version="9.0.0.100"/>
<PackageVersion Include="Net.IBM.Data.Db2" Version="9.0.0.100"/>
<PackageVersion Include="DotPulsar" Version="3.3.2"/>
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="8.16.3"/>
<PackageVersion Include="EventStore.Client.Grpc.Streams" Version="22.0.0"/>
Expand Down
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Couchbase",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.CouchDb", "src\Testcontainers.CouchDb\Testcontainers.CouchDb.csproj", "{DCECB1F6-D9AA-431F-AE42-25D56B9E7DFC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Db2", "src\Testcontainers.Db2\Testcontainers.Db2.csproj", "{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DynamoDb", "src\Testcontainers.DynamoDb\Testcontainers.DynamoDb.csproj", "{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearch", "src\Testcontainers.Elasticsearch\Testcontainers.Elasticsearch.csproj", "{641DDEA5-B6E0-41E6-BA11-7A28C0913127}"
Expand Down Expand Up @@ -135,6 +137,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.CouchDb.Test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Databases.Tests", "tests\Testcontainers.Databases.Tests\Testcontainers.Databases.Tests.csproj", "{DA54916E-1128-4200-B6AE-9F5BF02D832D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Db2.Tests", "tests\Testcontainers.Db2.Tests\Testcontainers.Db2.Tests.csproj", "{AF9853AB-86E7-49DE-8DF8-454838E90D6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DynamoDb.Tests", "tests\Testcontainers.DynamoDb.Tests\Testcontainers.DynamoDb.Tests.csproj", "{101515E6-74C1-40F9-85C8-871F742A378D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearch.Tests", "tests\Testcontainers.Elasticsearch.Tests\Testcontainers.Elasticsearch.Tests.csproj", "{DD5B3678-468F-4D73-AECE-705E3D66CD43}"
Expand Down Expand Up @@ -266,6 +270,10 @@ Global
{DCECB1F6-D9AA-431F-AE42-25D56B9E7DFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCECB1F6-D9AA-431F-AE42-25D56B9E7DFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCECB1F6-D9AA-431F-AE42-25D56B9E7DFC}.Release|Any CPU.Build.0 = Release|Any CPU
{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9}.Release|Any CPU.Build.0 = Release|Any CPU
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -466,6 +474,10 @@ Global
{DA54916E-1128-4200-B6AE-9F5BF02D832D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA54916E-1128-4200-B6AE-9F5BF02D832D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA54916E-1128-4200-B6AE-9F5BF02D832D}.Release|Any CPU.Build.0 = Release|Any CPU
{AF9853AB-86E7-49DE-8DF8-454838E90D6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF9853AB-86E7-49DE-8DF8-454838E90D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF9853AB-86E7-49DE-8DF8-454838E90D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF9853AB-86E7-49DE-8DF8-454838E90D6F}.Release|Any CPU.Build.0 = Release|Any CPU
{101515E6-74C1-40F9-85C8-871F742A378D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{101515E6-74C1-40F9-85C8-871F742A378D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{101515E6-74C1-40F9-85C8-871F742A378D}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -635,6 +647,7 @@ Global
{A724806F-8C94-4438-8011-04A9A1575318} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{58E94721-2681-4D82-8D94-0B2F9DB0D575} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{DCECB1F6-D9AA-431F-AE42-25D56B9E7DFC} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{ED3C611F-DFE2-4AB7-A323-B500E95B4FF9} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{641DDEA5-B6E0-41E6-BA11-7A28C0913127} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -685,6 +698,7 @@ Global
{809322BA-D690-4F2B-B884-23F895663963} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{E4520FB1-4466-4DCA-AD08-4075102C68D3} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{DA54916E-1128-4200-B6AE-9F5BF02D832D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{AF9853AB-86E7-49DE-8DF8-454838E90D6F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{101515E6-74C1-40F9-85C8-871F742A378D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{DD5B3678-468F-4D73-AECE-705E3D66CD43} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down
31 changes: 31 additions & 0 deletions docs/modules/db2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Db2

[Db2](https://www.ibm.com/db2) is a relational database engine developed by IBM.

Add the following dependency to your project file:

```shell title="NuGet"
dotnet add package Testcontainers.Db2
```

!!! warning

The Linux client dependency, [Net.IBM.Data.Db2-lnx](https://www.nuget.org/packages/Net.IBM.Data.Db2-lnx), requires additional configurations. We use the [Testcontainers.Db2.Tests.targets](https://github.com/testcontainers/testcontainers-dotnet/blob/develop/tests/Testcontainers.Db2.Tests/Testcontainers.Db2.Tests.targets) file to configure the environment variables: `LD_LIBRARY_PATH`, `PATH`, `DB2_CLI_DRIVER_INSTALL_PATH`, at runtime.

You can start an Db2 container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method.

=== "Usage Example"
```csharp
--8<-- "tests/Testcontainers.Db2.Tests/Db2ContainerTest.cs:UseDb2Container"
```

The test example uses the following NuGet dependencies:

=== "Package References"
```xml
--8<-- "tests/Testcontainers.Db2.Tests/Testcontainers.Db2.Tests.csproj:PackageReferences"
```

To execute the tests, use the command `dotnet test` from a terminal.

--8<-- "docs/modules/_call_out_test_projects.txt"
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ nav:
- examples/aspnet.md
- Modules:
- modules/index.md
- modules/db2.md
- modules/elasticsearch.md
- modules/mongodb.md
- modules/mssql.md
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Db2/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
149 changes: 149 additions & 0 deletions src/Testcontainers.Db2/Db2Builder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
namespace Testcontainers.Db2;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class Db2Builder : ContainerBuilder<Db2Builder, Db2Container, Db2Configuration>
{
public const string Db2Image = "icr.io/db2_community/db2:12.1.0.0";

public const ushort Db2Port = 50000;

public const string DefaultDatabase = "test";

public const string DefaultUsername = "db2inst1";

public const string DefaultPassword = "db2inst1";

private const string AcceptLicenseAgreementEnvVar = "LICENSE";

private const string AcceptLicenseAgreement = "accept";

private const string DeclineLicenseAgreement = "decline";

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

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

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

/// <summary>
/// Accepts the license agreement.
/// </summary>
/// <remarks>
/// When <paramref name="acceptLicenseAgreement" /> is set to <c>true</c>, the Db2 <see href="www.ibm.com/terms/?id=L-SNMD-UVTL8R">license</see> is accepted.
/// </remarks>
/// <param name="acceptLicenseAgreement">A boolean value indicating whether the Db2 license agreement is accepted.</param>
/// <returns>A configured instance of <see cref="Db2Builder" />.</returns>
public Db2Builder WithAcceptLicenseAgreement(bool acceptLicenseAgreement)
{
var licenseAgreement = acceptLicenseAgreement ? AcceptLicenseAgreement : DeclineLicenseAgreement;
return WithEnvironment(AcceptLicenseAgreementEnvVar, licenseAgreement);
}

/// <summary>
/// Sets the Db2 database name.
/// </summary>
/// <param name="database">The Db2 database.</param>
/// <returns>A configured instance of <see cref="Db2Builder" />.</returns>
public Db2Builder WithDatabase(string database)
{
return Merge(DockerResourceConfiguration, new Db2Configuration(database: database))
.WithEnvironment("DBNAME", database);
}

/// <summary>
/// Sets the Db2 username.
/// </summary>
/// <param name="username">The Db2 username.</param>
/// <returns>A configured instance of <see cref="Db2Builder" />.</returns>
public Db2Builder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new Db2Configuration(username: username))
.WithEnvironment("DB2INSTANCE", username)
.WithTmpfsMount(string.Join("/", string.Empty, "home", username, "data"));
}

/// <summary>
/// Sets the Db2 password.
/// </summary>
/// <param name="password">The Db2 password.</param>
/// <returns>A configured instance of <see cref="Db2Builder" />.</returns>
public Db2Builder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new Db2Configuration(password: password))
.WithEnvironment("DB2INST1_PASSWORD", password);
}

/// <inheritdoc />
public override Db2Container Build()
{
Validate();
return new Db2Container(DockerResourceConfiguration);
}

/// <inheritdoc />
protected override Db2Builder Init() => base.Init()
.WithImage(Db2Image)
.WithPortBinding(Db2Port, true)
.WithDatabase(DefaultDatabase)
.WithUsername(DefaultUsername)
.WithPassword(DefaultPassword)
.WithPrivileged(true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Setup has completed."));

/// <inheritdoc />
protected override void Validate()
{
const string message = "The image '{0}' requires you to accept a license agreement.";

base.Validate();

Predicate<Db2Configuration> licenseAgreementNotAccepted = value =>
!value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal);

_ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image))
.ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => throw new ArgumentException(string.Format(message, DockerResourceConfiguration.Image.FullName), argument.Name));

_ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username))
.NotNull()
.NotEmpty();

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

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

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

/// <inheritdoc />
protected override Db2Builder Merge(Db2Configuration oldValue, Db2Configuration newValue)
{
return new Db2Builder(new Db2Configuration(oldValue, newValue));
}
}
80 changes: 80 additions & 0 deletions src/Testcontainers.Db2/Db2Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Testcontainers.Db2;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class Db2Configuration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="Db2Configuration" /> class.
/// </summary>
/// <param name="database">The Db2 database.</param>
/// <param name="username">The Db2 username.</param>
/// <param name="password">The Db2 password.</param>
public Db2Configuration(
string database = null,
string username = null,
string password = null)
{
Database = database;
Username = username;
Password = password;
}

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

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

/// <summary>
/// Gets the Db2 database.
/// </summary>
public string Database { get; }

/// <summary>
/// Gets the Db2 username.
/// </summary>
public string Username { get; }

/// <summary>
/// Gets the Db2 password.
/// </summary>
public string Password { get; }
}
48 changes: 48 additions & 0 deletions src/Testcontainers.Db2/Db2Container.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Testcontainers.Db2;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class Db2Container : DockerContainer, IDatabaseContainer
{
private readonly Db2Configuration _configuration;

public Db2Container(Db2Configuration configuration) : base(configuration)
{
_configuration = configuration;
}

/// <summary>
/// Gets the Db2 connection string.
/// </summary>
/// <returns>The Db2 connection string.</returns>
public string GetConnectionString()
{
var properties = new Dictionary<string, string>();
properties.Add("Server", Hostname + ":" + GetMappedPublicPort(Db2Builder.Db2Port));
properties.Add("Database", _configuration.Database);
properties.Add("UID", _configuration.Username);
properties.Add("PWD", _configuration.Password);
return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value)));
}

/// <summary>
/// Executes the SQL script in the Db2 container.
/// </summary>
/// <param name="scriptContent">The content of the SQL script to execute.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the SQL script has been executed.</returns>
public async Task<ExecResult> ExecScriptAsync(string scriptContent, CancellationToken ct = default)
{
const string db2ShellCommandFormat = "su - {1} -c \"db2 connect to {0} && db2 -tvf '{2}'\"";

var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

var db2ShellCommand = string.Format(db2ShellCommandFormat, _configuration.Database, _configuration.Username, scriptFilePath);

await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct)
.ConfigureAwait(false);

return await ExecAsync(new [] { "/bin/sh", "-c", db2ShellCommand}, ct)
.ConfigureAwait(false);
}
}
Loading

0 comments on commit 7715969

Please sign in to comment.