diff --git a/samples/ReverseProxy.Config.Sample/Startup.cs b/samples/ReverseProxy.Config.Sample/Startup.cs index c8b45cbe76..135e8f5f2f 100644 --- a/samples/ReverseProxy.Config.Sample/Startup.cs +++ b/samples/ReverseProxy.Config.Sample/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.ReverseProxy.Middleware; @@ -63,7 +64,9 @@ public void Configure(IApplicationBuilder app) proxyPipeline.UseAffinitizedDestinationLookup(); proxyPipeline.UseProxyLoadBalancing(); proxyPipeline.UseRequestAffinitizer(); - }); + }) + .ConfigureRoutes((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}")) + .ConfigureRoute("route1", builder => builder.WithDisplayName("ReverseProxy (My special route name)")); }); } } diff --git a/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs b/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs index d346eb58d3..3da5ca11b5 100644 --- a/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs +++ b/src/ReverseProxy/Abstractions/RouteDiscovery/IRuntimeRouteBuilder.cs @@ -29,10 +29,5 @@ internal interface IRuntimeRouteBuilder /// and . /// RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute); - - /// - /// Sets the middleware pipeline to use when building routes. - /// - void SetProxyPipeline(RequestDelegate pipeline); } } diff --git a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs index f6c6f3fa26..e1a32b3285 100644 --- a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs +++ b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract; using Microsoft.ReverseProxy.Configuration.Contract; using Microsoft.ReverseProxy.Service; diff --git a/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs b/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs index e52ee2e063..5cf77741d8 100644 --- a/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs +++ b/src/ReverseProxy/Configuration/DependencyInjection/ReverseProxyServiceCollectionExtensions.cs @@ -3,6 +3,8 @@ using System; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.ReverseProxy; using Microsoft.ReverseProxy.Configuration; using Microsoft.ReverseProxy.Configuration.Contract; using Microsoft.ReverseProxy.Configuration.DependencyInjection; @@ -32,6 +34,8 @@ public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection servi .AddProxy() .AddBackgroundWorkers(); + services.TryAddSingleton(); + services.AddDataProtection(); services.AddAuthorization(); services.AddCors(); diff --git a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs index 4970d23d69..712a49c996 100644 --- a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs +++ b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs @@ -2,11 +2,14 @@ // Licensed under the MIT License. using System; +using System.Linq; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy; using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Middleware; using Microsoft.ReverseProxy.Service; +using Microsoft.ReverseProxy.Service.Management; namespace Microsoft.AspNetCore.Builder { @@ -33,7 +36,7 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints) /// Adds Reverse Proxy routes to the route table with the customized processing pipeline. The pipeline includes /// by default the initialization step and the final proxy step, but not LoadBalancingMiddleware or other intermediate components. /// - public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action configureApp) + public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints, Action configureApp) { if (endpoints is null) { @@ -50,16 +53,27 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action< appBuilder.UseMiddleware(); var app = appBuilder.Build(); - var routeBuilder = endpoints.ServiceProvider.GetRequiredService(); - routeBuilder.SetProxyPipeline(app); + var proxyEndpointFactory = endpoints.ServiceProvider.GetRequiredService(); + proxyEndpointFactory.SetProxyPipeline(app); - var configManager = endpoints.ServiceProvider.GetRequiredService(); + return GetOrCreateDataSource(endpoints).DefaultBuilder; + } + + private static ProxyConfigManager GetOrCreateDataSource(IEndpointRouteBuilder endpoints) + { + var dataSource = (ProxyConfigManager)endpoints.DataSources.OfType().FirstOrDefault(); + if (dataSource == null) + { + dataSource = (ProxyConfigManager)endpoints.ServiceProvider.GetRequiredService(); + endpoints.DataSources.Add(dataSource); + + // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent + // the app from starting, and B) so that all the config is ready before the server starts accepting requests. + // Reloads will be async. + dataSource.InitialLoadAsync().GetAwaiter().GetResult(); + } - // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent - // the app from starting, and B) so that all the config is ready before the server starts accepting requests. - // Reloads will be async. - var dataSource = configManager.InitialLoadAsync().GetAwaiter().GetResult(); - endpoints.DataSources.Add(dataSource); + return dataSource; } } } diff --git a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs index 22a051b366..c1870325c4 100644 --- a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs +++ b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs @@ -21,23 +21,13 @@ namespace Microsoft.ReverseProxy.Service /// internal class RuntimeRouteBuilder : IRuntimeRouteBuilder { - private static readonly IAuthorizeData DefaultAuthorization = new AuthorizeAttribute(); - private static readonly IEnableCorsAttribute DefaultCors = new EnableCorsAttribute(); - private static readonly IDisableCorsAttribute DisableCors = new DisableCorsAttribute(); - private readonly ITransformBuilder _transformBuilder; - private RequestDelegate _pipeline; public RuntimeRouteBuilder(ITransformBuilder transformBuilder) { _transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder)); } - public void SetProxyPipeline(RequestDelegate pipeline) - { - _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); - } - /// public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute) { @@ -46,74 +36,12 @@ public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runti var transforms = _transformBuilder.Build(source.Transforms); - // NOTE: `new RouteConfig(...)` needs a reference to the list of ASP .NET Core endpoints, - // but the ASP .NET Core endpoints cannot be created without a `RouteConfig` metadata item. - // We solve this chicken-egg problem by creating an (empty) list first - // and passing a read-only wrapper of it to `RouteConfig.ctor`. - // Recall that `List.AsReadOnly()` creates a wrapper over the original list, - // and changes to the underlying list *are* reflected on the read-only view. - var aspNetCoreEndpoints = new List(1); var newRouteConfig = new RouteConfig( runtimeRoute, source, cluster, - aspNetCoreEndpoints.AsReadOnly(), transforms); - // Catch-all pattern when no path was specified - var pathPattern = string.IsNullOrEmpty(source.Match.Path) ? "/{**catchall}" : source.Match.Path; - - var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder( - requestDelegate: _pipeline, - routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern), - order: source.Order.GetValueOrDefault()); - - endpointBuilder.DisplayName = source.RouteId; - endpointBuilder.Metadata.Add(newRouteConfig); - - if (source.Match.Hosts != null && source.Match.Hosts.Count != 0) - { - endpointBuilder.Metadata.Add(new AspNetCore.Routing.HostAttribute(source.Match.Hosts.ToArray())); - } - - bool acceptCorsPreflight; - if (string.Equals(CorsConstants.Default, source.CorsPolicy, StringComparison.OrdinalIgnoreCase)) - { - endpointBuilder.Metadata.Add(DefaultCors); - acceptCorsPreflight = true; - } - else if (string.Equals(CorsConstants.Disable, source.CorsPolicy, StringComparison.OrdinalIgnoreCase)) - { - endpointBuilder.Metadata.Add(DisableCors); - acceptCorsPreflight = true; - } - else if (!string.IsNullOrEmpty(source.CorsPolicy)) - { - endpointBuilder.Metadata.Add(new EnableCorsAttribute(source.CorsPolicy)); - acceptCorsPreflight = true; - } - else - { - acceptCorsPreflight = false; - } - - if (source.Match.Methods != null && source.Match.Methods.Count > 0) - { - endpointBuilder.Metadata.Add(new AspNetCore.Routing.HttpMethodMetadata(source.Match.Methods, acceptCorsPreflight)); - } - - if (string.Equals(AuthorizationConstants.Default, source.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)) - { - endpointBuilder.Metadata.Add(DefaultAuthorization); - } - else if (!string.IsNullOrEmpty(source.AuthorizationPolicy)) - { - endpointBuilder.Metadata.Add(new AuthorizeAttribute(source.AuthorizationPolicy)); - } - - var endpoint = endpointBuilder.Build(); - aspNetCoreEndpoints.Add(endpoint); - return newRouteConfig; } } diff --git a/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs b/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs new file mode 100644 index 0000000000..176d708c16 --- /dev/null +++ b/src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract; +using Microsoft.ReverseProxy.RuntimeModel; +using CorsConstants = Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract.CorsConstants; + +namespace Microsoft.ReverseProxy +{ + internal class ProxyEndpointFactory + { + private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute(); + private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute(); + private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute(); + + private RequestDelegate _pipeline; + + public void AddEndpoint(List endpoints, + RouteConfig route, + IReadOnlyList> conventions) + { + var proxyRoute = route.ProxyRoute; + var proxyMatch = proxyRoute.Match; + + // Catch-all pattern when no path was specified + var pathPattern = string.IsNullOrEmpty(proxyMatch.Path) ? "/{**catchall}" : proxyMatch.Path; + + var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder( + requestDelegate: _pipeline ?? Invoke, + routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern), + order: proxyRoute.Order.GetValueOrDefault()) + { + DisplayName = proxyRoute.RouteId + }; + + endpointBuilder.Metadata.Add(route); + + if (proxyMatch.Hosts != null && proxyMatch.Hosts.Count != 0) + { + endpointBuilder.Metadata.Add(new HostAttribute(proxyMatch.Hosts.ToArray())); + } + + bool acceptCorsPreflight; + if (string.Equals(CorsConstants.Default, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase)) + { + endpointBuilder.Metadata.Add(_defaultCors); + acceptCorsPreflight = true; + } + else if (string.Equals(CorsConstants.Disable, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase)) + { + endpointBuilder.Metadata.Add(_disableCors); + acceptCorsPreflight = true; + } + else if (!string.IsNullOrEmpty(proxyRoute.CorsPolicy)) + { + endpointBuilder.Metadata.Add(new EnableCorsAttribute(proxyRoute.CorsPolicy)); + acceptCorsPreflight = true; + } + else + { + acceptCorsPreflight = false; + } + + if (proxyMatch.Methods != null && proxyMatch.Methods.Count > 0) + { + endpointBuilder.Metadata.Add(new HttpMethodMetadata(proxyMatch.Methods, acceptCorsPreflight)); + } + + if (string.Equals(AuthorizationConstants.Default, proxyRoute.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)) + { + endpointBuilder.Metadata.Add(_defaultAuthorization); + } + else if (!string.IsNullOrEmpty(proxyRoute.AuthorizationPolicy)) + { + endpointBuilder.Metadata.Add(new AuthorizeAttribute(proxyRoute.AuthorizationPolicy)); + } + + for (var i = 0; i < conventions.Count; i++) + { + conventions[i](endpointBuilder); + } + + endpoints.Add(endpointBuilder.Build()); + } + + // This indirection is needed because on startup the routes are loaded from config and built before the + // proxy pipeline gets built. + private Task Invoke(HttpContext context) + { + var pipeline = _pipeline ?? throw new InvalidOperationException("The pipeline hasn't been provided yet."); + return pipeline(context); + } + + public void SetProxyPipeline(RequestDelegate pipeline) + { + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + } + } +} diff --git a/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs b/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs new file mode 100644 index 0000000000..5586ad2602 --- /dev/null +++ b/src/ReverseProxy/Service/DynamicEndpoint/ReverseProxyConventionBuilder.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.RuntimeModel; +using Microsoft.ReverseProxy.Service; + +namespace Microsoft.ReverseProxy +{ + public class ReverseProxyConventionBuilder : IEndpointConventionBuilder + { + // The lock is shared with the data source. + private readonly object _lock; + private readonly List> _conventions; + private readonly IProxyConfigProvider _proxyConfigProvider; + + internal ReverseProxyConventionBuilder(object @lock, List> conventions, IProxyConfigProvider proxyConfigProvider) + { + _lock = @lock; + _conventions = conventions; + _proxyConfigProvider = proxyConfigProvider; + } + + /// + /// Adds the specified convention to the builder. Conventions are used to customize instances. + /// + /// The convention to add to the builder. + public void Add(Action convention) + { + if (convention == null) + { + throw new ArgumentNullException(nameof(convention)); + } + + // The lock is shared with the data source. We want to lock here + // to avoid mutating this list while its read in the data source. + lock (_lock) + { + _conventions.Add(convention); + } + } + + public ReverseProxyConventionBuilder ConfigureClusters(Action convention) + { + void Action(EndpointBuilder endpointBuilder) + { + var routeConfig = endpointBuilder.Metadata.OfType().Single(); + + var config = _proxyConfigProvider.GetConfig(); + var cluster = config.Clusters.Single(x => x.Id.Equals(routeConfig.Cluster.ClusterId, StringComparison.OrdinalIgnoreCase)); + + var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); + convention(conventionBuilder, cluster.DeepClone()); + } + + Add(Action); + + return this; + } + + public ReverseProxyConventionBuilder ConfigureRoutes(Action convention) + { + void Action(EndpointBuilder endpointBuilder) + { + var routeConfig = endpointBuilder.Metadata.OfType().Single(); + + var config = _proxyConfigProvider.GetConfig(); + var route = config.Routes.Single(x => x.RouteId.Equals(routeConfig.Cluster.ClusterId, StringComparison.OrdinalIgnoreCase)); + + var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); + convention(conventionBuilder, route.DeepClone()); + } + + Add(Action); + + return this; + } + + public ReverseProxyConventionBuilder ConfigureCluster(string clusterId, Action convention) + { + void Action(EndpointBuilder endpointBuilder) + { + var routeConfig = endpointBuilder.Metadata.OfType().Single(); + if (routeConfig.Cluster.ClusterId.Equals(clusterId, StringComparison.OrdinalIgnoreCase)) + { + var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); + convention(conventionBuilder); + } + } + + Add(Action); + + return this; + } + + public ReverseProxyConventionBuilder ConfigureRoute(string routeId, Action convention) + { + void Action(EndpointBuilder endpointBuilder) + { + var routeConfig = endpointBuilder.Metadata.OfType().Single(); + if (routeConfig.Route.RouteId.Equals(routeId, StringComparison.OrdinalIgnoreCase)) + { + var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); + convention(conventionBuilder); + } + } + + Add(Action); + + return this; + } + + internal class EndpointBuilderConventionBuilder : IEndpointConventionBuilder + { + private readonly EndpointBuilder _endpointBuilder; + + public EndpointBuilderConventionBuilder(EndpointBuilder endpointBuilder) + { + _endpointBuilder = endpointBuilder; + } + + public void Add(Action convention) + { + convention(_endpointBuilder); + } + } + + } +} diff --git a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs index 79631db271..e233929202 100644 --- a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs +++ b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs @@ -4,10 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Security; -using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; @@ -39,9 +38,11 @@ internal class ProxyConfigManager : EndpointDataSource, IProxyConfigManager, IDi private readonly IEnumerable _filters; private readonly IConfigValidator _configValidator; private readonly IProxyHttpClientFactory _httpClientFactory; + private readonly ProxyEndpointFactory _proxyEndpointFactory; + private readonly List> _conventions; private IDisposable _changeSubscription; - private List _endpoints = new List(0); + private List _endpoints; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private IChangeToken _changeToken; @@ -53,6 +54,7 @@ public ProxyConfigManager( IRouteManager routeManager, IEnumerable filters, IConfigValidator configValidator, + ProxyEndpointFactory proxyEndpointFactory, IProxyHttpClientFactory httpClientFactory) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -62,15 +64,53 @@ public ProxyConfigManager( _routeManager = routeManager ?? throw new ArgumentNullException(nameof(routeManager)); _filters = filters ?? throw new ArgumentNullException(nameof(filters)); _configValidator = configValidator ?? throw new ArgumentNullException(nameof(configValidator)); + _proxyEndpointFactory = proxyEndpointFactory; + _conventions = new List>(); _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + DefaultBuilder = new ReverseProxyConventionBuilder(_syncRoot, _conventions, provider); _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); } + public ReverseProxyConventionBuilder DefaultBuilder { get; } + // EndpointDataSource /// - public override IReadOnlyList Endpoints => Volatile.Read(ref _endpoints); + public override IReadOnlyList Endpoints + { + get + { + Initialize(); + return _endpoints; + } + } + + private void Initialize() + { + if (_endpoints == null) + { + lock (_syncRoot) + { + if (_endpoints == null) + { + CreateEndpoints(); + } + } + } + } + + private void CreateEndpoints() + { + var endpoints = new List(); + foreach (var existingRoute in _routeManager.GetItems()) + { + var runtimeConfig = existingRoute.Config.Value; + _proxyEndpointFactory.AddEndpoint(endpoints, runtimeConfig, _conventions); + } + + UpdateEndpoints(endpoints); + } /// public override IChangeToken GetChangeToken() => Volatile.Read(ref _changeToken); @@ -124,7 +164,11 @@ private async Task ReloadConfigAsync() try { - await ApplyConfigAsync(newConfig); + var hasChanged = await ApplyConfigAsync(newConfig); + if (hasChanged) + { + CreateEndpoints(); + } } catch (Exception ex) { @@ -138,7 +182,7 @@ private async Task ReloadConfigAsync() } // Throws for validation failures - private async Task ApplyConfigAsync(IProxyConfig config) + private async Task ApplyConfigAsync(IProxyConfig config) { var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(config.Routes, cancellation: default); var (configuredClusters, clusterErrors) = await VerifyClustersAsync(config.Clusters, cancellation: default); @@ -150,7 +194,8 @@ private async Task ApplyConfigAsync(IProxyConfig config) // Update clusters first because routes need to reference them. UpdateRuntimeClusters(configuredClusters); - UpdateRuntimeRoutes(configuredRoutes); + var hasChanged = UpdateRuntimeRoutes(configuredRoutes); + return hasChanged; } private async Task<(IList, IList)> VerifyRoutesAsync(IReadOnlyList routes, CancellationToken cancellation) @@ -380,7 +425,7 @@ private void UpdateRuntimeDestinations(IDictionary newDesti } } - private void UpdateRuntimeRoutes(IList routes) + private bool UpdateRuntimeRoutes(IList routes) { var desiredRoutes = new HashSet(StringComparer.OrdinalIgnoreCase); var changed = false; @@ -435,20 +480,7 @@ private void UpdateRuntimeRoutes(IList routes) } } - if (changed) - { - var endpoints = new List(); - foreach (var existingRoute in _routeManager.GetItems()) - { - var runtimeConfig = existingRoute.Config.Value; - if (runtimeConfig?.Endpoints != null) - { - endpoints.AddRange(runtimeConfig.Endpoints); - } - } - - UpdateEndpoints(endpoints); - } + return changed; } /// diff --git a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs index 3c48b0deea..4489a64f35 100644 --- a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs +++ b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs @@ -20,19 +20,15 @@ namespace Microsoft.ReverseProxy.RuntimeModel /// internal sealed class RouteConfig { - private readonly ProxyRoute _proxyRoute; - public RouteConfig( RouteInfo route, ProxyRoute proxyRoute, ClusterInfo cluster, - IReadOnlyList aspNetCoreEndpoints, Transforms transforms) { Route = route ?? throw new ArgumentNullException(nameof(route)); - Endpoints = aspNetCoreEndpoints ?? throw new ArgumentNullException(nameof(aspNetCoreEndpoints)); - _proxyRoute = proxyRoute; + ProxyRoute = proxyRoute; Order = proxyRoute.Order; Cluster = cluster; Transforms = transforms; @@ -45,14 +41,14 @@ public RouteConfig( // May not be populated if the cluster config is missing. public ClusterInfo Cluster { get; } - public IReadOnlyList Endpoints { get; } - public Transforms Transforms { get; } + internal ProxyRoute ProxyRoute { get; } + public bool HasConfigChanged(ProxyRoute newConfig, ClusterInfo cluster) { return Cluster != cluster - || !ProxyRoute.Equals(_proxyRoute, newConfig); + || !ProxyRoute.Equals(ProxyRoute, newConfig); } } } diff --git a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs index 05ba59400a..b04fc006f9 100644 --- a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs +++ b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs @@ -91,11 +91,9 @@ internal IReverseProxyFeature GetDestinationsFeature(IReadOnlyList(1); var proxyRoute = new ProxyRoute(); - var routeConfig = new RouteConfig(new RouteInfo("route-1"), proxyRoute, cluster, endpoints.AsReadOnly(), Transforms.Empty); + var routeConfig = new RouteConfig(new RouteInfo("route-1"), proxyRoute, cluster, Transforms.Empty); var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeConfig), string.Empty); - endpoints.Add(endpoint); return endpoint; } } diff --git a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs index 980ae15d8e..35a9f0eddf 100644 --- a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs @@ -51,7 +51,6 @@ public async Task Invoke_SetsFeatures() new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster1, - aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); @@ -99,7 +98,6 @@ public async Task Invoke_NoHealthyEndpoints_503() route: new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster: cluster1, - aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); diff --git a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs index c6721690d6..6df3b8f0b4 100644 --- a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs @@ -61,7 +61,6 @@ public async Task Invoke_Works() route: new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster: cluster1, - aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); @@ -118,7 +117,6 @@ public async Task Invoke_ServiceReturnsNoResults_503() route: new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster: cluster1, - aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); diff --git a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs index d65c0036e1..e5e67c77b7 100644 --- a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs @@ -67,7 +67,6 @@ public async Task Invoke_Works() route: new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster: cluster1, - aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); @@ -137,7 +136,6 @@ public async Task NoDestinations_503() route: new RouteInfo("route1"), proxyRoute: new ProxyRoute(), cluster: cluster1, - aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); diff --git a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs index e334efde23..15847a6188 100644 --- a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs +++ b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs @@ -37,11 +37,10 @@ public void Constructor_Works() } [Fact] - public void BuildEndpoints_HostAndPath_Works() + public void BuildEndpoints_Works() { var services = CreateServices(); var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); var route = new ProxyRoute { @@ -59,351 +58,11 @@ public void BuildEndpoints_HostAndPath_Works() var config = builder.Build(route, cluster, routeInfo); Assert.Same(cluster, config.Cluster); + Assert.Same(route, config.ProxyRoute); + Assert.Same(routeInfo, config.Route); Assert.Equal(12, config.Order); - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Equal("route1", routeEndpoint.DisplayName); - Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); - Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); - - var hostMetadata = routeEndpoint.Metadata.GetMetadata(); - Assert.NotNull(hostMetadata); - Assert.Single(hostMetadata.Hosts); - Assert.Equal("example.com", hostMetadata.Hosts[0]); - } - - [Fact] - public void BuildEndpoints_JustHost_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Match = - { - Hosts = new[] { "example.com" }, - }, - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Same(cluster, config.Cluster); - Assert.Equal(12, config.Order); - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Equal("route1", routeEndpoint.DisplayName); - Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); - Assert.Equal(12, routeEndpoint.Order); - Assert.False(config.HasConfigChanged(route, cluster)); - - var hostMetadata = routeEndpoint.Metadata.GetMetadata(); - Assert.NotNull(hostMetadata); - Assert.Single(hostMetadata.Hosts); - Assert.Equal("example.com", hostMetadata.Hosts[0]); - } - - [Fact] - public void BuildEndpoints_JustHostWithWildcard_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Match = - { - Hosts = new[] { "*.example.com" }, - }, - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Same(cluster, config.Cluster); - Assert.Equal(12, config.Order); - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Equal("route1", routeEndpoint.DisplayName); - Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); - Assert.Equal(12, routeEndpoint.Order); - Assert.False(config.HasConfigChanged(route, cluster)); - - var hostMetadata = routeEndpoint.Metadata.GetMetadata(); - Assert.NotNull(hostMetadata); - Assert.Single(hostMetadata.Hosts); - Assert.Equal("*.example.com", hostMetadata.Hosts[0]); - } - - [Fact] - public void BuildEndpoints_JustPath_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Match = - { - Path = "/a", - }, - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Same(cluster, config.Cluster); - Assert.Equal(12, config.Order); - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Equal("route1", routeEndpoint.DisplayName); - Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); - Assert.Equal(12, routeEndpoint.Order); - Assert.False(config.HasConfigChanged(route, cluster)); - - var hostMetadata = routeEndpoint.Metadata.GetMetadata(); - Assert.Null(hostMetadata); - } - - [Fact] - public void BuildEndpoints_NullMatchers_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Same(cluster, config.Cluster); - Assert.Equal(12, config.Order); - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Equal("route1", routeEndpoint.DisplayName); - Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); - Assert.Equal(12, routeEndpoint.Order); - Assert.False(config.HasConfigChanged(route, cluster)); - - var hostMetadata = routeEndpoint.Metadata.GetMetadata(); - Assert.Null(hostMetadata); - } - - [Fact] - public void BuildEndpoints_InvalidPath_BubblesOutException() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Match = - { - Path = "/{invalid", - }, - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - Action action = () => builder.Build(route, cluster, routeInfo); - - Assert.Throws(action); - } - - [Fact] - public void BuildEndpoints_DefaultAuth_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - AuthorizationPolicy = "defaulT", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); - Assert.Null(attribute.Policy); - } - - [Fact] - public void BuildEndpoints_CustomAuth_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - AuthorizationPolicy = "custom", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("custom", attribute.Policy); - } - - [Fact] - public void BuildEndpoints_NoAuth_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - } - - [Fact] - public void BuildEndpoints_DefaultCors_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - CorsPolicy = "defaulT", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); - Assert.Null(attribute.PolicyName); - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - } - - [Fact] - public void BuildEndpoints_CustomCors_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - CorsPolicy = "custom", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); - Assert.Equal("custom", attribute.PolicyName); - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - } - - [Fact] - public void BuildEndpoints_DisableCors_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - CorsPolicy = "disAble", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.IsType(routeEndpoint.Metadata.GetMetadata()); - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - } - - [Fact] - public void BuildEndpoints_NoCors_Works() - { - var services = CreateServices(); - var builder = services.GetRequiredService(); - builder.SetProxyPipeline(context => Task.CompletedTask); - - var route = new ProxyRoute - { - RouteId = "route1", - Order = 12, - }; - var cluster = new ClusterInfo("cluster1", new DestinationManager()); - var routeInfo = new RouteInfo("route1"); - - var config = builder.Build(route, cluster, routeInfo); - - Assert.Single(config.Endpoints); - var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; - Assert.Null(routeEndpoint.Metadata.GetMetadata()); - Assert.Null(routeEndpoint.Metadata.GetMetadata()); } } } diff --git a/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs b/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs new file mode 100644 index 0000000000..5e5911b4ef --- /dev/null +++ b/test/ReverseProxy.Tests/Service/DynamicEndpoint/ProxyEndpointFactorTests.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.RuntimeModel; +using Microsoft.ReverseProxy.Service.Management; +using Microsoft.ReverseProxy.Service.RuntimeModel.Transforms; +using Xunit; + +namespace Microsoft.ReverseProxy.Service.DynamicEndpoint +{ + public class ProxyEndpointFactorTests + { + private IServiceProvider CreateServices() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + return serviceCollection.BuildServiceProvider(); + } + + [Fact] + public void Constructor_Works() + { + var services = CreateServices(); + _ = services.GetRequiredService(); + } + + [Fact] + public void AddEndpoint_HostAndPath_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Match = + { + Hosts = new[] { "example.com" }, + Path = "/a", + }, + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Same(cluster, routeConfig.Cluster); + Assert.Equal(12, routeConfig.Order); + Assert.Equal("route1", routeEndpoint.DisplayName); + Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); + Assert.Equal(12, routeEndpoint.Order); + Assert.False(routeConfig.HasConfigChanged(route, cluster)); + + var hostMetadata = routeEndpoint.Metadata.GetMetadata(); + Assert.NotNull(hostMetadata); + Assert.Single(hostMetadata.Hosts); + Assert.Equal("example.com", hostMetadata.Hosts[0]); + } + + private (RouteEndpoint routeEndpoint, RouteConfig routeConfig) CreateEndpoint(ProxyEndpointFactory factory, RouteInfo routeInfo, ProxyRoute proxyRoute, ClusterInfo clusterInfo) + { + var routeConfig = new RouteConfig(routeInfo, proxyRoute, clusterInfo, Transforms.Empty); + var endpoints = new List(); + + factory.AddEndpoint(endpoints, routeConfig, Array.Empty>()); + + var endpoint = Assert.Single(endpoints); + var routeEndpoint = Assert.IsType(endpoint); + + return (routeEndpoint, routeConfig); + } + + [Fact] + public void AddEndpoint_JustHost_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Match = + { + Hosts = new[] { "example.com" }, + }, + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Same(cluster, routeConfig.Cluster); + Assert.Equal(12, routeConfig.Order); + Assert.Equal("route1", routeEndpoint.DisplayName); + Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); + Assert.Equal(12, routeEndpoint.Order); + Assert.False(routeConfig.HasConfigChanged(route, cluster)); + + var hostMetadata = routeEndpoint.Metadata.GetMetadata(); + Assert.NotNull(hostMetadata); + Assert.Single(hostMetadata.Hosts); + Assert.Equal("example.com", hostMetadata.Hosts[0]); + } + + [Fact] + public void AddEndpoint_JustHostWithWildcard_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Match = + { + Hosts = new[] { "*.example.com" }, + }, + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Same(cluster, routeConfig.Cluster); + Assert.Equal(12, routeConfig.Order); + Assert.Equal("route1", routeEndpoint.DisplayName); + Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); + Assert.Equal(12, routeEndpoint.Order); + Assert.False(routeConfig.HasConfigChanged(route, cluster)); + + var hostMetadata = routeEndpoint.Metadata.GetMetadata(); + Assert.NotNull(hostMetadata); + Assert.Single(hostMetadata.Hosts); + Assert.Equal("*.example.com", hostMetadata.Hosts[0]); + } + + [Fact] + public void AddEndpoint_JustPath_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Match = + { + Path = "/a", + }, + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Same(cluster, routeConfig.Cluster); + Assert.Equal(12, routeConfig.Order); + Assert.Equal("route1", routeEndpoint.DisplayName); + Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); + Assert.Equal(12, routeEndpoint.Order); + Assert.False(routeConfig.HasConfigChanged(route, cluster)); + + var hostMetadata = routeEndpoint.Metadata.GetMetadata(); + Assert.Null(hostMetadata); + } + + [Fact] + public void AddEndpoint_NullMatchers_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Same(cluster, routeConfig.Cluster); + Assert.Equal(12, routeConfig.Order); + Assert.Equal("route1", routeEndpoint.DisplayName); + Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); + Assert.Equal(12, routeEndpoint.Order); + Assert.False(routeConfig.HasConfigChanged(route, cluster)); + + var hostMetadata = routeEndpoint.Metadata.GetMetadata(); + Assert.Null(hostMetadata); + } + + [Fact] + public void AddEndpoint_InvalidPath_BubblesOutException() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Match = + { + Path = "/{invalid", + }, + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + Action action = () => CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Throws(action); + } + + [Fact] + public void AddEndpoint_DefaultAuth_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + AuthorizationPolicy = "defaulT", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(attribute.Policy); + } + + [Fact] + public void AddEndpoint_CustomAuth_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + AuthorizationPolicy = "custom", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("custom", attribute.Policy); + } + + [Fact] + public void AddEndpoint_NoAuth_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_DefaultCors_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + CorsPolicy = "defaulT", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(attribute.PolicyName); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_CustomCors_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + CorsPolicy = "custom", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); + Assert.Equal("custom", attribute.PolicyName); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_DisableCors_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + CorsPolicy = "disAble", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.IsType(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_NoCors_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new ProxyRoute + { + RouteId = "route1", + Order = 12, + }; + var cluster = new ClusterInfo("cluster1", new DestinationManager()); + var routeInfo = new RouteInfo("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeInfo, route, cluster); + + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + } +} diff --git a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs index 3c4aee2a73..907637a947 100644 --- a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs +++ b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs @@ -31,7 +31,7 @@ private IServiceProvider CreateServices(List routes, List c serviceCollection.TryAddSingleton(new Mock().Object); configureProxy?.Invoke(proxyBuilder); var services = serviceCollection.BuildServiceProvider(); - var routeBuilder = services.GetRequiredService(); + var routeBuilder = services.GetRequiredService(); routeBuilder.SetProxyPipeline(context => Task.CompletedTask); return services; }