diff --git a/Directory.Build.props b/Directory.Build.props index feef18c9263..86d123d0c29 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,6 +7,7 @@ preview MIT $(MSBuildThisFileDirectory)/src/Shared/ + $(MSBuildThisFileDirectory)/src/Components/ $([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), 'tests', 'Shared')) $([MSBuild]::NormalizeDirectory($(TestsSharedDir), 'RepoTesting')) $(MSBuildThisFileDirectory)/src/Vendoring/ diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs index 82f1391e383..53d1e4eac7a 100644 --- a/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs +++ b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs @@ -7,7 +7,8 @@ .WithDataVolume(); builder.AddProject("elasticsearch-apiservice") - .WithReference(elasticsearch); + .WithReference(elasticsearch) + .WaitFor(elasticsearch); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/spelling.dic b/spelling.dic index 5c6a474ea4b..a19ed0d1398 100644 --- a/spelling.dic +++ b/spelling.dic @@ -63,3 +63,4 @@ uris urls kubernetes Pgweb +elasticsearch diff --git a/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj index 4f18a0ec493..bc35cd8223e 100644 --- a/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj +++ b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj @@ -15,10 +15,12 @@ + + diff --git a/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs index c746ffdeaf2..0bd9f42d281 100644 --- a/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs +++ b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Elastic.Clients.Elasticsearch; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Elasticsearch; using Aspire.Hosting.Utils; +using Elastic.Clients.Elasticsearch; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Aspire.Hosting; @@ -19,7 +23,7 @@ public static class ElasticsearchBuilderExtensions /// Adds an Elasticsearch container resource to the application model. /// /// - /// The default image is "elasticsearch" and the tag is "8.14.0". + /// The default image is "elasticsearch" and the tag is "8.15.1". /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -51,6 +55,30 @@ public static IResourceBuilder AddElasticsearch( var elasticsearch = new ElasticsearchResource(name, passwordParameter); + string? connectionString = null; + ElasticsearchClient? elasticsearchClient = null; + + builder.Eventing.Subscribe(elasticsearch, async (@event, ct) => + { + connectionString = await elasticsearch.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + if (connectionString is null) + { + throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{elasticsearch.Name}' resource but the connection string was null."); + } + elasticsearchClient = new ElasticsearchClient(new Uri(connectionString)); + }); + + var healthCheckKey = $"{name}_check"; + // todo: Use health check from AspNetCore.Diagnostics.HealthChecks once following PR released: + // https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/pull/2244 + builder.Services.AddHealthChecks() + .Add(new HealthCheckRegistration( + healthCheckKey, + sp => new ElasticsearchHealthCheck(elasticsearchClient!), + failureStatus: default, + tags: default, + timeout: default)); + return builder.AddResource(elasticsearch) .WithImage(ElasticsearchContainerImageTags.Image, ElasticsearchContainerImageTags.Tag) .WithImageRegistry(ElasticsearchContainerImageTags.Registry) @@ -61,7 +89,8 @@ public static IResourceBuilder AddElasticsearch( .WithEnvironment(context => { context.EnvironmentVariables["ELASTIC_PASSWORD"] = elasticsearch.PasswordParameter; - }); + }) + .WithHealthCheck(healthCheckKey); } /// diff --git a/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs index 4a84bb9ecb9..56c2ab8900a 100644 --- a/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs +++ b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Components.Common.Tests; +using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Elastic.Clients.Elasticsearch; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using Polly; using Xunit; @@ -199,6 +201,46 @@ await pipeline.ExecuteAsync( } } + [Fact] + [RequiresDocker] + public async Task VerifyWaitForOnElasticsearchBlocksDependentResources() + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); + + var healthCheckTcs = new TaskCompletionSource(); + builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () => + { + return healthCheckTcs.Task; + }); + + var resource = builder.AddElasticsearch("resource") + .WithHealthCheck("blocking_check"); + + var dependentResource = builder.AddElasticsearch("dependentresource") + .WaitFor(resource); + + using var app = builder.Build(); + + var pendingStart = app.StartAsync(cts.Token); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token); + + healthCheckTcs.SetResult(HealthCheckResult.Healthy()); + + await rns.WaitForResourceAsync(resource.Resource.Name, (re => re.Snapshot.HealthStatus == Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy), cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await pendingStart; + + await app.StopAsync(); + } + private static async Task CreateTestData(ElasticsearchClient elasticsearchClient, ITestOutputHelper testOutputHelper, CancellationToken cancellationToken) { var indexResponse = await elasticsearchClient.IndexAsync(s_person, IndexName, s_person.Id, cancellationToken); diff --git a/tests/helix/send-to-helix-basictests.targets b/tests/helix/send-to-helix-basictests.targets index ebbc6a5c1d7..986eec363bb 100644 --- a/tests/helix/send-to-helix-basictests.targets +++ b/tests/helix/send-to-helix-basictests.targets @@ -32,7 +32,7 @@ <_DefaultWorkItems TimeoutMs="900000" /> - <_DefaultWorkItems Condition="'%(FileName)' == 'Aspire.Hosting.Elasticsearch.Tests'" TimeoutMs="1200000" /> + <_DefaultWorkItems Condition="'%(FileName)' == 'Aspire.Hosting.Elasticsearch.Tests'" TimeoutMs="3600000" /> <_DefaultWorkItems Condition="'%(FileName)' == 'Aspire.Hosting.Oracle.Tests'" TimeoutMs="1200000" /> <_DefaultWorkItems Condition="'%(FileName)' == 'Aspire.Pomelo.EntityFrameworkCore.MySql.Tests'" TimeoutMs="1200000" />