Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Aspire.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Compile Include="$(SharedDir)Model\KnownRelationshipTypes.cs" Link="Dashboard\KnownRelationshipTypes.cs" />
<Compile Include="$(SharedDir)IConfigurationExtensions.cs" Link="Utils\IConfigurationExtensions.cs" />
<Compile Include="$(SharedDir)KnownFormats.cs" Link="Utils\KnownFormats.cs" />
<Compile Include="$(SharedDir)KnownHealthCheckNames.cs" Link="Utils\KnownHealthCheckNames.cs" />
<Compile Include="$(SharedDir)KnownResourceNames.cs" Link="Utils\KnownResourceNames.cs" />
<Compile Include="$(SharedDir)KnownConfigNames.cs" Link="Utils\KnownConfigNames.cs" />
<Compile Include="$(SharedDir)PathNormalizer.cs" Link="Utils\PathNormalizer.cs" />
Expand Down
31 changes: 28 additions & 3 deletions src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -21,7 +22,8 @@ internal class AppHostRpcTarget(
IServiceProvider serviceProvider,
IDistributedApplicationEventing eventing,
PublishingActivityProgressReporter activityReporter,
IHostApplicationLifetime lifetime
IHostApplicationLifetime lifetime,
DistributedApplicationOptions options
)
{
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
Expand Down Expand Up @@ -101,6 +103,29 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)

public Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync()
{
return GetDashboardUrlsAsync(CancellationToken.None);
}

public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken)
{
if (!options.DashboardEnabled)
{
logger.LogError("Dashboard URL requested but dashboard is disabled.");
throw new InvalidOperationException("Dashboard URL requested but dashboard is disabled.");
}

// Wait for the dashboard to be healthy before returning the URL. This next statement has several
// layers of hacks. Some to work around devcontainer/codespaces port forwarding behavior, and one to
// temporarily work around the fact that resource events abuse the state to mark the resource as
// hidden instead of having another field. There is a corresponding modification in the ResourceHealthService
// which allows the dashboard resource to trigger health reports even though it never enters
// the Running state. This is a hack. The reason we can't just check HealthStatus is because
// the current implementation of HealthStatus depends on the state of the resource as well.
await resourceNotificationService.WaitForResourceAsync(
KnownResourceNames.AspireDashboard,
re => re.Snapshot.HealthReports.All(h => h.Status == HealthStatus.Healthy),
cancellationToken).ConfigureAwait(false);

var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>();

if (dashboardOptions is null)
Expand All @@ -122,11 +147,11 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)

if (baseUrlWithLoginToken == codespacesUrlWithLoginToken)
{
return Task.FromResult<(string, string?)>((baseUrlWithLoginToken, null));
return (baseUrlWithLoginToken, null);
}
else
{
return Task.FromResult((baseUrlWithLoginToken, codespacesUrlWithLoginToken));
return (baseUrlWithLoginToken, codespacesUrlWithLoginToken);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ private void AddDashboardResource(DistributedApplicationModel model)
nameGenerator.EnsureDcpInstancesPopulated(dashboardResource);

ConfigureAspireDashboardResource(dashboardResource);

// Make the dashboard first in the list so it starts as fast as possible.
model.Resources.Insert(0, dashboardResource);
}
Expand Down Expand Up @@ -179,6 +178,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
dashboardResource.Annotations.Add(new ResourceSnapshotAnnotation(snapshot));

dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(ConfigureEnvironmentVariables));
dashboardResource.Annotations.Add(new HealthCheckAnnotation(KnownHealthCheckNames.DasboardHealthCheck));
}

internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext context)
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Orchestrator;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -331,6 +332,20 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddLifecycleHook<DashboardLifecycleHook>();
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DashboardOptions>, ConfigureDefaultDashboardOptions>());
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<DashboardOptions>, ValidateDashboardOptions>());

// Dashboard health check.
_innerBuilder.Services.AddHealthChecks().AddUrlGroup(sp => {

var dashboardOptions = sp.GetRequiredService<IOptions<DashboardOptions>>().Value;
if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var firstDashboardUrl))
{
return firstDashboardUrl;
}
else
{
throw new DistributedApplicationException($"The dashboard resource '{KnownResourceNames.AspireDashboard}' does not have endpoints.");
}
}, KnownHealthCheckNames.DasboardHealthCheck);
}

if (options.EnableResourceLogging)
Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Hosting/Health/ResourceHealthCheckService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
}

if (resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running)
// HACK: We are special casing the Aspire dashboard here until we address the issue of the Hidden state
// making it impossible to determine whether a hidden resource is running or not. When that change
// is made we can remove the special case logic here for the dashboard.
if (resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running || resourceEvent.Resource.Name == KnownResourceNames.AspireDashboard)
{
if (state == null)
{
Expand Down
12 changes: 12 additions & 0 deletions src/Shared/KnownHealthCheckNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire;

internal static class KnownHealthCheckNames
{
/// <summary>
/// Common name for dashboard health check.
/// </summary>
public const string DasboardHealthCheck = "aspire_dashboard_check";
}