Skip to content

Commit 594ae5d

Browse files
authored
feat: Add Socat container implementation (#1416)
1 parent 6526e5a commit 594ae5d

File tree

8 files changed

+284
-5
lines changed

8 files changed

+284
-5
lines changed

src/Testcontainers/Builders/BuildConfiguration.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public static T Combine<T>(T oldValue, T newValue)
2727
/// <typeparam name="T">Type of <see cref="IEnumerable{T}" />.</typeparam>
2828
/// <returns>An updated configuration.</returns>
2929
public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T> newValue)
30-
where T : class
3130
{
3231
if (newValue == null && oldValue == null)
3332
{
@@ -51,7 +50,6 @@ public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T>
5150
/// <typeparam name="T">Type of <see cref="IReadOnlyList{T}" />.</typeparam>
5251
/// <returns>An updated configuration.</returns>
5352
public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyList<T> newValue)
54-
where T : class
5553
{
5654
if (newValue == null && oldValue == null)
5755
{
@@ -75,8 +73,6 @@ public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyLi
7573
/// <typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
7674
/// <returns>An updated configuration.</returns>
7775
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, IReadOnlyDictionary<TKey, TValue> newValue)
78-
where TKey : class
79-
where TValue : class
8076
{
8177
if (newValue == null && oldValue == null)
8278
{
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
namespace DotNet.Testcontainers.Containers
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Docker.DotNet.Models;
7+
using DotNet.Testcontainers.Builders;
8+
using DotNet.Testcontainers.Configurations;
9+
using JetBrains.Annotations;
10+
11+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
12+
[PublicAPI]
13+
public sealed class SocatBuilder : ContainerBuilder<SocatBuilder, SocatContainer, SocatConfiguration>
14+
{
15+
public const string SocatImage = "alpine/socat:1.7.4.3-r0";
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="SocatBuilder" /> class.
19+
/// </summary>
20+
public SocatBuilder()
21+
: this(new SocatConfiguration())
22+
{
23+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="SocatBuilder" /> class.
28+
/// </summary>
29+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
30+
private SocatBuilder(SocatConfiguration resourceConfiguration)
31+
: base(resourceConfiguration)
32+
{
33+
DockerResourceConfiguration = resourceConfiguration;
34+
}
35+
36+
/// <inheritdoc />
37+
protected override SocatConfiguration DockerResourceConfiguration { get; }
38+
39+
/// <summary>
40+
/// Sets the Socat target.
41+
/// </summary>
42+
/// <param name="exposedPort">The Socat exposed port.</param>
43+
/// <param name="host">The Socat target host.</param>
44+
/// <returns>A configured instance of <see cref="SocatBuilder" />.</returns>
45+
public SocatBuilder WithTarget(int exposedPort, string host)
46+
{
47+
return WithTarget(exposedPort, host, exposedPort);
48+
}
49+
50+
/// <summary>
51+
/// Sets the Socat target.
52+
/// </summary>
53+
/// <param name="exposedPort">The Socat exposed port.</param>
54+
/// <param name="host">The Socat target host.</param>
55+
/// <param name="internalPort">The Socat target port.</param>
56+
/// <returns>A configured instance of <see cref="SocatBuilder" />.</returns>
57+
public SocatBuilder WithTarget(int exposedPort, string host, int internalPort)
58+
{
59+
var targets = new Dictionary<int, string> { { exposedPort, $"{host}:{internalPort}" } };
60+
return Merge(DockerResourceConfiguration, new SocatConfiguration(targets))
61+
.WithPortBinding(exposedPort, true);
62+
}
63+
64+
/// <inheritdoc />
65+
public override SocatContainer Build()
66+
{
67+
Validate();
68+
69+
const string argument = "socat TCP-LISTEN:{0},fork,reuseaddr TCP:{1}";
70+
71+
var command = string.Join(" & ", DockerResourceConfiguration.Targets
72+
.Select(item => string.Format(argument, item.Key, item.Value)));
73+
74+
var waitStrategy = DockerResourceConfiguration.Targets
75+
.Aggregate(Wait.ForUnixContainer(), (waitStrategy, item) => waitStrategy.UntilPortIsAvailable(item.Key));
76+
77+
var socatBuilder = WithCommand(command).WithWaitStrategy(waitStrategy);
78+
return new SocatContainer(socatBuilder.DockerResourceConfiguration);
79+
}
80+
81+
/// <inheritdoc />
82+
protected override SocatBuilder Init()
83+
{
84+
return base.Init()
85+
.WithImage(SocatImage)
86+
.WithEntrypoint("/bin/sh", "-c");
87+
}
88+
89+
/// <inheritdoc />
90+
protected override void Validate()
91+
{
92+
const string message = "Missing targets. One target must be specified to be created.";
93+
94+
base.Validate();
95+
96+
_ = Guard.Argument(DockerResourceConfiguration.Targets, nameof(DockerResourceConfiguration.Targets))
97+
.ThrowIf(argument => argument.Value.Count == 0, argument => new ArgumentException(message, argument.Name));
98+
}
99+
100+
/// <inheritdoc />
101+
protected override SocatBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
102+
{
103+
return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration));
104+
}
105+
106+
/// <inheritdoc />
107+
protected override SocatBuilder Clone(IContainerConfiguration resourceConfiguration)
108+
{
109+
return Merge(DockerResourceConfiguration, new SocatConfiguration(resourceConfiguration));
110+
}
111+
112+
/// <inheritdoc />
113+
protected override SocatBuilder Merge(SocatConfiguration oldValue, SocatConfiguration newValue)
114+
{
115+
return new SocatBuilder(new SocatConfiguration(oldValue, newValue));
116+
}
117+
}
118+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace DotNet.Testcontainers.Containers
2+
{
3+
using System.Collections.Generic;
4+
using Docker.DotNet.Models;
5+
using DotNet.Testcontainers.Builders;
6+
using DotNet.Testcontainers.Configurations;
7+
using JetBrains.Annotations;
8+
9+
/// <inheritdoc cref="ContainerConfiguration" />
10+
[PublicAPI]
11+
public sealed class SocatConfiguration : ContainerConfiguration
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="SocatConfiguration" /> class.
15+
/// </summary>
16+
/// <param name="targets">A list of target addresses.</param>
17+
public SocatConfiguration(
18+
IReadOnlyDictionary<int, string> targets = null)
19+
{
20+
Targets = targets;
21+
}
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="SocatConfiguration" /> class.
25+
/// </summary>
26+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
27+
public SocatConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
28+
: base(resourceConfiguration)
29+
{
30+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
31+
}
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="SocatConfiguration" /> class.
35+
/// </summary>
36+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
37+
public SocatConfiguration(IContainerConfiguration resourceConfiguration)
38+
: base(resourceConfiguration)
39+
{
40+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
41+
}
42+
43+
/// <summary>
44+
/// Initializes a new instance of the <see cref="SocatConfiguration" /> class.
45+
/// </summary>
46+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
47+
public SocatConfiguration(SocatConfiguration resourceConfiguration)
48+
: this(new SocatConfiguration(), resourceConfiguration)
49+
{
50+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
51+
}
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="SocatConfiguration" /> class.
55+
/// </summary>
56+
/// <param name="oldValue">The old Docker resource configuration.</param>
57+
/// <param name="newValue">The new Docker resource configuration.</param>
58+
public SocatConfiguration(SocatConfiguration oldValue, SocatConfiguration newValue)
59+
: base(oldValue, newValue)
60+
{
61+
Targets = BuildConfiguration.Combine(oldValue.Targets, newValue.Targets);
62+
}
63+
64+
/// <summary>
65+
/// Gets a list of target addresses.
66+
/// </summary>
67+
public IReadOnlyDictionary<int, string> Targets { get; }
68+
}
69+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace DotNet.Testcontainers.Containers
2+
{
3+
using JetBrains.Annotations;
4+
5+
/// <inheritdoc cref="DockerContainer" />
6+
[PublicAPI]
7+
public sealed class SocatContainer : DockerContainer
8+
{
9+
private readonly SocatConfiguration _configuration;
10+
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="SocatContainer" /> class.
13+
/// </summary>
14+
/// <param name="configuration">The container configuration.</param>
15+
public SocatContainer(SocatConfiguration configuration)
16+
: base(configuration)
17+
{
18+
_configuration = configuration;
19+
}
20+
}
21+
}

tests/Testcontainers.Commons/CommonImages.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ public static class CommonImages
55
{
66
public static readonly IImage Ryuk = new DockerImage("testcontainers/ryuk:0.9.0");
77

8+
public static readonly IImage HelloWorld = new DockerImage("testcontainers/helloworld:1.2.0");
9+
810
public static readonly IImage Alpine = new DockerImage("alpine:3.17");
911

1012
public static readonly IImage Socat = new DockerImage("alpine/socat:1.8.0.0");
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace Testcontainers.Tests;
2+
3+
public sealed class SocatContainerTest : IAsyncLifetime
4+
{
5+
private const string HelloWorldAlias = "hello-world-container";
6+
7+
private readonly INetwork _network;
8+
9+
private readonly IContainer _helloWorldContainer;
10+
11+
private readonly IContainer _socatContainer;
12+
13+
public SocatContainerTest()
14+
{
15+
_network = new NetworkBuilder()
16+
.Build();
17+
18+
_helloWorldContainer = new ContainerBuilder()
19+
.WithImage(CommonImages.HelloWorld)
20+
.WithNetwork(_network)
21+
.WithNetworkAliases(HelloWorldAlias)
22+
.Build();
23+
24+
_socatContainer = new SocatBuilder()
25+
.WithNetwork(_network)
26+
.WithTarget(8080, HelloWorldAlias)
27+
.WithTarget(8081, HelloWorldAlias, 8080)
28+
.Build();
29+
}
30+
31+
public async Task InitializeAsync()
32+
{
33+
await _helloWorldContainer.StartAsync()
34+
.ConfigureAwait(false);
35+
36+
await _socatContainer.StartAsync()
37+
.ConfigureAwait(false);
38+
}
39+
40+
public async Task DisposeAsync()
41+
{
42+
await _socatContainer.DisposeAsync()
43+
.ConfigureAwait(false);
44+
45+
await _helloWorldContainer.DisposeAsync()
46+
.ConfigureAwait(false);
47+
48+
await _network.DisposeAsync()
49+
.ConfigureAwait(false);
50+
}
51+
52+
[Theory]
53+
[InlineData(8080)]
54+
[InlineData(8081)]
55+
public async Task RequestTargetContainer(int containerPort)
56+
{
57+
// Given
58+
using var httpClient = new HttpClient();
59+
httpClient.BaseAddress = new UriBuilder(Uri.UriSchemeHttp, _socatContainer.Hostname, _socatContainer.GetMappedPublicPort(containerPort)).Uri;
60+
61+
// When
62+
using var httpResponse = await httpClient.GetAsync("/ping")
63+
.ConfigureAwait(true);
64+
65+
var response = await httpResponse.Content.ReadAsStringAsync()
66+
.ConfigureAwait(true);
67+
68+
// Then
69+
Assert.Equal("PONG", response);
70+
}
71+
}

tests/Testcontainers.Platform.Linux.Tests/Usings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
global using System.IO;
55
global using System.Linq;
66
global using System.Net;
7+
global using System.Net.Http;
78
global using System.Net.Sockets;
89
global using System.Text;
910
global using System.Threading;
@@ -16,6 +17,7 @@
1617
global using DotNet.Testcontainers.Commons;
1718
global using DotNet.Testcontainers.Configurations;
1819
global using DotNet.Testcontainers.Containers;
20+
global using DotNet.Testcontainers.Networks;
1921
global using ICSharpCode.SharpZipLib.Tar;
2022
global using JetBrains.Annotations;
2123
global using Microsoft.Extensions.Logging.Abstractions;

tests/Testcontainers.WebDriver.Tests/WebDriverContainerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public abstract class WebDriverContainerTest : IAsyncLifetime
1111
private WebDriverContainerTest(WebDriverContainer webDriverContainer)
1212
{
1313
_helloWorldContainer = new ContainerBuilder()
14-
.WithImage("testcontainers/helloworld:1.1.0")
14+
.WithImage(CommonImages.HelloWorld)
1515
.WithNetwork(webDriverContainer.GetNetwork())
1616
.WithNetworkAliases(_helloWorldBaseAddress.Host)
1717
.WithPortBinding(_helloWorldBaseAddress.Port, true)

0 commit comments

Comments
 (0)