diff --git a/.editorconfig b/.editorconfig index 04989a5b4..25215ffca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -231,7 +231,7 @@ csharp_using_directive_placement = outside_namespace:error # New-line options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#new-line-options -csharp_new_line_before_open_brace = methods, properties, control_blocks, types, anonymous_methods, lambdas, object_collection_array_initializers +csharp_new_line_before_open_brace = methods, properties, control_blocks, types, anonymous_methods, lambdas, object_collection_array_initializers, accessors csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true diff --git a/samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs b/samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs index b9ce57a4c..564010c1d 100644 --- a/samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs +++ b/samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs @@ -52,7 +52,7 @@ public ValueTask ConfigureClusterAsync(ClusterConfig origCluster, return new ValueTask(origCluster with { Destinations = newDests }); } - public ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel) + public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { // Example: do not let config based routes take priority over code based routes. // Lower numbers are higher priority. Code routes default to 0. diff --git a/src/ReverseProxy/Configuration/IProxyConfigFilter.cs b/src/ReverseProxy/Configuration/IProxyConfigFilter.cs index 8a7a999e6..d3eb4f374 100644 --- a/src/ReverseProxy/Configuration/IProxyConfigFilter.cs +++ b/src/ReverseProxy/Configuration/IProxyConfigFilter.cs @@ -21,6 +21,7 @@ public interface IProxyConfigFilter /// Allows modification of a route configuration. /// /// The instance to configure. - ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel); + /// The instance related to . + ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig? cluster, CancellationToken cancel); } } diff --git a/src/ReverseProxy/Management/ProxyConfigManager.cs b/src/ReverseProxy/Management/ProxyConfigManager.cs index 44d59cad2..f9651fc1a 100644 --- a/src/ReverseProxy/Management/ProxyConfigManager.cs +++ b/src/ReverseProxy/Management/ProxyConfigManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -15,9 +16,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.Model; -using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Routing; using Yarp.ReverseProxy.Transforms.Builder; @@ -34,6 +35,8 @@ namespace Yarp.ReverseProxy.Management /// internal sealed class ProxyConfigManager : EndpointDataSource, IDisposable { + private static readonly IReadOnlyDictionary _emptyClusterDictionary = new ReadOnlyDictionary(new Dictionary()); + private readonly object _syncRoot = new object(); private readonly ILogger _logger; private readonly IProxyConfigProvider _provider; @@ -209,8 +212,8 @@ private async Task ReloadConfigAsync() // Throws for validation failures private async Task ApplyConfigAsync(IProxyConfig config) { - var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(config.Routes, cancellation: default); var (configuredClusters, clusterErrors) = await VerifyClustersAsync(config.Clusters, cancellation: default); + var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(config.Routes, configuredClusters, cancellation: default); if (routeErrors.Count > 0 || clusterErrors.Count > 0) { @@ -218,12 +221,12 @@ private async Task ApplyConfigAsync(IProxyConfig config) } // Update clusters first because routes need to reference them. - UpdateRuntimeClusters(configuredClusters); + UpdateRuntimeClusters(configuredClusters.Values); var routesChanged = UpdateRuntimeRoutes(configuredRoutes); return routesChanged; } - private async Task<(IList, IList)> VerifyRoutesAsync(IReadOnlyList routes, CancellationToken cancellation) + private async Task<(IList, IList)> VerifyRoutesAsync(IReadOnlyList routes, IReadOnlyDictionary clusters, CancellationToken cancellation) { if (routes == null) { @@ -246,9 +249,18 @@ private async Task ApplyConfigAsync(IProxyConfig config) try { - foreach (var filter in _filters) + if (_filters.Length != 0) { - route = await filter.ConfigureRouteAsync(route, cancellation); + ClusterConfig? cluster = null; + if (route.ClusterId != null) + { + clusters.TryGetValue(route.ClusterId, out cluster); + } + + foreach (var filter in _filters) + { + route = await filter.ConfigureRouteAsync(route, cluster, cancellation); + } } } catch (Exception ex) @@ -275,29 +287,26 @@ private async Task ApplyConfigAsync(IProxyConfig config) return (configuredRoutes, errors); } - private async Task<(IList, IList)> VerifyClustersAsync(IReadOnlyList clusters, CancellationToken cancellation) + private async Task<(IReadOnlyDictionary, IList)> VerifyClustersAsync(IReadOnlyList clusters, CancellationToken cancellation) { if (clusters == null) { - return (Array.Empty(), Array.Empty()); + return (_emptyClusterDictionary, Array.Empty()); } - var seenClusterIds = new HashSet(clusters.Count, StringComparer.OrdinalIgnoreCase); - var configuredClusters = new List(clusters.Count); + var configuredClusters = new Dictionary(clusters.Count, StringComparer.OrdinalIgnoreCase); var errors = new List(); // The IProxyConfigProvider provides a fresh snapshot that we need to reconfigure each time. foreach (var c in clusters) { try { - if (seenClusterIds.Contains(c.ClusterId)) + if (configuredClusters.ContainsKey(c.ClusterId)) { errors.Add(new ArgumentException($"Duplicate cluster '{c.ClusterId}'.")); continue; } - seenClusterIds.Add(c.ClusterId); - // Don't modify the original var cluster = c; @@ -313,7 +322,7 @@ private async Task ApplyConfigAsync(IProxyConfig config) continue; } - configuredClusters.Add(cluster); + configuredClusters.Add(cluster.ClusterId, cluster); } catch (Exception ex) { @@ -323,13 +332,13 @@ private async Task ApplyConfigAsync(IProxyConfig config) if (errors.Count > 0) { - return (Array.Empty(), errors); + return (_emptyClusterDictionary, errors); } return (configuredClusters, errors); } - private void UpdateRuntimeClusters(IList incomingClusters) + private void UpdateRuntimeClusters(IEnumerable incomingClusters) { var desiredClusters = new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs b/test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs index 3d209f5c1..7d3dd8fb3 100644 --- a/test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs +++ b/test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs @@ -365,7 +365,7 @@ public ValueTask ConfigureClusterAsync(ClusterConfig cluster, Can return new ValueTask(cluster); } - public ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel) + public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { return new ValueTask(route with { @@ -387,30 +387,50 @@ public ValueTask ConfigureClusterAsync(ClusterConfig cluster, Can }); } - public ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel) + public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { - return new ValueTask(route with { Order = 12 }); + string order; + if (cluster != null) + { + order = cluster.Metadata["Order"]; + } + else + { + order = "12"; + } + + return new ValueTask(route with { Order = int.Parse(order) }); } } [Fact] public async Task LoadAsync_ConfigFilterConfiguresCluster_Works() { - var route = new RouteConfig + var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; + var route2 = new RouteConfig + { + RouteId = "route2", + ClusterId = "cluster2", + Match = new RouteMatch { Path = "/" } + }; var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } + }, + Metadata = new Dictionary + { + ["Order"] = "47" } }; - var services = CreateServices(new List() { route }, new List() { cluster }, proxyBuilder => + var services = CreateServices(new List() { route1, route2 }, new List() { cluster }, proxyBuilder => { proxyBuilder.AddConfigFilter(); }); @@ -418,14 +438,21 @@ public async Task LoadAsync_ConfigFilterConfiguresCluster_Works() var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); - var endpoint = Assert.Single(dataSource.Endpoints); - var routeConfig = endpoint.Metadata.GetMetadata(); - var clusterState = routeConfig.Cluster; - Assert.NotNull(clusterState); - Assert.True(clusterState.Model.Config.HealthCheck.Active.Enabled); - Assert.Equal(TimeSpan.FromSeconds(12), clusterState.Model.Config.HealthCheck.Active.Interval); - var destination = Assert.Single(clusterState.DestinationsState.AllDestinations); + Assert.Equal(2, dataSource.Endpoints.Count); + + var endpoint1 = Assert.Single(dataSource.Endpoints.Where(x => x.DisplayName == "route1")); + var routeConfig1 = endpoint1.Metadata.GetMetadata(); + Assert.Equal(47, routeConfig1.Config.Order); + var clusterState1 = routeConfig1.Cluster; + Assert.NotNull(clusterState1); + Assert.True(clusterState1.Model.Config.HealthCheck.Active.Enabled); + Assert.Equal(TimeSpan.FromSeconds(12), clusterState1.Model.Config.HealthCheck.Active.Interval); + var destination = Assert.Single(clusterState1.DestinationsState.AllDestinations); Assert.Equal("http://localhost", destination.Model.Config.Address); + + var endpoint2 = Assert.Single(dataSource.Endpoints.Where(x => x.DisplayName == "route2")); + var routeConfig2 = endpoint2.Metadata.GetMetadata(); + Assert.Equal(12, routeConfig2.Config.Order); } private class ClusterAndRouteThrows : IProxyConfigFilter @@ -435,7 +462,7 @@ public ValueTask ConfigureClusterAsync(ClusterConfig cluster, Can throw new NotFiniteNumberException("Test exception"); } - public ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel) + public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { throw new NotFiniteNumberException("Test exception"); } diff --git a/testassets/ReverseProxy.Config/CustomConfigFilter.cs b/testassets/ReverseProxy.Config/CustomConfigFilter.cs index 15a02ab7a..f2ddfbae3 100644 --- a/testassets/ReverseProxy.Config/CustomConfigFilter.cs +++ b/testassets/ReverseProxy.Config/CustomConfigFilter.cs @@ -53,7 +53,7 @@ public ValueTask ConfigureClusterAsync(ClusterConfig cluster, Can return new ValueTask(cluster); } - public ValueTask ConfigureRouteAsync(RouteConfig route, CancellationToken cancel) + public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { // Do not let config based routes take priority over code based routes. // Lower numbers are higher priority. Code routes default to 0.