diff --git a/Aspirant.sln b/Aspirant.sln index 775f2aa..e8280a5 100644 --- a/Aspirant.sln +++ b/Aspirant.sln @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.PostgreSQL EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.RabbitMQ", "src\Aspirant.Hosting.RabbitMQ\Aspirant.Hosting.RabbitMQ.csproj", "{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.WebApplication", "src\Aspirant.Hosting.WebApplication\Aspirant.Hosting.WebApplication.csproj", "{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +112,10 @@ Global {45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Release|Any CPU.Build.0 = Release|Any CPU + {BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +136,7 @@ Global {135004A9-F7E0-46D9-B3D0-3D64BA3C40B9} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34} {77D17495-C537-4446-8C2E-8B46560E0992} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34} {45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34} + {BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {52627286-38AF-4E95-BDA0-B21166B2B18C} diff --git a/src/Aspirant.Hosting.WebApplication/Aspirant.Hosting.WebApplication.csproj b/src/Aspirant.Hosting.WebApplication/Aspirant.Hosting.WebApplication.csproj new file mode 100644 index 0000000..6f19045 --- /dev/null +++ b/src/Aspirant.Hosting.WebApplication/Aspirant.Hosting.WebApplication.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + A custom ASP.NET Core web app resource for .NET Aspire App Host projects. + aspire hosting aspnet aspnetcore + + + + + + + + + + + + diff --git a/src/Aspirant.Hosting.WebApplication/WebApplicationResource.cs b/src/Aspirant.Hosting.WebApplication/WebApplicationResource.cs new file mode 100644 index 0000000..89375c3 --- /dev/null +++ b/src/Aspirant.Hosting.WebApplication/WebApplicationResource.cs @@ -0,0 +1,225 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Aspirant.Hosting; + +/// +/// Represents a ASP.NET Web Application resource. +/// +public class WebApplicationResource(string name) : Resource(name), IResourceWithServiceDiscovery, IResourceWithEnvironment +{ + +} + +public abstract class WebApplicationResourceLifecycleHook( + IHostEnvironment hostEnvironment, + DistributedApplicationExecutionContext executionContext, + ResourceNotificationService resourceNotificationService, + ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable + where TResource : WebApplicationResource +{ + protected abstract string ResourceTypeName { get; } + + private WebApplication? _app; + + /// + public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + if (executionContext.IsPublishMode) + { + return; + } + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + if (resource is null) + { + return; + } + + await resourceNotificationService.PublishUpdateAsync(resource, s => s with + { + ResourceType = ResourceTypeName, + State = "Starting" + }); + + var bindings = resource.Annotations.OfType().ToList(); + + foreach (var b in bindings) + { + b.IsProxied = false; + } + } + + /// + public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + if (executionContext.IsPublishMode) + { + return; + } + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + if (resource is null) + { + return; + } + + var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions + { + ContentRootPath = hostEnvironment.ContentRootPath, + EnvironmentName = hostEnvironment.EnvironmentName, + WebRootPath = Path.Combine(hostEnvironment.ContentRootPath, "wwwroot") + }); + + builder.Logging.ClearProviders(); + + builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(resource.Name))); + + if (resource.TryGetEnvironmentVariables(out var environmentVariables)) + { + var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + + foreach (var cb in environmentVariables) + { + await cb.Callback(context); + } + + var dict = new Dictionary(); + foreach (var (k, v) in context.EnvironmentVariables) + { + var val = v switch + { + string s => s, + IValueProvider vp => await vp.GetValueAsync(context.CancellationToken), + _ => throw new NotSupportedException() + }; + + if (val is not null) + { + dict[k.Replace("__", ":")] = val; + } + } + + builder.Configuration.AddInMemoryCollection(dict); + + await ConfigureBuilderAsync(builder, resource, cancellationToken); + + resource.TryGetEndpoints(out var endpoints); + var defaultScheme = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Contains("https://") == true ? "https" : "http"; + var needHttps = defaultScheme == "https" || endpoints?.Any(ep => ep.UriScheme == "https") == true; + + if (needHttps) + { + builder.WebHost.UseKestrelHttpsConfiguration(); + } + + _app = builder.Build(); + + var urlToEndpointNameMap = new Dictionary(); + + if (endpoints is null) + { + var url = $"{defaultScheme}://127.0.0.1:0/"; + _app.Urls.Add(url); + urlToEndpointNameMap[url] = "default"; + } + else + { + foreach (var ep in endpoints) + { + var scheme = ep.UriScheme ?? defaultScheme; + needHttps = needHttps || scheme == "https"; + + var url = ep.Port switch + { + null => $"{scheme}://127.0.0.1:0/", + _ => $"{scheme}://localhost:{ep.Port}" + }; + + var uri = new Uri(url); + _app.Urls.Add(url); + urlToEndpointNameMap[uri.ToString()] = ep.Name; + } + } + + await ConfigureApplicationAsync(_app, resource, cancellationToken); + + await _app.StartAsync(cancellationToken); + + var addresses = _app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses; + + foreach (var url in addresses) + { + if (urlToEndpointNameMap.TryGetValue(new Uri(url).ToString(), out var name) + || urlToEndpointNameMap.TryGetValue((new UriBuilder(url) { Port = 0 }).Uri.ToString(), out name)) + { + var ep = endpoints?.FirstOrDefault(ep => ep.Name == name); + if (ep is not null) + { + var uri = new Uri(url); + var host = uri.Host is "127.0.0.1" or "[::1]" ? "localhost" : uri.Host; + ep.AllocatedEndpoint = new(ep, host, uri.Port); + } + } + } + + await resourceNotificationService.PublishUpdateAsync(resource, s => s with + { + State = "Running", + Urls = [.. endpoints?.Select(ep => new UrlSnapshot(ep.Name, ep.AllocatedEndpoint?.UriString ?? "", IsInternal: false))], + }); + } + } + + protected abstract ValueTask ConfigureBuilderAsync(WebApplicationBuilder builder, TResource resource, CancellationToken cancellationToken); + + protected abstract ValueTask ConfigureApplicationAsync(WebApplication application, TResource resource, CancellationToken cancellationToken); + + /// + public ValueTask DisposeAsync() + { + return _app?.DisposeAsync() ?? default; + } + + private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new ResourceLogger(logger); + } + + public void Dispose() + { + } + + private class ResourceLogger(ILogger logger) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + { + return logger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logger.IsEnabled(logLevel); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + } + } +} \ No newline at end of file diff --git a/src/Aspirant.Hosting.WebApplication/WebApplicationResourceExtensions.cs b/src/Aspirant.Hosting.WebApplication/WebApplicationResourceExtensions.cs new file mode 100644 index 0000000..e972dec --- /dev/null +++ b/src/Aspirant.Hosting.WebApplication/WebApplicationResourceExtensions.cs @@ -0,0 +1,35 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; + +namespace Aspirant.Hosting; + + +/// +/// Extensions method to add WebApplication resources. +/// +public static class WebApplicationResourceExtensions +{ + public static IResourceBuilder AddWebApplication(this IDistributedApplicationBuilder builder, TResource resource, bool excludeFromManifest = false) + where TResource : WebApplicationResource + where TLifecycleHook : WebApplicationResourceLifecycleHook + { + var webApplicationResource = builder.Resources.OfType().SingleOrDefault(); + + if (webApplicationResource is not null) + { + throw new InvalidOperationException($"A resource of type {nameof(TResource)} has already been added to this application"); + } + + builder.Services.TryAddLifecycleHook(); + + var resourceBuilder = builder.AddResource(resource); + + if (excludeFromManifest) + { + resourceBuilder = resourceBuilder.ExcludeFromManifest(); + } + + return resourceBuilder; + } +} \ No newline at end of file diff --git a/src/Aspirant.Hosting.Yarp/Aspirant.Hosting.Yarp.csproj b/src/Aspirant.Hosting.Yarp/Aspirant.Hosting.Yarp.csproj index e6cef82..0b4f4f9 100644 --- a/src/Aspirant.Hosting.Yarp/Aspirant.Hosting.Yarp.csproj +++ b/src/Aspirant.Hosting.Yarp/Aspirant.Hosting.Yarp.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Aspirant.Hosting.Yarp/YarpResource.cs b/src/Aspirant.Hosting.Yarp/YarpResource.cs index 6084e95..baf09e3 100644 --- a/src/Aspirant.Hosting.Yarp/YarpResource.cs +++ b/src/Aspirant.Hosting.Yarp/YarpResource.cs @@ -1,238 +1,276 @@ -using System.Linq; -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Lifecycle; -using k8s.Models; +using Aspire.Hosting.ApplicationModel; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; +using Aspirant.Hosting; + namespace Aspire.Hosting; /// /// Represents a YARP resource. /// /// The name of the resource in the application model. -public class YarpResource(string name) : Resource(name), IResourceWithServiceDiscovery, IResourceWithEnvironment +public class YarpResource(string name) : WebApplicationResource(name) { - // YARP configuration internal Dictionary RouteConfigs { get; } = []; internal Dictionary ClusterConfigs { get; } = []; internal string? ConfigurationSectionName { get; set; } } -// This starts up the YARP reverse proxy with the configuration from the resource -internal class YarpResourceLifecyclehook( - IHostEnvironment hostEnvironment, - DistributedApplicationExecutionContext executionContext, - ResourceNotificationService resourceNotificationService, - ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable +internal class YarpResourceLifecycleHook(IHostEnvironment hostEnvironment, DistributedApplicationExecutionContext executionContext, ResourceNotificationService resourceNotificationService, ResourceLoggerService resourceLoggerService) : WebApplicationResourceLifecycleHook(hostEnvironment, executionContext, resourceNotificationService, resourceLoggerService) { - private WebApplication? _app; + protected override string ResourceTypeName { get; } = "Yarp"; - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + protected override ValueTask ConfigureApplicationAsync(WebApplication app, YarpResource resource, CancellationToken cancellationToken) { - if (executionContext.IsPublishMode) - { - return; - } - - var yarpResource = appModel.Resources.OfType().SingleOrDefault(); - - if (yarpResource is null) - { - return; - } + app.MapReverseProxy(); - await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with - { - ResourceType = "Yarp", - State = "Starting" - }); - - // We don't want to proxy for yarp resources so force endpoints to not proxy - var bindings = yarpResource.Annotations.OfType().ToList(); - - foreach (var b in bindings) - { - b.IsProxied = false; - } + return ValueTask.CompletedTask; } - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + protected override ValueTask ConfigureBuilderAsync(WebApplicationBuilder builder, YarpResource resource, CancellationToken cancellationToken) { - if (executionContext.IsPublishMode) - { - return; - } - - var yarpResource = appModel.Resources.OfType().SingleOrDefault(); - - if (yarpResource is null) - { - return; - } - var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions - { - ContentRootPath = hostEnvironment.ContentRootPath, - EnvironmentName = hostEnvironment.EnvironmentName, - WebRootPath = Path.Combine(hostEnvironment.ContentRootPath, "wwwroot") - }); - - builder.Logging.ClearProviders(); - - builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(yarpResource.Name))); - - // Convert environment variables into configuration - if (yarpResource.TryGetEnvironmentVariables(out var envAnnotations)) - { - var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); - - foreach (var cb in envAnnotations) - { - await cb.Callback(context); - } - - var dict = new Dictionary(); - foreach (var (k, v) in context.EnvironmentVariables) - { - var val = v switch - { - string s => s, - IValueProvider vp => await vp.GetValueAsync(context.CancellationToken), - _ => throw new NotSupportedException() - }; - - if (val is not null) - { - dict[k.Replace("__", ":")] = val; - } - } - - builder.Configuration.AddInMemoryCollection(dict); - } - builder.Services.AddServiceDiscovery(); var proxyBuilder = builder.Services.AddReverseProxy(); - if (yarpResource.RouteConfigs.Count > 0) + if (resource.RouteConfigs.Count > 0) { - proxyBuilder.LoadFromMemory([.. yarpResource.RouteConfigs.Values], [.. yarpResource.ClusterConfigs.Values]); + proxyBuilder.LoadFromMemory([.. resource.RouteConfigs.Values], [.. resource.ClusterConfigs.Values]); } - if (yarpResource.ConfigurationSectionName is not null) + if (resource.ConfigurationSectionName is not null) { - proxyBuilder.LoadFromConfig(builder.Configuration.GetSection(yarpResource.ConfigurationSectionName)); + proxyBuilder.LoadFromConfig(builder.Configuration.GetSection(resource.ConfigurationSectionName)); } proxyBuilder.AddServiceDiscoveryDestinationResolver(); - yarpResource.TryGetEndpoints(out var endpoints); - var defaultScheme = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Contains("https://") == true ? "https" : "http"; - var needHttps = defaultScheme == "https" || endpoints?.Any(ep => ep.UriScheme == "https") == true; - - if (needHttps) - { - builder.WebHost.UseKestrelHttpsConfiguration(); - } - - _app = builder.Build(); - - var urlToEndpointNameMap = new Dictionary(); - - if (endpoints is null) - { - var url = $"{defaultScheme}://127.0.0.1:0/"; - _app.Urls.Add(url); - urlToEndpointNameMap[url] = "default"; - } - else - { - foreach (var ep in endpoints) - { - var scheme = ep.UriScheme ?? defaultScheme; - needHttps = needHttps || scheme == "https"; - - var url = ep.Port switch - { - null => $"{scheme}://127.0.0.1:0/", - _ => $"{scheme}://localhost:{ep.Port}" - }; - - var uri = new Uri(url); - _app.Urls.Add(url); - urlToEndpointNameMap[uri.ToString()] = ep.Name; - } - } - - _app.MapReverseProxy(); - - await _app.StartAsync(cancellationToken); - - var addresses = _app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses; - - // Update the EndpointAnnotations with the allocated URLs from ASP.NET Core - foreach (var url in addresses) - { - if (urlToEndpointNameMap.TryGetValue(new Uri(url).ToString(), out var name) - || urlToEndpointNameMap.TryGetValue((new UriBuilder(url) { Port = 0 }).Uri.ToString(), out name)) - { - var ep = endpoints?.FirstOrDefault(ep => ep.Name == name); - if (ep is not null) - { - var uri = new Uri(url); - var host = uri.Host is "127.0.0.1" or "[::1]" ? "localhost" : uri.Host; - ep.AllocatedEndpoint = new(ep, host, uri.Port); - } - } - } - - await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with - { - State = "Running", - Urls = [.. endpoints?.Select(ep => new UrlSnapshot(ep.Name, ep.AllocatedEndpoint?.UriString ?? "", IsInternal: false))], - }); - } - - public ValueTask DisposeAsync() - { - return _app?.DisposeAsync() ?? default; + return ValueTask.CompletedTask; } +} - private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider - { - public ILogger CreateLogger(string categoryName) - { - return new ResourceLogger(logger); - } - - public void Dispose() - { - } - private class ResourceLogger(ILogger logger) : ILogger - { - public IDisposable? BeginScope(TState state) where TState : notnull - { - return logger.BeginScope(state); - } - - public bool IsEnabled(LogLevel logLevel) - { - return logger.IsEnabled(logLevel); - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - logger.Log(logLevel, eventId, state, exception, formatter); - } - } - } -} \ No newline at end of file +// /// +// /// Represents a YARP resource. +// /// +// /// The name of the resource in the application model. +// public class YarpResource(string name) : Resource(name), IResourceWithServiceDiscovery, IResourceWithEnvironment +// { +// // YARP configuration +// internal Dictionary RouteConfigs { get; } = []; +// internal Dictionary ClusterConfigs { get; } = []; +// internal string? ConfigurationSectionName { get; set; } +// } + +// // This starts up the YARP reverse proxy with the configuration from the resource +// internal class YarpResourceLifecyclehook( +// IHostEnvironment hostEnvironment, +// DistributedApplicationExecutionContext executionContext, +// ResourceNotificationService resourceNotificationService, +// ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable +// { +// private WebApplication? _app; + +// public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) +// { +// if (executionContext.IsPublishMode) +// { +// return; +// } + +// var yarpResource = appModel.Resources.OfType().SingleOrDefault(); + +// if (yarpResource is null) +// { +// return; +// } + +// await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with +// { +// ResourceType = "Yarp", +// State = "Starting" +// }); + +// // We don't want to proxy for yarp resources so force endpoints to not proxy +// var bindings = yarpResource.Annotations.OfType().ToList(); + +// foreach (var b in bindings) +// { +// b.IsProxied = false; +// } +// } + +// public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) +// { +// if (executionContext.IsPublishMode) +// { +// return; +// } + +// var yarpResource = appModel.Resources.OfType().SingleOrDefault(); + +// if (yarpResource is null) +// { +// return; +// } +// var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions +// { +// ContentRootPath = hostEnvironment.ContentRootPath, +// EnvironmentName = hostEnvironment.EnvironmentName, +// WebRootPath = Path.Combine(hostEnvironment.ContentRootPath, "wwwroot") +// }); + +// builder.Logging.ClearProviders(); + +// builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(yarpResource.Name))); + +// // Convert environment variables into configuration +// if (yarpResource.TryGetEnvironmentVariables(out var envAnnotations)) +// { +// var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken); + +// foreach (var cb in envAnnotations) +// { +// await cb.Callback(context); +// } + +// var dict = new Dictionary(); +// foreach (var (k, v) in context.EnvironmentVariables) +// { +// var val = v switch +// { +// string s => s, +// IValueProvider vp => await vp.GetValueAsync(context.CancellationToken), +// _ => throw new NotSupportedException() +// }; + +// if (val is not null) +// { +// dict[k.Replace("__", ":")] = val; +// } +// } + +// builder.Configuration.AddInMemoryCollection(dict); +// } + +// builder.Services.AddServiceDiscovery(); + +// var proxyBuilder = builder.Services.AddReverseProxy(); + +// if (yarpResource.RouteConfigs.Count > 0) +// { +// proxyBuilder.LoadFromMemory([.. yarpResource.RouteConfigs.Values], [.. yarpResource.ClusterConfigs.Values]); +// } + +// if (yarpResource.ConfigurationSectionName is not null) +// { +// proxyBuilder.LoadFromConfig(builder.Configuration.GetSection(yarpResource.ConfigurationSectionName)); +// } + +// proxyBuilder.AddServiceDiscoveryDestinationResolver(); + +// yarpResource.TryGetEndpoints(out var endpoints); +// var defaultScheme = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Contains("https://") == true ? "https" : "http"; +// var needHttps = defaultScheme == "https" || endpoints?.Any(ep => ep.UriScheme == "https") == true; + +// if (needHttps) +// { +// builder.WebHost.UseKestrelHttpsConfiguration(); +// } + +// _app = builder.Build(); + +// var urlToEndpointNameMap = new Dictionary(); + +// if (endpoints is null) +// { +// var url = $"{defaultScheme}://127.0.0.1:0/"; +// _app.Urls.Add(url); +// urlToEndpointNameMap[url] = "default"; +// } +// else +// { +// foreach (var ep in endpoints) +// { +// var scheme = ep.UriScheme ?? defaultScheme; +// needHttps = needHttps || scheme == "https"; + +// var url = ep.Port switch +// { +// null => $"{scheme}://127.0.0.1:0/", +// _ => $"{scheme}://localhost:{ep.Port}" +// }; + +// var uri = new Uri(url); +// _app.Urls.Add(url); +// urlToEndpointNameMap[uri.ToString()] = ep.Name; +// } +// } + +// _app.MapReverseProxy(); + +// await _app.StartAsync(cancellationToken); + +// var addresses = _app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses; + +// // Update the EndpointAnnotations with the allocated URLs from ASP.NET Core +// foreach (var url in addresses) +// { +// if (urlToEndpointNameMap.TryGetValue(new Uri(url).ToString(), out var name) +// || urlToEndpointNameMap.TryGetValue((new UriBuilder(url) { Port = 0 }).Uri.ToString(), out name)) +// { +// var ep = endpoints?.FirstOrDefault(ep => ep.Name == name); +// if (ep is not null) +// { +// var uri = new Uri(url); +// var host = uri.Host is "127.0.0.1" or "[::1]" ? "localhost" : uri.Host; +// ep.AllocatedEndpoint = new(ep, host, uri.Port); +// } +// } +// } + +// await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with +// { +// State = "Running", +// Urls = [.. endpoints?.Select(ep => new UrlSnapshot(ep.Name, ep.AllocatedEndpoint?.UriString ?? "", IsInternal: false))], +// }); +// } + +// public ValueTask DisposeAsync() +// { +// return _app?.DisposeAsync() ?? default; +// } + +// private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider +// { +// public ILogger CreateLogger(string categoryName) +// { +// return new ResourceLogger(logger); +// } + +// public void Dispose() +// { +// } + +// private class ResourceLogger(ILogger logger) : ILogger +// { +// public IDisposable? BeginScope(TState state) where TState : notnull +// { +// return logger.BeginScope(state); +// } + +// public bool IsEnabled(LogLevel logLevel) +// { +// return logger.IsEnabled(logLevel); +// } + +// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) +// { +// logger.Log(logLevel, eventId, state, exception, formatter); +// } +// } +// } +// } \ No newline at end of file diff --git a/src/Aspirant.Hosting.Yarp/YarpResourceExtensions.cs b/src/Aspirant.Hosting.Yarp/YarpResourceExtensions.cs index efb3d49..ff7d381 100644 --- a/src/Aspirant.Hosting.Yarp/YarpResourceExtensions.cs +++ b/src/Aspirant.Hosting.Yarp/YarpResourceExtensions.cs @@ -1,5 +1,5 @@ +using Aspirant.Hosting; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Lifecycle; namespace Aspire.Hosting; @@ -17,48 +17,9 @@ public static class YarpResourceExtensions /// public static IResourceBuilder AddYarp(this IDistributedApplicationBuilder builder, string name) { - var yarp = builder.Resources.OfType().SingleOrDefault(); - - if (yarp is not null) - { - // You only need one yarp resource per application - throw new InvalidOperationException("A yarp resource has already been added to this application"); - } - - builder.Services.TryAddLifecycleHook(); - var resource = new YarpResource(name); - return builder.AddResource(resource).ExcludeFromManifest(); - - // REVIEW: YARP resource type? - //.WithManifestPublishingCallback(context => - // { - // context.Writer.WriteString("type", "yarp.v0"); - // context.Writer.WriteStartObject("routes"); - // // REVIEW: Make this less YARP specific - // foreach (var r in resource.RouteConfigs.Values) - // { - // context.Writer.WriteStartObject(r.RouteId); - - // context.Writer.WriteStartObject("match"); - // context.Writer.WriteString("path", r.Match.Path); - - // if (r.Match.Hosts is not null) - // { - // context.Writer.WriteStartArray("hosts"); - // foreach (var h in r.Match.Hosts) - // { - // context.Writer.WriteStringValue(h); - // } - // context.Writer.WriteEndArray(); - // } - // context.Writer.WriteEndObject(); - // context.Writer.WriteString("destination", r.ClusterId); - // context.Writer.WriteEndObject(); - // } - // context.Writer.WriteEndObject(); - // }); + return builder.AddWebApplication(resource, excludeFromManifest: true); } /// @@ -73,3 +34,74 @@ public static IResourceBuilder LoadFromConfiguration(this IResourc return builder; } } + +// /// +// /// Extensions for the . +// /// +// public static class YarpResourceExtensions +// { +// /// +// /// Adds a YARP resource to the application. +// /// +// /// The builder. +// /// The name of the resource. +// /// The builder. +// /// +// public static IResourceBuilder AddYarp(this IDistributedApplicationBuilder builder, string name) +// { +// var yarp = builder.Resources.OfType().SingleOrDefault(); + +// if (yarp is not null) +// { +// // You only need one yarp resource per application +// throw new InvalidOperationException("A yarp resource has already been added to this application"); +// } + +// builder.Services.TryAddLifecycleHook(); + +// var resource = new YarpResource(name); +// return builder.AddResource(resource).ExcludeFromManifest(); + +// // REVIEW: YARP resource type? +// //.WithManifestPublishingCallback(context => +// // { +// // context.Writer.WriteString("type", "yarp.v0"); + +// // context.Writer.WriteStartObject("routes"); +// // // REVIEW: Make this less YARP specific +// // foreach (var r in resource.RouteConfigs.Values) +// // { +// // context.Writer.WriteStartObject(r.RouteId); + +// // context.Writer.WriteStartObject("match"); +// // context.Writer.WriteString("path", r.Match.Path); + +// // if (r.Match.Hosts is not null) +// // { +// // context.Writer.WriteStartArray("hosts"); +// // foreach (var h in r.Match.Hosts) +// // { +// // context.Writer.WriteStringValue(h); +// // } +// // context.Writer.WriteEndArray(); +// // } +// // context.Writer.WriteEndObject(); +// // context.Writer.WriteString("destination", r.ClusterId); +// // context.Writer.WriteEndObject(); +// // } +// // context.Writer.WriteEndObject(); +// // }); +// } + +// /// +// /// Loads the YARP configuration from the specified configuration section. +// /// +// /// The builder. +// /// The configuration section name to load from. +// /// The builder. +// public static IResourceBuilder LoadFromConfiguration(this IResourceBuilder builder, string sectionName) +// { +// builder.Resource.ConfigurationSectionName = sectionName; +// return builder; +// } +// }