-
Notifications
You must be signed in to change notification settings - Fork 496
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[release/8.0]: Backporting dashboard refactoring (#3548)
* Improve service address allocation (#3294) * Improve service address allocation Should fix #3265 * Make the dashboard an appmodel resource (#3453) * Make the dashboard an appmodel resource - Moved dashboard resource into a lifecycle hook instead of making it a dcp resource. This removes the specialized code from ApplicationExecutor from knowing about the dashboard. As a result of this change I also cleaned up how we configure and validate dcp options to use IConfigureOptions and IValidateOptions. - Added tests for the dashboard resource - Made a change to ApplicationExecutor to allow resources that start as hidden to remain hidden. - Added hidden to a new known resource states class - Added more test cases * Only add dashboard services if the dashboard is enabled (#3489) * Only add dashboard services if the dashboard is enabled * Don't wait until after we've started the entire app to print the token (#3472) - Print it right after we print the dashboard url - Refactored the dashboard resource to use DashboardOptions instead of DcpOptions --------- Co-authored-by: Karol Zadora-Przylecki <karolz@microsoft.com> Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com>
- Loading branch information
1 parent
f1b9c55
commit a4cf987
Showing
23 changed files
with
820 additions
and
434 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics; | ||
using Aspire.Dashboard.Model; | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Dcp; | ||
using Aspire.Hosting.Lifecycle; | ||
using Aspire.Hosting.Utils; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Aspire.Hosting.Dashboard; | ||
|
||
internal sealed class DashboardLifecycleHook(IConfiguration configuration, | ||
IOptions<DashboardOptions> dashboardOptions, | ||
ILogger<DistributedApplication> distributedApplicationLogger, | ||
IDashboardEndpointProvider dashboardEndpointProvider, | ||
DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook | ||
{ | ||
public Task BeforeStartAsync(DistributedApplicationModel model, CancellationToken cancellationToken) | ||
{ | ||
Debug.Assert(executionContext.IsRunMode, "Dashboard resource should only be added in run mode"); | ||
|
||
if (model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is { } dashboardResource) | ||
{ | ||
ConfigureAspireDashboardResource(dashboardResource); | ||
|
||
// Make the dashboard first in the list so it starts as fast as possible. | ||
model.Resources.Remove(dashboardResource); | ||
model.Resources.Insert(0, dashboardResource); | ||
} | ||
else | ||
{ | ||
AddDashboardResource(model); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private void AddDashboardResource(DistributedApplicationModel model) | ||
{ | ||
if (dashboardOptions.Value.DashboardPath is not { } dashboardPath) | ||
{ | ||
throw new DistributedApplicationException("Dashboard path empty or file does not exist."); | ||
} | ||
|
||
var fullyQualifiedDashboardPath = Path.GetFullPath(dashboardPath); | ||
var dashboardWorkingDirectory = Path.GetDirectoryName(fullyQualifiedDashboardPath); | ||
|
||
ExecutableResource? dashboardResource = default; | ||
|
||
if (string.Equals(".dll", Path.GetExtension(fullyQualifiedDashboardPath), StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
// The dashboard path is a DLL, so run it with `dotnet <dll>` | ||
dashboardResource = new ExecutableResource(KnownResourceNames.AspireDashboard, "dotnet", dashboardWorkingDirectory ?? ""); | ||
|
||
dashboardResource.Annotations.Add(new CommandLineArgsCallbackAnnotation(args => | ||
{ | ||
args.Add(fullyQualifiedDashboardPath); | ||
})); | ||
} | ||
else | ||
{ | ||
// Assume the dashboard path is directly executable | ||
dashboardResource = new ExecutableResource(KnownResourceNames.AspireDashboard, fullyQualifiedDashboardPath, dashboardWorkingDirectory ?? ""); | ||
} | ||
|
||
ConfigureAspireDashboardResource(dashboardResource); | ||
|
||
// Make the dashboard first in the list so it starts as fast as possible. | ||
model.Resources.Insert(0, dashboardResource); | ||
} | ||
|
||
private void ConfigureAspireDashboardResource(IResource dashboardResource) | ||
{ | ||
// Remove endpoint annotations because we are directly configuring | ||
// the dashboard app (it doesn't go through the proxy!). | ||
var endpointAnnotations = dashboardResource.Annotations.OfType<EndpointAnnotation>().ToList(); | ||
foreach (var endpointAnnotation in endpointAnnotations) | ||
{ | ||
dashboardResource.Annotations.Remove(endpointAnnotation); | ||
} | ||
|
||
var snapshot = new CustomResourceSnapshot() | ||
{ | ||
Properties = [], | ||
ResourceType = dashboardResource switch | ||
{ | ||
ExecutableResource => KnownResourceTypes.Executable, | ||
ProjectResource => KnownResourceTypes.Project, | ||
_ => KnownResourceTypes.Container | ||
}, | ||
State = configuration.GetBool("DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES") is true ? null : KnownResourceStates.Hidden | ||
}; | ||
|
||
dashboardResource.Annotations.Add(new ResourceSnapshotAnnotation(snapshot)); | ||
|
||
dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(async context => | ||
{ | ||
var options = dashboardOptions.Value; | ||
|
||
// Options should have been validated these should not be null | ||
|
||
Debug.Assert(options.DashboardUrl is not null, "DashboardUrl should not be null"); | ||
Debug.Assert(options.OtlpEndpointUrl is not null, "OtlpEndpointUrl should not be null"); | ||
|
||
var dashboardUrls = options.DashboardUrl; | ||
var otlpEndpointUrl = options.OtlpEndpointUrl; | ||
|
||
var environment = options.AspNetCoreEnvironment; | ||
var browserToken = options.DashboardToken; | ||
var otlpApiKey = options.OtlpApiKey; | ||
|
||
var resourceServiceUrl = await dashboardEndpointProvider.GetResourceServiceUriAsync(context.CancellationToken).ConfigureAwait(false); | ||
|
||
context.EnvironmentVariables["ASPNETCORE_ENVIRONMENT"] = environment; | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardFrontendUrlName.EnvVarName] = dashboardUrls; | ||
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceUrlName.EnvVarName] = resourceServiceUrl; | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpUrlName.EnvVarName] = otlpEndpointUrl; | ||
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceAuthModeName.EnvVarName] = "Unsecured"; | ||
|
||
if (!string.IsNullOrEmpty(browserToken)) | ||
{ | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName] = "BrowserToken"; | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardFrontendBrowserTokenName.EnvVarName] = browserToken; | ||
} | ||
else | ||
{ | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardFrontendAuthModeName.EnvVarName] = "Unsecured"; | ||
} | ||
|
||
if (!string.IsNullOrEmpty(otlpApiKey)) | ||
{ | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName] = "ApiKey"; | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpPrimaryApiKeyName.EnvVarName] = otlpApiKey; | ||
} | ||
else | ||
{ | ||
context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpAuthModeName.EnvVarName] = "Unsecured"; | ||
} | ||
|
||
// We need to print out the url so that dotnet watch can launch the dashboard | ||
// technically this is too early, but it's late ne | ||
if (StringUtils.TryGetUriFromDelimitedString(dashboardUrls, ";", out var firstDashboardUrl)) | ||
{ | ||
distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", firstDashboardUrl.ToString().TrimEnd('/')); | ||
} | ||
|
||
if (!string.IsNullOrEmpty(browserToken)) | ||
{ | ||
LoggingHelpers.WriteDashboardUrl(distributedApplicationLogger, dashboardUrls, browserToken); | ||
} | ||
})); | ||
} | ||
} |
19 changes: 0 additions & 19 deletions
19
src/Aspire.Hosting/Dashboard/DashboardManifestExclusionHook.cs
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Aspire.Hosting.Dcp; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Aspire.Hosting.Dashboard; | ||
|
||
internal class DashboardOptions | ||
{ | ||
public string? DashboardPath { get; set; } | ||
public string? DashboardUrl { get; set; } | ||
public string? DashboardToken { get; set; } | ||
public string? OtlpEndpointUrl { get; set; } | ||
public string? OtlpApiKey { get; set; } | ||
public string AspNetCoreEnvironment { get; set; } = "Production"; | ||
} | ||
|
||
internal class ConfigureDefaultDashboardOptions(IConfiguration configuration, IOptions<DcpOptions> dcpOptions) : IConfigureOptions<DashboardOptions> | ||
{ | ||
public void Configure(DashboardOptions options) | ||
{ | ||
options.DashboardPath = dcpOptions.Value.DashboardPath; | ||
options.DashboardUrl = configuration["ASPNETCORE_URLS"]; | ||
options.DashboardToken = configuration["AppHost:BrowserToken"]; | ||
|
||
options.OtlpEndpointUrl = configuration["DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"]; | ||
options.OtlpApiKey = configuration["AppHost:OtlpApiKey"]; | ||
|
||
options.AspNetCoreEnvironment = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Production"; | ||
} | ||
} | ||
|
||
internal class ValidateDashboardOptions : IValidateOptions<DashboardOptions> | ||
{ | ||
public ValidateOptionsResult Validate(string? name, DashboardOptions options) | ||
{ | ||
var builder = new ValidateOptionsResultBuilder(); | ||
|
||
if (string.IsNullOrEmpty(options.DashboardUrl)) | ||
{ | ||
builder.AddError("Failed to configure dashboard resource because ASPNETCORE_URLS environment variable was not set."); | ||
} | ||
|
||
if (string.IsNullOrEmpty(options.OtlpEndpointUrl)) | ||
{ | ||
builder.AddError("Failed to configure dashboard resource because DOTNET_DASHBOARD_OTLP_ENDPOINT_URL environment variable was not set."); | ||
} | ||
|
||
return builder.Build(); | ||
} | ||
} |
Oops, something went wrong.