Skip to content

Commit bad5b70

Browse files
feat: Add Azure Data Explorer Kusto emulator module (#963)
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
1 parent 513b36e commit bad5b70

File tree

13 files changed

+315
-48
lines changed

13 files changed

+315
-48
lines changed

Testcontainers.sln

+14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka", "src
3535
EndProject
3636
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto", "src\Testcontainers.Kusto\Testcontainers.Kusto.csproj", "{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}"
39+
EndProject
3840
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack", "src\Testcontainers.LocalStack\Testcontainers.LocalStack.csproj", "{3792268A-EF08-4569-8118-991E08FD61C4}"
3941
EndProject
4042
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb", "src\Testcontainers.MariaDb\Testcontainers.MariaDb.csproj", "{4B204EB3-C478-422E-9B6F-62DF3871291A}"
@@ -91,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka.Tests"
9193
EndProject
9294
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}"
9395
EndProject
96+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto.Tests", "tests\Testcontainers.Kusto.Tests\Testcontainers.Kusto.Tests.csproj", "{FA59D75A-8D3A-412C-92E6-4A56033162DD}"
97+
EndProject
9498
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack.Tests", "tests\Testcontainers.LocalStack.Tests\Testcontainers.LocalStack.Tests.csproj", "{728CBE16-1D52-4F84-AF01-7229E6013512}"
9599
EndProject
96100
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb.Tests", "tests\Testcontainers.MariaDb.Tests\Testcontainers.MariaDb.Tests.csproj", "{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D}"
@@ -182,6 +186,10 @@ Global
182186
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU
183187
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU
184188
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU
189+
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
190+
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
191+
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
192+
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Release|Any CPU.Build.0 = Release|Any CPU
185193
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
186194
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
187195
{3792268A-EF08-4569-8118-991E08FD61C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -294,6 +302,10 @@ Global
294302
{4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
295303
{4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
296304
{4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU
305+
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
306+
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
307+
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
308+
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Release|Any CPU.Build.0 = Release|Any CPU
297309
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
298310
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.Build.0 = Debug|Any CPU
299311
{728CBE16-1D52-4F84-AF01-7229E6013512}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -383,6 +395,7 @@ Global
383395
{111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
384396
{E93E40CE-59AA-4561-9B4C-E7B0A686326E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
385397
{AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
398+
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
386399
{3792268A-EF08-4569-8118-991E08FD61C4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
387400
{4B204EB3-C478-422E-9B6F-62DF3871291A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
388401
{1266E1E6-5CEF-4161-8B45-83282455746E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -411,6 +424,7 @@ Global
411424
{F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
412425
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
413426
{4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
427+
{FA59D75A-8D3A-412C-92E6-4A56033162DD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
414428
{728CBE16-1D52-4F84-AF01-7229E6013512} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
415429
{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
416430
{5DB1F35F-B714-4B62-84BE-16A33084D3E1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
namespace Testcontainers.Kusto;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
/// <remarks>
5+
/// Builds a container running the Azure Data Explorer Kusto emulator:
6+
/// https://learn.microsoft.com/azure/data-explorer/kusto-emulator-overview.
7+
/// </remarks>
8+
[PublicAPI]
9+
public sealed class KustoBuilder : ContainerBuilder<KustoBuilder, KustoContainer, KustoConfiguration>
10+
{
11+
public const string KustoImage = "mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest";
12+
13+
public const ushort KustoPort = 8080;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="KustoBuilder" /> class.
17+
/// </summary>
18+
public KustoBuilder()
19+
: this(new KustoConfiguration())
20+
{
21+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="KustoBuilder" /> class.
26+
/// </summary>
27+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
28+
private KustoBuilder(KustoConfiguration resourceConfiguration)
29+
: base(resourceConfiguration)
30+
{
31+
DockerResourceConfiguration = resourceConfiguration;
32+
}
33+
34+
/// <inheritdoc />
35+
protected override KustoConfiguration DockerResourceConfiguration { get; }
36+
37+
/// <inheritdoc />
38+
public override KustoContainer Build()
39+
{
40+
Validate();
41+
return new KustoContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
42+
}
43+
44+
/// <inheritdoc />
45+
protected override KustoBuilder Init()
46+
{
47+
return base.Init()
48+
.WithImage(KustoImage)
49+
.WithPortBinding(KustoPort, true)
50+
.WithEnvironment("ACCEPT_EULA", "Y")
51+
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request
52+
.WithMethod(HttpMethod.Post)
53+
.ForPort(KustoPort)
54+
.ForPath("/v1/rest/mgmt")
55+
.WithContent(() => new StringContent("{\"csl\":\".show cluster\"}", Encoding.Default, "application/json"))));
56+
}
57+
58+
/// <inheritdoc />
59+
protected override KustoBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
60+
{
61+
return Merge(DockerResourceConfiguration, new KustoConfiguration(resourceConfiguration));
62+
}
63+
64+
/// <inheritdoc />
65+
protected override KustoBuilder Clone(IContainerConfiguration resourceConfiguration)
66+
{
67+
return Merge(DockerResourceConfiguration, new KustoConfiguration(resourceConfiguration));
68+
}
69+
70+
/// <inheritdoc />
71+
protected override KustoBuilder Merge(KustoConfiguration oldValue, KustoConfiguration newValue)
72+
{
73+
return new KustoBuilder(new KustoConfiguration(oldValue, newValue));
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace Testcontainers.Kusto;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class KustoConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="KustoConfiguration" /> class.
9+
/// </summary>
10+
public KustoConfiguration()
11+
{
12+
}
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="KustoConfiguration" /> class.
16+
/// </summary>
17+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
18+
public KustoConfiguration(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="KustoConfiguration" /> class.
26+
/// </summary>
27+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
28+
public KustoConfiguration(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="KustoConfiguration" /> class.
36+
/// </summary>
37+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
38+
public KustoConfiguration(KustoConfiguration resourceConfiguration)
39+
: this(new KustoConfiguration(), 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="KustoConfiguration" /> 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 KustoConfiguration(KustoConfiguration oldValue, KustoConfiguration newValue)
50+
: base(oldValue, newValue)
51+
{
52+
}
53+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Testcontainers.Kusto;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class KustoContainer : DockerContainer
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="KustoContainer" /> class.
9+
/// </summary>
10+
/// <param name="configuration">The container configuration.</param>
11+
/// <param name="logger">The logger.</param>
12+
public KustoContainer(KustoConfiguration configuration, ILogger logger)
13+
: base(configuration, logger)
14+
{
15+
}
16+
17+
/// <summary>
18+
/// Gets the Kusto connection string.
19+
/// </summary>
20+
/// <returns>The Kusto connection string.</returns>
21+
public string GetConnectionString()
22+
{
23+
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(KustoBuilder.KustoPort)).ToString();
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
8+
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
9+
</ItemGroup>
10+
<ItemGroup>
11+
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
12+
</ItemGroup>
13+
</Project>

src/Testcontainers.Kusto/Usings.cs

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

src/Testcontainers/Configurations/WaitStrategies/HttpWaitStrategy.cs

+66-47
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ public sealed class HttpWaitStrategy : IWaitUntil
3838

3939
private HttpMessageHandler _httpMessageHandler;
4040

41+
private Func<HttpContent> _httpContentCallback;
42+
4143
/// <summary>
4244
/// Initializes a new instance of the <see cref="HttpWaitStrategy" /> class.
4345
/// </summary>
4446
public HttpWaitStrategy()
4547
{
46-
_ = WithMethod(HttpMethod.Get).UsingTls(false).ForPath("/").ForResponseMessageMatching(_ => Task.FromResult(true));
48+
_ = WithMethod(HttpMethod.Get).UsingTls(false).ForPath("/").ForResponseMessageMatching(_ => Task.FromResult(true)).WithContent(() => null);
4749
}
4850

4951
/// <inheritdoc />
@@ -75,51 +77,54 @@ public async Task<bool> UntilAsync(IContainer container)
7577
httpRequestMessage.Headers.Add(httpHeader.Key, httpHeader.Value);
7678
}
7779

78-
HttpResponseMessage httpResponseMessage;
79-
80-
try
81-
{
82-
httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
83-
.ConfigureAwait(false);
84-
}
85-
catch (HttpRequestException)
86-
{
87-
return false;
88-
}
89-
90-
Predicate<HttpStatusCode> predicate;
91-
92-
if (!_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
80+
using (httpRequestMessage.Content = _httpContentCallback())
9381
{
94-
predicate = statusCode => HttpStatusCode.OK.Equals(statusCode);
95-
}
96-
else if (_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
97-
{
98-
predicate = statusCode => _httpStatusCodes.Contains(statusCode);
99-
}
100-
else if (_httpStatusCodes.Any())
101-
{
102-
predicate = statusCode => _httpStatusCodes.Contains(statusCode) || _httpStatusCodePredicate.Invoke(statusCode);
103-
}
104-
else
105-
{
106-
predicate = _httpStatusCodePredicate;
107-
}
108-
109-
try
110-
{
111-
var responseMessagePredicate = await _httpResponseMessagePredicate.Invoke(httpResponseMessage)
112-
.ConfigureAwait(false);
113-
114-
return responseMessagePredicate && predicate.Invoke(httpResponseMessage.StatusCode);
115-
}
116-
catch
117-
{
118-
return false;
119-
}
120-
finally
121-
{
122-
httpResponseMessage.Dispose();
82+
HttpResponseMessage httpResponseMessage;
83+
84+
try
85+
{
86+
httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
87+
.ConfigureAwait(false);
88+
}
89+
catch (HttpRequestException)
90+
{
91+
return false;
92+
}
93+
94+
Predicate<HttpStatusCode> predicate;
95+
96+
if (!_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
97+
{
98+
predicate = statusCode => HttpStatusCode.OK.Equals(statusCode);
99+
}
100+
else if (_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
101+
{
102+
predicate = statusCode => _httpStatusCodes.Contains(statusCode);
103+
}
104+
else if (_httpStatusCodes.Any())
105+
{
106+
predicate = statusCode => _httpStatusCodes.Contains(statusCode) || _httpStatusCodePredicate.Invoke(statusCode);
107+
}
108+
else
109+
{
110+
predicate = _httpStatusCodePredicate;
111+
}
112+
113+
try
114+
{
115+
var responseMessagePredicate = await _httpResponseMessagePredicate.Invoke(httpResponseMessage)
116+
.ConfigureAwait(false);
117+
118+
return responseMessagePredicate && predicate.Invoke(httpResponseMessage.StatusCode);
119+
}
120+
catch
121+
{
122+
return false;
123+
}
124+
finally
125+
{
126+
httpResponseMessage.Dispose();
127+
}
123128
}
124129
}
125130
}
@@ -198,9 +203,9 @@ public HttpWaitStrategy UsingTls(bool tlsEnabled = true)
198203
}
199204

200205
/// <summary>
201-
/// Defines a custom <see cref="HttpMessageHandler"/> which should be used by the internal <see cref="HttpClient"/>.
206+
/// Defines a custom <see cref="HttpMessageHandler" /> which should be used by the internal <see cref="HttpClient"/>.
202207
/// </summary>
203-
/// <param name="handler">The handler to pass to the <see cref="HttpClient"/> when it is created.</param>
208+
/// <param name="handler">The handler to pass to the <see cref="HttpClient" /> when it is created.</param>
204209
/// <returns>A configured instance of <see cref="HttpWaitStrategy" />.</returns>
205210
public HttpWaitStrategy UsingHttpMessageHandler(HttpMessageHandler handler)
206211
{
@@ -254,5 +259,19 @@ public HttpWaitStrategy WithHeaders(IReadOnlyDictionary<string, string> headers)
254259
{
255260
return headers.Aggregate(this, (httpWaitStrategy, header) => httpWaitStrategy.WithHeader(header.Key, header.Value));
256261
}
262+
263+
/// <summary>
264+
/// Sets the HTTP message body of the HTTP request.
265+
/// </summary>
266+
/// <param name="httpContentCallback">The callback to invoke to create the HTTP message body.</param>
267+
/// <remarks>
268+
/// It is important to create a new instance of <see cref="HttpContent" /> within the callback, the HTTP client disposes the content after each call.
269+
/// </remarks>
270+
/// <returns>A configured instance of <see cref="HttpWaitStrategy" />.</returns>
271+
public HttpWaitStrategy WithContent(Func<HttpContent> httpContentCallback)
272+
{
273+
_httpContentCallback = httpContentCallback;
274+
return this;
275+
}
257276
}
258277
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true

0 commit comments

Comments
 (0)