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