Skip to content

Commit 8617ccf

Browse files
feat: Adds Toxiproxy module (#1454)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent c396748 commit 8617ccf

File tree

18 files changed

+420
-5
lines changed

18 files changed

+420
-5
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@
8787
<PackageVersion Include="RavenDB.Client" Version="5.4.100"/>
8888
<PackageVersion Include="Selenium.WebDriver" Version="4.8.1"/>
8989
<PackageVersion Include="StackExchange.Redis" Version="2.6.90"/>
90+
<PackageVersion Include="Toxiproxy.Net" Version="2.0.1"/>
9091
</ItemGroup>
9192
</Project>

Testcontainers.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus",
126126
EndProject
127127
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}"
128128
EndProject
129+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy", "src\Testcontainers.Toxiproxy\Testcontainers.Toxiproxy.csproj", "{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}"
130+
EndProject
129131
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Typesense", "src\Testcontainers.Typesense\Testcontainers.Typesense.csproj", "{E044A94A-3081-4EE4-8DC6-81601F96DA14}"
130132
EndProject
131133
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}"
@@ -262,6 +264,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp.Tests",
262264
EndProject
263265
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}"
264266
EndProject
267+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Toxiproxy.Tests", "tests\Testcontainers.Toxiproxy.Tests\Testcontainers.Toxiproxy.Tests.csproj", "{10726AAA-E93F-4B40-A05E-28308423DABE}"
268+
EndProject
265269
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Typesense.Tests", "tests\Testcontainers.Typesense.Tests\Testcontainers.Typesense.Tests.csproj", "{73CC8E45-5608-1398-4029-0802428B5565}"
266270
EndProject
267271
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate.Tests", "tests\Testcontainers.Weaviate.Tests\Testcontainers.Weaviate.Tests.csproj", "{DDB41BC8-5826-4D97-9C5F-001151E3FFD6}"
@@ -490,6 +494,10 @@ Global
490494
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
491495
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
492496
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.Build.0 = Release|Any CPU
497+
{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
498+
{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
499+
{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
500+
{65A47BA4-4DC8-4206-9B00-CBC87FC944FC}.Release|Any CPU.Build.0 = Release|Any CPU
493501
{E044A94A-3081-4EE4-8DC6-81601F96DA14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
494502
{E044A94A-3081-4EE4-8DC6-81601F96DA14}.Debug|Any CPU.Build.0 = Debug|Any CPU
495503
{E044A94A-3081-4EE4-8DC6-81601F96DA14}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -762,6 +770,10 @@ Global
762770
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
763771
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
764772
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.Build.0 = Release|Any CPU
773+
{10726AAA-E93F-4B40-A05E-28308423DABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
774+
{10726AAA-E93F-4B40-A05E-28308423DABE}.Debug|Any CPU.Build.0 = Debug|Any CPU
775+
{10726AAA-E93F-4B40-A05E-28308423DABE}.Release|Any CPU.ActiveCfg = Release|Any CPU
776+
{10726AAA-E93F-4B40-A05E-28308423DABE}.Release|Any CPU.Build.0 = Release|Any CPU
765777
{73CC8E45-5608-1398-4029-0802428B5565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
766778
{73CC8E45-5608-1398-4029-0802428B5565}.Debug|Any CPU.Build.0 = Debug|Any CPU
767779
{73CC8E45-5608-1398-4029-0802428B5565}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -841,6 +853,7 @@ Global
841853
{45D6F69C-4D87-4130-AA90-0DB2F7460DAE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
842854
{2E39E532-B81E-4B48-A004-FAE18EDF9E79} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
843855
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
856+
{65A47BA4-4DC8-4206-9B00-CBC87FC944FC} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
844857
{E044A94A-3081-4EE4-8DC6-81601F96DA14} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
845858
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
846859
{64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -909,6 +922,7 @@ Global
909922
{232DD918-46ED-4BA8-B383-1A9146D83064} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
910923
{B73C3CC0-9F16-4B34-92BE-6EC0853912C5} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
911924
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
925+
{10726AAA-E93F-4B40-A05E-28308423DABE} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
912926
{73CC8E45-5608-1398-4029-0802428B5565} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
913927
{DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
914928
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}

docs/modules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ await moduleNameContainer.StartAsync();
7272
| Redpanda | `docker.redpanda.com/redpandadata/redpanda:v22.2.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redpanda) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redpanda) |
7373
| Sftp | `atmoz/sftp:alpine` | [NuGet](https://www.nuget.org/packages/Testcontainers.Sftp) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Sftp) |
7474
| SQL Server | `mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04` | [NuGet](https://www.nuget.org/packages/Testcontainers.MsSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MsSql) |
75+
| Toxiproxy | `ghcr.io/shopify/toxiproxy:2.12.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Toxiproxy) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Toxiproxy) |
7576
| Typesense | `typesense/typesense:28.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Typesense) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Typesense) |
7677
| Weaviate | `semitechnologies/weaviate:1.26.14` | [NuGet](https://www.nuget.org/packages/Testcontainers.Weaviate) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Weaviate) |
7778
| WebDriver | `selenium/standalone-chrome:110.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.WebDriver) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.WebDriver) |

docs/modules/toxiproxy.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Toxiproxy
2+
3+
[Toxiproxy](https://github.com/Shopify/toxiproxy) is a proxy to simulate network and system conditions for chaos and resiliency testing. It can simulate latency, timeouts, bandwidth limits, and connection issues between services.
4+
5+
Add the following dependency to your project file:
6+
7+
```shell title="NuGet"
8+
dotnet add package Testcontainers.Toxiproxy
9+
```
10+
11+
You can start a Toxiproxy container instance from any .NET application. This example demonstrates how to test network conditions by proxying traffic through Toxiproxy to a Redis container. The test creates both containers in the same network and configures Toxiproxy to redirect traffic from the test host to the Redis container.
12+
13+
=== "Create Container Instance"
14+
```csharp
15+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs:CreateToxiproxyContainer"
16+
```
17+
18+
This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the containers. Both the Redis and Toxiproxy containers are started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the containers are removed in the `DisposeAsync` method.
19+
20+
=== "Usage Example"
21+
```csharp
22+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs:UseToxiproxyContainer"
23+
```
24+
25+
## How it works
26+
27+
To test network conditions with Toxiproxy, you need to configure the communication between your test host, Toxiproxy, and the service you want to test:
28+
29+
1. Place both containers in the same network so the Toxiproxy container and the service container (Redis in this example) can communicate with each other.
30+
31+
2. Configure the Toxiproxy proxy to redirect traffic from the test host to the service container. The proxy's `Listen` address specifies where Toxiproxy listens for incoming connections, and the `Upstream` address specifies the target service.
32+
33+
=== "Proxy Configuration"
34+
```csharp
35+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs:ProxyConfiguration"
36+
```
37+
38+
3. Connect through Toxiproxy using its hostname and one of its proxied ports. The Toxiproxy module initializes `32` ports starting from `8666` that can be used to configure proxies and redirect traffic. In the example, the Redis connection uses the Toxiproxy container's hostname and port instead of connecting directly to Redis.
39+
40+
=== "Connect Through Toxiproxy"
41+
```csharp
42+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs:ConnectThroughToxiproxy"
43+
```
44+
45+
4. Apply toxics to the proxy to simulate network conditions. For example, a latency toxic to the downstream traffic.
46+
47+
=== "Toxic Configuration"
48+
```csharp
49+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/ToxiproxyContainerTest.cs:ToxicConfiguration"
50+
```
51+
52+
!!! tip
53+
Toxiproxy allows you to configure the `Toxicity` property, which determines the probability (0.0 to 1.0) that a toxic will be applied to the connection. A value of 1.0 means the toxic is applied 100% of the time, while 0.5 would apply it to approximately 50% of requests.
54+
55+
The test example uses the following NuGet dependencies:
56+
57+
=== "Package References"
58+
```xml
59+
--8<-- "tests/Testcontainers.Toxiproxy.Tests/Testcontainers.Toxiproxy.Tests.csproj:PackageReferences"
60+
```
61+
62+
To execute the tests, use the command `dotnet test` from a terminal.
63+
64+
--8<-- "docs/modules/_call_out_test_projects.txt"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,6 @@ nav:
6565
- modules/postgres.md
6666
- modules/qdrant.md
6767
- modules/rabbitmq.md
68+
- modules/toxiproxy.md
6869
- contributing.md
6970
- contributing_docs.md

src/Testcontainers.Grafana/GrafanaContainer.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ namespace Testcontainers.Grafana;
44
[PublicAPI]
55
public sealed class GrafanaContainer : DockerContainer
66
{
7-
private readonly GrafanaConfiguration _configuration;
8-
97
/// <summary>
108
/// Initializes a new instance of the <see cref="GrafanaContainer" /> class.
119
/// </summary>
1210
/// <param name="configuration">The container configuration.</param>
1311
public GrafanaContainer(GrafanaConfiguration configuration)
1412
: base(configuration)
1513
{
16-
_configuration = configuration;
1714
}
1815

1916
/// <summary>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
8+
</ItemGroup>
9+
<ItemGroup>
10+
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
11+
</ItemGroup>
12+
</Project>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
namespace Testcontainers.Toxiproxy;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class ToxiproxyBuilder : ContainerBuilder<ToxiproxyBuilder, ToxiproxyContainer, ToxiproxyConfiguration>
6+
{
7+
public const string ToxiproxyImage = "ghcr.io/shopify/toxiproxy:2.12.0";
8+
9+
public const ushort ToxiproxyControlPort = 8474;
10+
11+
public const ushort FirstProxiedPort = 8666;
12+
13+
public const ushort LastProxiedPort = 8666 + 32;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ToxiproxyBuilder" /> class.
17+
/// </summary>
18+
public ToxiproxyBuilder()
19+
: this(new ToxiproxyConfiguration())
20+
{
21+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="ToxiproxyBuilder" /> class.
26+
/// </summary>
27+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
28+
private ToxiproxyBuilder(ToxiproxyConfiguration resourceConfiguration)
29+
: base(resourceConfiguration)
30+
{
31+
DockerResourceConfiguration = resourceConfiguration;
32+
}
33+
34+
/// <inheritdoc />
35+
protected override ToxiproxyConfiguration DockerResourceConfiguration { get; }
36+
37+
/// <inheritdoc />
38+
public override ToxiproxyContainer Build()
39+
{
40+
Validate();
41+
return new ToxiproxyContainer(DockerResourceConfiguration);
42+
}
43+
44+
/// <inheritdoc />
45+
protected override ToxiproxyBuilder Init()
46+
{
47+
const int count = LastProxiedPort - FirstProxiedPort;
48+
49+
var toxiproxyBuilder = base.Init()
50+
.WithImage(ToxiproxyImage)
51+
.WithPortBinding(ToxiproxyControlPort, true)
52+
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
53+
request.ForPath("/version").ForPort(ToxiproxyControlPort)));
54+
55+
// Allows up to 32 ports to be proxied (arbitrary value). The ports are
56+
// exposed here, but whether Toxiproxy listens on them is controlled at
57+
// runtime when configuring the proxy.
58+
return Enumerable.Range(FirstProxiedPort, count)
59+
.Aggregate(toxiproxyBuilder, (builder, port) =>
60+
builder.WithPortBinding(port, true));
61+
}
62+
63+
/// <inheritdoc />
64+
protected override ToxiproxyBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
65+
{
66+
return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(resourceConfiguration));
67+
}
68+
69+
/// <inheritdoc />
70+
protected override ToxiproxyBuilder Clone(IContainerConfiguration resourceConfiguration)
71+
{
72+
return Merge(DockerResourceConfiguration, new ToxiproxyConfiguration(resourceConfiguration));
73+
}
74+
75+
/// <inheritdoc />
76+
protected override ToxiproxyBuilder Merge(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue)
77+
{
78+
return new ToxiproxyBuilder(new ToxiproxyConfiguration(oldValue, newValue));
79+
}
80+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace Testcontainers.Toxiproxy;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class ToxiproxyConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="ToxiproxyConfiguration" /> class.
9+
/// </summary>
10+
public ToxiproxyConfiguration()
11+
{
12+
}
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="ToxiproxyConfiguration" /> class.
16+
/// </summary>
17+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
18+
public ToxiproxyConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
19+
: base(resourceConfiguration)
20+
{
21+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="ToxiproxyConfiguration" /> class.
26+
/// </summary>
27+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
28+
public ToxiproxyConfiguration(IContainerConfiguration resourceConfiguration)
29+
: base(resourceConfiguration)
30+
{
31+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
32+
}
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="ToxiproxyConfiguration" /> class.
36+
/// </summary>
37+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
38+
public ToxiproxyConfiguration(ToxiproxyConfiguration resourceConfiguration)
39+
: this(new ToxiproxyConfiguration(), resourceConfiguration)
40+
{
41+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
42+
}
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="ToxiproxyConfiguration" /> class.
46+
/// </summary>
47+
/// <param name="oldValue">The old Docker resource configuration.</param>
48+
/// <param name="newValue">The new Docker resource configuration.</param>
49+
public ToxiproxyConfiguration(ToxiproxyConfiguration oldValue, ToxiproxyConfiguration newValue)
50+
: base(oldValue, newValue)
51+
{
52+
}
53+
}

0 commit comments

Comments
 (0)