Skip to content

Commit

Permalink
feat: Add Azure Data Explorer Kusto emulator module (#963)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
  • Loading branch information
JonasBenz and HofmeisterAn authored Aug 7, 2023
1 parent 513b36e commit bad5b70
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 48 deletions.
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.Kafka", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto", "src\Testcontainers.Kusto\Testcontainers.Kusto.csproj", "{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack", "src\Testcontainers.LocalStack\Testcontainers.LocalStack.csproj", "{3792268A-EF08-4569-8118-991E08FD61C4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb", "src\Testcontainers.MariaDb\Testcontainers.MariaDb.csproj", "{4B204EB3-C478-422E-9B6F-62DF3871291A}"
Expand Down Expand Up @@ -91,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto.Tests", "tests\Testcontainers.Kusto.Tests\Testcontainers.Kusto.Tests.csproj", "{FA59D75A-8D3A-412C-92E6-4A56033162DD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack.Tests", "tests\Testcontainers.LocalStack.Tests\Testcontainers.LocalStack.Tests.csproj", "{728CBE16-1D52-4F84-AF01-7229E6013512}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb.Tests", "tests\Testcontainers.MariaDb.Tests\Testcontainers.MariaDb.Tests.csproj", "{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D}"
Expand Down Expand Up @@ -182,6 +186,10 @@ Global
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF}.Release|Any CPU.Build.0 = Release|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -294,6 +302,10 @@ Global
{4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA59D75A-8D3A-412C-92E6-4A56033162DD}.Release|Any CPU.Build.0 = Release|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.Build.0 = Debug|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -383,6 +395,7 @@ Global
{111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{E93E40CE-59AA-4561-9B4C-E7B0A686326E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{FCF59758-2403-4EC9-9EAE-4EC69A3F27AF} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{3792268A-EF08-4569-8118-991E08FD61C4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{4B204EB3-C478-422E-9B6F-62DF3871291A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{1266E1E6-5CEF-4161-8B45-83282455746E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -411,6 +424,7 @@ Global
{F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{FA59D75A-8D3A-412C-92E6-4A56033162DD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{728CBE16-1D52-4F84-AF01-7229E6013512} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{5DB1F35F-B714-4B62-84BE-16A33084D3E1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Kusto/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
75 changes: 75 additions & 0 deletions src/Testcontainers.Kusto/KustoBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Testcontainers.Kusto;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
/// <remarks>
/// Builds a container running the Azure Data Explorer Kusto emulator:
/// https://learn.microsoft.com/azure/data-explorer/kusto-emulator-overview.
/// </remarks>
[PublicAPI]
public sealed class KustoBuilder : ContainerBuilder<KustoBuilder, KustoContainer, KustoConfiguration>
{
public const string KustoImage = "mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest";

public const ushort KustoPort = 8080;

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

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

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

/// <inheritdoc />
public override KustoContainer Build()
{
Validate();
return new KustoContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
}

/// <inheritdoc />
protected override KustoBuilder Init()
{
return base.Init()
.WithImage(KustoImage)
.WithPortBinding(KustoPort, true)
.WithEnvironment("ACCEPT_EULA", "Y")
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request
.WithMethod(HttpMethod.Post)
.ForPort(KustoPort)
.ForPath("/v1/rest/mgmt")
.WithContent(() => new StringContent("{\"csl\":\".show cluster\"}", Encoding.Default, "application/json"))));
}

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

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

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

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class KustoConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="KustoConfiguration" /> class.
/// </summary>
public KustoConfiguration()
{
}

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

/// <summary>
/// Initializes a new instance of the <see cref="KustoConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public KustoConfiguration(KustoConfiguration oldValue, KustoConfiguration newValue)
: base(oldValue, newValue)
{
}
}
25 changes: 25 additions & 0 deletions src/Testcontainers.Kusto/KustoContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Testcontainers.Kusto;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class KustoContainer : DockerContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="KustoContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public KustoContainer(KustoConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
}

/// <summary>
/// Gets the Kusto connection string.
/// </summary>
/// <returns>The Kusto connection string.</returns>
public string GetConnectionString()
{
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(KustoBuilder.KustoPort)).ToString();
}
}
13 changes: 13 additions & 0 deletions src/Testcontainers.Kusto/Testcontainers.Kusto.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Testcontainers.Kusto/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global using System;
global using System.Net.Http;
global using System.Text;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using JetBrains.Annotations;
global using Microsoft.Extensions.Logging;
113 changes: 66 additions & 47 deletions src/Testcontainers/Configurations/WaitStrategies/HttpWaitStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ public sealed class HttpWaitStrategy : IWaitUntil

private HttpMessageHandler _httpMessageHandler;

private Func<HttpContent> _httpContentCallback;

/// <summary>
/// Initializes a new instance of the <see cref="HttpWaitStrategy" /> class.
/// </summary>
public HttpWaitStrategy()
{
_ = WithMethod(HttpMethod.Get).UsingTls(false).ForPath("/").ForResponseMessageMatching(_ => Task.FromResult(true));
_ = WithMethod(HttpMethod.Get).UsingTls(false).ForPath("/").ForResponseMessageMatching(_ => Task.FromResult(true)).WithContent(() => null);
}

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

HttpResponseMessage httpResponseMessage;

try
{
httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
.ConfigureAwait(false);
}
catch (HttpRequestException)
{
return false;
}

Predicate<HttpStatusCode> predicate;

if (!_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
using (httpRequestMessage.Content = _httpContentCallback())
{
predicate = statusCode => HttpStatusCode.OK.Equals(statusCode);
}
else if (_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
{
predicate = statusCode => _httpStatusCodes.Contains(statusCode);
}
else if (_httpStatusCodes.Any())
{
predicate = statusCode => _httpStatusCodes.Contains(statusCode) || _httpStatusCodePredicate.Invoke(statusCode);
}
else
{
predicate = _httpStatusCodePredicate;
}

try
{
var responseMessagePredicate = await _httpResponseMessagePredicate.Invoke(httpResponseMessage)
.ConfigureAwait(false);

return responseMessagePredicate && predicate.Invoke(httpResponseMessage.StatusCode);
}
catch
{
return false;
}
finally
{
httpResponseMessage.Dispose();
HttpResponseMessage httpResponseMessage;

try
{
httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
.ConfigureAwait(false);
}
catch (HttpRequestException)
{
return false;
}

Predicate<HttpStatusCode> predicate;

if (!_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
{
predicate = statusCode => HttpStatusCode.OK.Equals(statusCode);
}
else if (_httpStatusCodes.Any() && _httpStatusCodePredicate == null)
{
predicate = statusCode => _httpStatusCodes.Contains(statusCode);
}
else if (_httpStatusCodes.Any())
{
predicate = statusCode => _httpStatusCodes.Contains(statusCode) || _httpStatusCodePredicate.Invoke(statusCode);
}
else
{
predicate = _httpStatusCodePredicate;
}

try
{
var responseMessagePredicate = await _httpResponseMessagePredicate.Invoke(httpResponseMessage)
.ConfigureAwait(false);

return responseMessagePredicate && predicate.Invoke(httpResponseMessage.StatusCode);
}
catch
{
return false;
}
finally
{
httpResponseMessage.Dispose();
}
}
}
}
Expand Down Expand Up @@ -198,9 +203,9 @@ public HttpWaitStrategy UsingTls(bool tlsEnabled = true)
}

/// <summary>
/// Defines a custom <see cref="HttpMessageHandler"/> which should be used by the internal <see cref="HttpClient"/>.
/// Defines a custom <see cref="HttpMessageHandler" /> which should be used by the internal <see cref="HttpClient"/>.
/// </summary>
/// <param name="handler">The handler to pass to the <see cref="HttpClient"/> when it is created.</param>
/// <param name="handler">The handler to pass to the <see cref="HttpClient" /> when it is created.</param>
/// <returns>A configured instance of <see cref="HttpWaitStrategy" />.</returns>
public HttpWaitStrategy UsingHttpMessageHandler(HttpMessageHandler handler)
{
Expand Down Expand Up @@ -254,5 +259,19 @@ public HttpWaitStrategy WithHeaders(IReadOnlyDictionary<string, string> headers)
{
return headers.Aggregate(this, (httpWaitStrategy, header) => httpWaitStrategy.WithHeader(header.Key, header.Value));
}

/// <summary>
/// Sets the HTTP message body of the HTTP request.
/// </summary>
/// <param name="httpContentCallback">The callback to invoke to create the HTTP message body.</param>
/// <remarks>
/// It is important to create a new instance of <see cref="HttpContent" /> within the callback, the HTTP client disposes the content after each call.
/// </remarks>
/// <returns>A configured instance of <see cref="HttpWaitStrategy" />.</returns>
public HttpWaitStrategy WithContent(Func<HttpContent> httpContentCallback)
{
_httpContentCallback = httpContentCallback;
return this;
}
}
}
1 change: 1 addition & 0 deletions tests/Testcontainers.Kusto.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
Loading

0 comments on commit bad5b70

Please sign in to comment.