diff --git a/.github/workflows/healthchecks_elasticsearch_ci.yml b/.github/workflows/healthchecks_elasticsearch_ci.yml
index 680562c06f..64cf6d46f6 100644
--- a/.github/workflows/healthchecks_elasticsearch_ci.yml
+++ b/.github/workflows/healthchecks_elasticsearch_ci.yml
@@ -29,47 +29,8 @@ on:
jobs:
build:
- runs-on: ubuntu-latest
- services:
- elasticsearch:
- image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
- ports:
- - 9300:9300
- - 9201:9200
- steps:
- - uses: actions/checkout@v3
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: |
- 8.0.x
- 9.0.x
- - run:
- ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose
- - name: Restore
- run: |
- dotnet restore ./src/HealthChecks.Elasticsearch/HealthChecks.Elasticsearch.csproj &&
- dotnet restore ./test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
- - name: Check formatting
- run: |
- dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.Elasticsearch/HealthChecks.Elasticsearch.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) &&
- dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1)
- - name: Build
- run: |
- dotnet build --no-restore ./src/HealthChecks.Elasticsearch/HealthChecks.Elasticsearch.csproj &&
- dotnet build --no-restore ./test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
- - name: Test
- run: >
- dotnet test
- ./test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
- --no-restore
- --no-build
- --collect "XPlat Code Coverage"
- --results-directory .coverage
- --
- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
- - name: Upload Coverage
- uses: codecov/codecov-action@v5
- with:
- flags: Elasticsearch
- directory: .coverage
+ uses: ./.github/workflows/reusable_ci_workflow.yml
+ with:
+ PROJECT_PATH: ./src/HealthChecks.Elasticsearch/HealthChecks.Elasticsearch.csproj
+ TEST_PROJECT_PATH: ./test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
+ CODECOV_FLAGS: Elasticsearch
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2c2207765a..fd756329d2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -29,7 +29,6 @@
-
@@ -104,6 +103,7 @@
+
@@ -119,4 +119,4 @@
-
+
\ No newline at end of file
diff --git a/test/HealthChecks.Elasticsearch.Tests/ElasticsearchContainerFixture.cs b/test/HealthChecks.Elasticsearch.Tests/ElasticsearchContainerFixture.cs
new file mode 100644
index 0000000000..f4a882f189
--- /dev/null
+++ b/test/HealthChecks.Elasticsearch.Tests/ElasticsearchContainerFixture.cs
@@ -0,0 +1,107 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text;
+using System.Text.Json;
+using Testcontainers.Elasticsearch;
+
+namespace HealthChecks.Elasticsearch.Tests;
+
+public class ElasticsearchContainerFixture : IAsyncLifetime
+{
+ private const string Registry = "docker.io";
+
+ private const string Image = "library/elasticsearch";
+
+ private const string Tag = "8.19.2";
+
+ private const string ApiKeyName = "healthchecks";
+
+ private string? _apiKey;
+
+ public string Username => ElasticsearchBuilder.DefaultUsername;
+
+ public string Password => ElasticsearchBuilder.DefaultPassword;
+
+ public ElasticsearchContainer? Container { get; private set; }
+
+ public string GetConnectionString()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+
+ return Container.GetConnectionString();
+ }
+
+ public string GetApiKey()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+
+ return _apiKey ?? throw new InvalidOperationException("The API key was not initialized.");
+ }
+
+ public async Task InitializeAsync()
+ {
+ Container = await CreateContainerAsync();
+
+ await SetupApiKeyAsync();
+ }
+
+ public Task DisposeAsync() => Container?.DisposeAsync().AsTask() ?? Task.CompletedTask;
+
+ private static async Task CreateContainerAsync()
+ {
+ var container = new ElasticsearchBuilder()
+ .WithImage($"{Registry}/{Image}:{Tag}")
+ .Build();
+
+ await container.StartAsync();
+
+ return container;
+ }
+
+ private async Task SetupApiKeyAsync()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+
+ var handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = delegate
+ {
+ return true;
+ }
+ };
+
+ using var httpClient = new HttpClient(handler);
+
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
+ nameof(AuthenticationSchemes.Basic),
+ Convert.ToBase64String(Encoding.ASCII.GetBytes(
+ $"{ElasticsearchBuilder.DefaultUsername}:{ElasticsearchBuilder.DefaultPassword}")));
+
+ var uriBuilder = new UriBuilder(
+ Uri.UriSchemeHttps,
+ Container.Hostname,
+ Container.GetMappedPublicPort(ElasticsearchBuilder.ElasticsearchHttpsPort),
+ "/_security/api_key");
+
+ using var response = await httpClient
+ .PostAsJsonAsync(uriBuilder.Uri, new { name = ApiKeyName })
+ .ConfigureAwait(false);
+
+ var apiKeyResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false)
+ ?? throw new JsonException();
+
+ _apiKey = apiKeyResponse.Encoded;
+ }
+
+ private record ApiKeyResponse(string Encoded);
+}
diff --git a/test/HealthChecks.Elasticsearch.Tests/Fixtures/ElasticContainerFixture.cs b/test/HealthChecks.Elasticsearch.Tests/Fixtures/ElasticContainerFixture.cs
deleted file mode 100644
index f3f2f35043..0000000000
--- a/test/HealthChecks.Elasticsearch.Tests/Fixtures/ElasticContainerFixture.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using System.Text;
-using System.Text.Json;
-using Ductus.FluentDocker.Builders;
-using Ductus.FluentDocker.Services;
-using Ductus.FluentDocker.Services.Extensions;
-
-namespace HealthChecks.Elasticsearch.Tests.Fixtures;
-
-public class ElasticContainerFixture : IAsyncLifetime
-{
- private const string SETUP_DONE_MESSAGE = "All done!";
- private const long TIME_OUT_IN_MILLIS = 180000;
- private const string ELASTIC_CONTAINER_NAME = "es01";
- private const string CONTAINER_CERTIFICATE_PATH = "/usr/share/elasticsearch/config/certs/ca/ca.crt";
-
- public const string ELASTIC_PASSWORD = "abcDEF123!";
- private readonly string _composeFilePath = $"{Directory.GetCurrentDirectory()}/Resources/docker-compose.yml";
- private readonly ICompositeService _compositeService;
-
- public string? ApiKey { get; set; }
-
- public ElasticContainerFixture()
- {
- _compositeService = new Builder()
- .UseContainer()
- .UseCompose()
- .FromFile(_composeFilePath)
- .ForceRecreate()
- .Build()
- .Start();
-
- var elasticContainer =
- _compositeService.Containers.First(container => container.Name.Contains(ELASTIC_CONTAINER_NAME));
- var setupContainer = _compositeService.Containers.First(container => container != elasticContainer);
- setupContainer.WaitForMessageInLogs(SETUP_DONE_MESSAGE, TIME_OUT_IN_MILLIS);
- elasticContainer.CopyFrom(CONTAINER_CERTIFICATE_PATH, ".", true);
- }
-
- public async Task InitializeAsync() => ApiKey = await SetApiKeyInElasticSearchAsync().ConfigureAwait(false);
-
- public Task DisposeAsync()
- {
- Dispose();
- return Task.CompletedTask;
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- _compositeService.Dispose();
- }
- }
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private async Task SetApiKeyInElasticSearchAsync()
- {
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = delegate
- {
- return true;
- }
- };
- using var httpClient = new HttpClient(handler);
- httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(
- Encoding.ASCII.GetBytes($"elastic:{ELASTIC_PASSWORD}")));
- using var response = await httpClient.PostAsJsonAsync("https://localhost:9200/_security/api_key?pretty",
- new { name = "new-api-key", role_descriptors = new { } }).ConfigureAwait(false);
- var apiKeyResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false) ?? throw new JsonException();
-
- return apiKeyResponse.Encoded;
- }
-
- private record ApiKeyResponse(string Encoded);
-}
diff --git a/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchAuthenticationTests.cs b/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchAuthenticationTests.cs
index 6b7f05c435..eebe7db5df 100644
--- a/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchAuthenticationTests.cs
+++ b/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchAuthenticationTests.cs
@@ -1,21 +1,14 @@
using System.Net;
-using HealthChecks.Elasticsearch.Tests.Fixtures;
namespace HealthChecks.Elasticsearch.Tests.Functional;
-public class ElasticsearchAuthenticationTests : IClassFixture
+public class ElasticsearchAuthenticationTests(ElasticsearchContainerFixture elasticsearchFixture) : IClassFixture
{
- private readonly ElasticContainerFixture _fixture;
-
- public ElasticsearchAuthenticationTests(ElasticContainerFixture fixture)
- {
- _fixture = fixture;
- }
-
[Fact]
public async Task be_healthy_if_elasticsearch_is_using_valid_api_key()
{
- var connectionString = @"https://localhost:9200";
+ string connectionString = elasticsearchFixture.GetConnectionString();
+ string apiKey = elasticsearchFixture.GetApiKey();
var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
@@ -24,7 +17,7 @@ public async Task be_healthy_if_elasticsearch_is_using_valid_api_key()
.AddElasticsearch(options =>
{
options.UseServer(connectionString);
- options.UseApiKey(_fixture.ApiKey!);
+ options.UseApiKey(apiKey);
options.UseCertificateValidationCallback(delegate
{
return true;
@@ -50,7 +43,7 @@ public async Task be_healthy_if_elasticsearch_is_using_valid_api_key()
[Fact]
public async Task be_healthy_if_elasticsearch_is_using_valid_user_and_password()
{
- var connectionString = @"https://localhost:9200";
+ string connectionString = elasticsearchFixture.GetConnectionString();
var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
@@ -59,7 +52,7 @@ public async Task be_healthy_if_elasticsearch_is_using_valid_user_and_password()
.AddElasticsearch(options =>
{
options.UseServer(connectionString);
- options.UseBasicAuthentication("elastic", ElasticContainerFixture.ELASTIC_PASSWORD);
+ options.UseBasicAuthentication(elasticsearchFixture.Username, elasticsearchFixture.Password);
options.UseCertificateValidationCallback(delegate
{
return true;
diff --git a/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchHealthCheckTests.cs b/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchHealthCheckTests.cs
index 1101ee18f9..d5b1cd5d24 100644
--- a/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchHealthCheckTests.cs
+++ b/test/HealthChecks.Elasticsearch.Tests/Functional/ElasticsearchHealthCheckTests.cs
@@ -2,18 +2,20 @@
namespace HealthChecks.Elasticsearch.Tests.Functional;
-public class elasticsearch_healthcheck_should
+public class elasticsearch_healthcheck_should(ElasticsearchContainerFixture elasticsearchFixture) : IClassFixture
{
[Fact]
public async Task be_healthy_if_elasticsearch_is_available()
{
- var connectionString = @"http://localhost:9201";
+ string connectionString = elasticsearchFixture.GetConnectionString();
var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHealthChecks()
- .AddElasticsearch(connectionString, tags: ["elasticsearch"]);
+ .AddElasticsearch(options => options
+ .UseServer(connectionString)
+ .UseCertificateValidationCallback((_, _, _, _) => true), tags: ["elasticsearch"]);
})
.Configure(app =>
{
diff --git a/test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj b/test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
index 0d1e61e095..c191c7e963 100644
--- a/test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
+++ b/test/HealthChecks.Elasticsearch.Tests/HealthChecks.Elasticsearch.Tests.csproj
@@ -7,7 +7,6 @@
-
@@ -16,4 +15,8 @@
+
+
+
+
diff --git a/test/HealthChecks.Elasticsearch.Tests/Resources/docker-compose.yml b/test/HealthChecks.Elasticsearch.Tests/Resources/docker-compose.yml
deleted file mode 100644
index db2c474bdb..0000000000
--- a/test/HealthChecks.Elasticsearch.Tests/Resources/docker-compose.yml
+++ /dev/null
@@ -1,88 +0,0 @@
-services:
- setup:
- image: docker.elastic.co/elasticsearch/elasticsearch:8.1.3
- volumes:
- - certs:/usr/share/elasticsearch/config/certs
- user: "0"
- command: >
- bash -c '
- if [ ! -f certs/ca.zip ]; then
- echo "Creating CA";
- bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
- unzip config/certs/ca.zip -d config/certs;
- fi;
- if [ ! -f certs/certs.zip ]; then
- echo "Creating certs";
- echo -ne \
- "instances:\n"\
- " - name: es01\n"\
- " dns:\n"\
- " - es01\n"\
- " - localhost\n"\
- " ip:\n"\
- " - 127.0.0.1\n"\
- > config/certs/instances.yml;
- bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
- unzip config/certs/certs.zip -d config/certs;
- fi;
- echo "Setting file permissions"
- chown -R root:root config/certs;
- find . -type d -exec chmod 750 \{\} \;;
- find . -type f -exec chmod 640 \{\} \;;
- echo "Waiting for Elasticsearch availability";
- until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
- echo "All done!";
- '
- healthcheck:
- test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
- interval: 1s
- timeout: 5s
- retries: 120
-
- es01:
- depends_on:
- setup:
- condition: service_healthy
- image: docker.elastic.co/elasticsearch/elasticsearch:8.1.3
- volumes:
- - certs:/usr/share/elasticsearch/config/certs
- - esdata01:/usr/share/elasticsearch/data
- ports:
- - 9200:9200
- environment:
- - node.name=es01
- - discovery.type=single-node
- - ELASTIC_PASSWORD=abcDEF123!
- - bootstrap.memory_lock=true
- - xpack.security.enabled=true
- - xpack.security.http.ssl.enabled=true
- - xpack.security.http.ssl.key=certs/es01/es01.key
- - xpack.security.http.ssl.certificate=certs/es01/es01.crt
- - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
- - xpack.security.http.ssl.verification_mode=certificate
- - xpack.security.transport.ssl.enabled=true
- - xpack.security.transport.ssl.key=certs/es01/es01.key
- - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
- - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- - xpack.security.transport.ssl.verification_mode=certificate
- - xpack.license.self_generated.type=basic
- mem_limit: 1073741824
- ulimits:
- memlock:
- soft: -1
- hard: -1
- healthcheck:
- test:
- [
- "CMD-SHELL",
- "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
- ]
- interval: 10s
- timeout: 10s
- retries: 120
-
-volumes:
- certs:
- driver: local
- esdata01:
- driver: local