Skip to content

Commit

Permalink
[release/8.0]: Backporting dashboard refactoring (#3548)
Browse files Browse the repository at this point in the history
* 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
3 people authored Apr 10, 2024
1 parent f1b9c55 commit a4cf987
Show file tree
Hide file tree
Showing 23 changed files with 820 additions and 434 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static IResourceBuilder<IDaprComponentResource> AddDaprComponent(this IDi
{
Properties = [],
ResourceType = "DaprComponent",
State = "Hidden"
State = KnownResourceStates.Hidden
})
.WithAnnotation(new ManifestPublishingCallbackAnnotation(context => WriteDaprComponentResourceToManifest(context, resource)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static IResourceBuilder<T> WithDaprSidecar<T>(this IResourceBuilder<T> bu
{
Properties = [],
ResourceType = "DaprSidecar",
State = "Hidden"
State = KnownResourceStates.Hidden
});

configureSidecar(sidecarBuilder);
Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,14 @@ public static class KnownResourceStateStyles
public static readonly string Warn = "warn";

}

/// <summary>
/// The set of well known resource states
/// </summary>
public static class KnownResourceStates
{
/// <summary>
/// The hidden state. Useful for hiding the resource.
/// </summary>
public static readonly string Hidden = "Hidden";
}
157 changes: 157 additions & 0 deletions src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
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 src/Aspire.Hosting/Dashboard/DashboardManifestExclusionHook.cs

This file was deleted.

53 changes: 53 additions & 0 deletions src/Aspire.Hosting/Dashboard/DashboardOptions.cs
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();
}
}
Loading

0 comments on commit a4cf987

Please sign in to comment.