Skip to content

Commit a1e14cb

Browse files
authored
feat: Add SQL Edge module (#812)
1 parent 6688b92 commit a1e14cb

12 files changed

+344
-1
lines changed

Testcontainers.sln

+14
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ResourceReap
101101
EndProject
102102
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}"
103103
EndProject
104+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.SqlEdge", "src\Testcontainers.SqlEdge\Testcontainers.SqlEdge.csproj", "{C95A3B2F-2B28-49A7-8806-731C158BBC21}"
105+
EndProject
106+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.SqlEdge.Tests", "tests\Testcontainers.SqlEdge.Tests\Testcontainers.SqlEdge.Tests.csproj", "{1A1983E6-5297-435F-B467-E8E1F11277D6}"
107+
EndProject
104108
Global
105109
GlobalSection(SolutionConfigurationPlatforms) = preSolution
106110
Debug|Any CPU = Debug|Any CPU
@@ -286,6 +290,14 @@ Global
286290
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
287291
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
288292
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.Build.0 = Release|Any CPU
293+
{C95A3B2F-2B28-49A7-8806-731C158BBC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
294+
{C95A3B2F-2B28-49A7-8806-731C158BBC21}.Debug|Any CPU.Build.0 = Debug|Any CPU
295+
{C95A3B2F-2B28-49A7-8806-731C158BBC21}.Release|Any CPU.ActiveCfg = Release|Any CPU
296+
{C95A3B2F-2B28-49A7-8806-731C158BBC21}.Release|Any CPU.Build.0 = Release|Any CPU
297+
{1A1983E6-5297-435F-B467-E8E1F11277D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
298+
{1A1983E6-5297-435F-B467-E8E1F11277D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
299+
{1A1983E6-5297-435F-B467-E8E1F11277D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
300+
{1A1983E6-5297-435F-B467-E8E1F11277D6}.Release|Any CPU.Build.0 = Release|Any CPU
289301
EndGlobalSection
290302
GlobalSection(NestedProjects) = preSolution
291303
{58E94721-2681-4D82-8D94-0B2F9DB0D575} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -332,5 +344,7 @@ Global
332344
{867BD04E-4670-4FBA-98D5-9F83220E6DFB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
333345
{9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
334346
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
347+
{C95A3B2F-2B28-49A7-8806-731C158BBC21} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
348+
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
335349
EndGlobalSection
336350
EndGlobal
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
namespace Testcontainers.SqlEdge;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class SqlEdgeBuilder : ContainerBuilder<SqlEdgeBuilder, SqlEdgeContainer, SqlEdgeConfiguration>
6+
{
7+
public const string SqlEdgeImage = "mcr.microsoft.com/azure-sql-edge:1.0.7";
8+
9+
public const ushort SqlEdgePort = 1433;
10+
11+
public const string DefaultDatabase = "master";
12+
13+
public const string DefaultUsername = "sa";
14+
15+
public const string DefaultPassword = "yourStrong(!)Password";
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="SqlEdgeBuilder" /> class.
19+
/// </summary>
20+
public SqlEdgeBuilder()
21+
: this(new SqlEdgeConfiguration())
22+
{
23+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="SqlEdgeBuilder" /> class.
28+
/// </summary>
29+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
30+
private SqlEdgeBuilder(SqlEdgeConfiguration resourceConfiguration)
31+
: base(resourceConfiguration)
32+
{
33+
DockerResourceConfiguration = resourceConfiguration;
34+
}
35+
36+
/// <inheritdoc />
37+
protected override SqlEdgeConfiguration DockerResourceConfiguration { get; }
38+
39+
/// <summary>
40+
/// Sets the SqlEdge password.
41+
/// </summary>
42+
/// <param name="password">The SqlEdge password.</param>
43+
/// <returns>A configured instance of <see cref="SqlEdgeBuilder" />.</returns>
44+
public SqlEdgeBuilder WithPassword(string password)
45+
{
46+
return Merge(DockerResourceConfiguration, new SqlEdgeConfiguration(password: password))
47+
.WithEnvironment("MSSQL_SA_PASSWORD", password);
48+
}
49+
50+
/// <inheritdoc />
51+
public override SqlEdgeContainer Build()
52+
{
53+
Validate();
54+
return new SqlEdgeContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
55+
}
56+
57+
/// <inheritdoc />
58+
protected override SqlEdgeBuilder Init()
59+
{
60+
return base.Init()
61+
.WithImage(SqlEdgeImage)
62+
.WithPortBinding(SqlEdgePort, true)
63+
.WithEnvironment("ACCEPT_EULA", "Y")
64+
.WithDatabase(DefaultDatabase)
65+
.WithUsername(DefaultUsername)
66+
.WithPassword(DefaultPassword)
67+
.WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil()));
68+
}
69+
70+
/// <inheritdoc />
71+
protected override void Validate()
72+
{
73+
base.Validate();
74+
75+
_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
76+
.NotNull()
77+
.NotEmpty();
78+
}
79+
80+
/// <inheritdoc />
81+
protected override SqlEdgeBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
82+
{
83+
return Merge(DockerResourceConfiguration, new SqlEdgeConfiguration(resourceConfiguration));
84+
}
85+
86+
/// <inheritdoc />
87+
protected override SqlEdgeBuilder Clone(IContainerConfiguration resourceConfiguration)
88+
{
89+
return Merge(DockerResourceConfiguration, new SqlEdgeConfiguration(resourceConfiguration));
90+
}
91+
92+
/// <inheritdoc />
93+
protected override SqlEdgeBuilder Merge(SqlEdgeConfiguration oldValue, SqlEdgeConfiguration newValue)
94+
{
95+
return new SqlEdgeBuilder(new SqlEdgeConfiguration(oldValue, newValue));
96+
}
97+
98+
/// <summary>
99+
/// Sets the SqlEdge database.
100+
/// </summary>
101+
/// <remarks>
102+
/// The Docker image does not allow to configure the database.
103+
/// </remarks>
104+
/// <param name="database">The SqlEdge database.</param>
105+
/// <returns>A configured instance of <see cref="SqlEdgeBuilder" />.</returns>
106+
private SqlEdgeBuilder WithDatabase(string database)
107+
{
108+
return Merge(DockerResourceConfiguration, new SqlEdgeConfiguration(database: database));
109+
}
110+
111+
/// <summary>
112+
/// Sets the SqlEdge username.
113+
/// </summary>
114+
/// <remarks>
115+
/// The Docker image does not allow to configure the username.
116+
/// </remarks>
117+
/// <param name="username">The SqlEdge username.</param>
118+
/// <returns>A configured instance of <see cref="SqlEdgeBuilder" />.</returns>
119+
private SqlEdgeBuilder WithUsername(string username)
120+
{
121+
return Merge(DockerResourceConfiguration, new SqlEdgeConfiguration(username: username));
122+
}
123+
124+
/// <inheritdoc cref="IWaitUntil" />
125+
private sealed class WaitUntil : IWaitUntil
126+
{
127+
/// <inheritdoc />
128+
public async Task<bool> UntilAsync(IContainer container)
129+
{
130+
var (stdout, _) = await container.GetLogs(timestampsEnabled: false)
131+
.ConfigureAwait(false);
132+
133+
return stdout.Contains("Recovery is complete.");
134+
}
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
namespace Testcontainers.SqlEdge;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class SqlEdgeConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="SqlEdgeConfiguration" /> class.
9+
/// </summary>
10+
/// <param name="database">The SqlEdge database.</param>
11+
/// <param name="username">The SqlEdge username.</param>
12+
/// <param name="password">The SqlEdge password.</param>
13+
public SqlEdgeConfiguration(
14+
string database = null,
15+
string username = null,
16+
string password = null)
17+
{
18+
Database = database;
19+
Username = username;
20+
Password = password;
21+
}
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="SqlEdgeConfiguration" /> class.
25+
/// </summary>
26+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
27+
public SqlEdgeConfiguration(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="SqlEdgeConfiguration" /> class.
35+
/// </summary>
36+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
37+
public SqlEdgeConfiguration(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="SqlEdgeConfiguration" /> class.
45+
/// </summary>
46+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
47+
public SqlEdgeConfiguration(SqlEdgeConfiguration resourceConfiguration)
48+
: this(new SqlEdgeConfiguration(), 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="SqlEdgeConfiguration" /> 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 SqlEdgeConfiguration(SqlEdgeConfiguration oldValue, SqlEdgeConfiguration newValue)
59+
: base(oldValue, newValue)
60+
{
61+
Database = BuildConfiguration.Combine(oldValue.Database, newValue.Database);
62+
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
63+
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
64+
}
65+
66+
/// <summary>
67+
/// Gets the SqlEdge database.
68+
/// </summary>
69+
public string Database { get; }
70+
71+
/// <summary>
72+
/// Gets the SqlEdge username.
73+
/// </summary>
74+
public string Username { get; }
75+
76+
/// <summary>
77+
/// Gets the SqlEdge password.
78+
/// </summary>
79+
public string Password { get; }
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Testcontainers.SqlEdge;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class SqlEdgeContainer : DockerContainer
6+
{
7+
private readonly SqlEdgeConfiguration _configuration;
8+
9+
/// <summary>
10+
/// Initializes a new instance of the <see cref="SqlEdgeContainer" /> class.
11+
/// </summary>
12+
/// <param name="configuration">The container configuration.</param>
13+
/// <param name="logger">The logger.</param>
14+
public SqlEdgeContainer(SqlEdgeConfiguration configuration, ILogger logger)
15+
: base(configuration, logger)
16+
{
17+
_configuration = configuration;
18+
}
19+
20+
/// <summary>
21+
/// Gets the SqlEdge connection string.
22+
/// </summary>
23+
/// <returns>The SqlEdge connection string.</returns>
24+
public string GetConnectionString()
25+
{
26+
var properties = new Dictionary<string, string>();
27+
properties.Add("Server", Hostname + "," + GetMappedPublicPort(SqlEdgeBuilder.SqlEdgePort));
28+
properties.Add("Database", _configuration.Database);
29+
properties.Add("User Id", _configuration.Username);
30+
properties.Add("Password", _configuration.Password);
31+
properties.Add("TrustServerCertificate", bool.TrueString);
32+
return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value)));
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1"/>
8+
</ItemGroup>
9+
<ItemGroup>
10+
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
11+
</ItemGroup>
12+
</Project>

src/Testcontainers.SqlEdge/Usings.cs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
global using System.Collections.Generic;
2+
global using System.Linq;
3+
global using System.Threading.Tasks;
4+
global using Docker.DotNet.Models;
5+
global using DotNet.Testcontainers;
6+
global using DotNet.Testcontainers.Builders;
7+
global using DotNet.Testcontainers.Configurations;
8+
global using DotNet.Testcontainers.Containers;
9+
global using JetBrains.Annotations;
10+
global using Microsoft.Extensions.Logging;

src/Testcontainers/Clients/ContainerConfigurationConverter.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ public ToPortBindings()
144144

145145
public override IEnumerable<KeyValuePair<string, IList<PortBinding>>> Convert([CanBeNull] IEnumerable<KeyValuePair<string, string>> source)
146146
{
147+
// https://github.com/moby/moby/pull/41805#issuecomment-893349240.
147148
return source?.Select(portBinding => new KeyValuePair<string, IList<PortBinding>>(
148-
GetQualifiedPort(portBinding.Key), new[] { new PortBinding { HostPort = portBinding.Value } }));
149+
GetQualifiedPort(portBinding.Key), new[] { new PortBinding { HostIP = "0.0.0.0", HostPort = portBinding.Value } }));
149150
}
150151
}
151152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Testcontainers.SqlEdge;
2+
3+
public sealed class SqlEdgeContainerTest : IAsyncLifetime
4+
{
5+
private readonly SqlEdgeContainer _sqlEdgeContainer = new SqlEdgeBuilder().Build();
6+
7+
public Task InitializeAsync()
8+
{
9+
return _sqlEdgeContainer.StartAsync();
10+
}
11+
12+
public Task DisposeAsync()
13+
{
14+
return _sqlEdgeContainer.DisposeAsync().AsTask();
15+
}
16+
17+
[Fact]
18+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
19+
public void ConnectionStateReturnsOpen()
20+
{
21+
// Given
22+
using DbConnection connection = new SqlConnection(_sqlEdgeContainer.GetConnectionString());
23+
24+
// When
25+
connection.Open();
26+
27+
// Then
28+
Assert.Equal(ConnectionState.Open, connection.State);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net6.0</TargetFrameworks>
4+
<IsPackable>false</IsPackable>
5+
<IsPublishable>false</IsPublishable>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1"/>
9+
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
10+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"/>
11+
<PackageReference Include="xunit" Version="2.4.2"/>
12+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.0"/>
13+
</ItemGroup>
14+
<ItemGroup>
15+
<ProjectReference Include="$(SolutionDir)src/Testcontainers.SqlEdge/Testcontainers.SqlEdge.csproj"/>
16+
<ProjectReference Include="$(SolutionDir)tests/Testcontainers.Commons/Testcontainers.Commons.csproj"/>
17+
</ItemGroup>
18+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
global using System.Data;
2+
global using System.Data.Common;
3+
global using System.Threading.Tasks;
4+
global using DotNet.Testcontainers.Commons;
5+
global using Microsoft.Data.SqlClient;
6+
global using Xunit;

0 commit comments

Comments
 (0)