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
26 changes: 23 additions & 3 deletions src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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 +102,25 @@ 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 we return the URL. This is to avoid
// a race condition when using Codespaces or devcontainers where the dashboard URL
// is displayed before the dashboard port forwarding is actually configured. It is
// also a point of friction to show the URL before the dashboard is ready to be used
// when using Devcontainers/Codespaces because people think that something isn't working
// when in fact they just need to refresh the page.
await resourceNotificationService.WaitForResourceHealthyAsync(KnownResourceNames.AspireDashboard, cancellationToken).ConfigureAwait(false);

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

if (dashboardOptions is null)
Expand All @@ -122,11 +142,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
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";
}