public async Task ApplyConfigurationsAsync_OneClusterOneDestinationOneRoute_Works()
        {
            // Arrange
            const string TestAddress = "https://localhost:123/";

            var cluster = new Cluster
            {
                Destinations = {
                    { "d1", new Destination { Address = TestAddress } }
                }
            };
            var route = new ParsedRoute
            {
                RouteId = "route1",
                ClusterId = "cluster1",
            };

            var dynamicConfigRoot = new DynamicConfigRoot
            {
                Clusters = new Dictionary<string, Cluster> { { "cluster1", cluster }  },
                Routes = new[] { route },
            };
            Mock<IDynamicConfigBuilder>()
                .Setup(d => d.BuildConfigAsync(It.IsAny<CancellationToken>()))
                .ReturnsAsync(dynamicConfigRoot);

            var proxyManager = Create<ReverseProxyConfigManager>();

            // Act
            await proxyManager.ApplyConfigurationsAsync(CancellationToken.None);

            // Assert

            var actualClusters = _clusterManager.GetItems();
            Assert.Single(actualClusters);
            Assert.Equal("cluster1", actualClusters[0].ClusterId);
            Assert.NotNull(actualClusters[0].DestinationManager);
            Assert.NotNull(actualClusters[0].Config.Value);

            var actualDestinations = actualClusters[0].DestinationManager.GetItems();
            Assert.Single(actualDestinations);
            Assert.Equal("d1", actualDestinations[0].DestinationId);
            Assert.NotNull(actualDestinations[0].Config);
            Assert.Equal(TestAddress, actualDestinations[0].Config.Address);

            var actualRoutes = _routeManager.GetItems();
            Assert.Single(actualRoutes);
            Assert.Equal("route1", actualRoutes[0].RouteId);
            Assert.NotNull(actualRoutes[0].Config.Value);
            Assert.Same(actualClusters[0], actualRoutes[0].Config.Value.Cluster);
        }
Beispiel #2
0
        public async Task <DynamicConfigRoot> BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation)
        {
            var clusters = await GetClustersAsync(errorReporter, cancellation);

            var routes = await GetRoutesAsync(errorReporter, cancellation);

            var config = new DynamicConfigRoot
            {
                Clusters = clusters,
                Routes   = routes,
            };

            return(config);
        }
        public async Task <Result <DynamicConfigRoot> > BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation)
        {
            Contracts.CheckValue(errorReporter, nameof(errorReporter));

            var backends = await GetBackendsAsync(errorReporter, cancellation);

            var routes = await GetRoutesAsync(errorReporter, cancellation);

            var config = new DynamicConfigRoot
            {
                Backends = backends,
                Routes   = routes,
            };

            return(Result.Success(config));
        }
        public async Task <Result <DynamicConfigRoot> > BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation)
        {
            Contracts.CheckValue(errorReporter, nameof(errorReporter));

            var backends = await _backendsRepo.GetBackendsAsync(cancellation) ?? new Dictionary <string, Backend>(StringComparer.Ordinal);

            var routes = await GetRoutesAsync(errorReporter, cancellation);

            var config = new DynamicConfigRoot
            {
                Backends = backends,
                Routes   = routes,
            };

            return(Result.Success(config));
        }
        public async Task ApplyConfigurationsAsync_OneBackendOneEndpointOneRoute_Works()
        {
            // Arrange
            const string TestAddress = "https://localhost:123/";

            var backend = new Backend
            {
                Endpoints =
                {
                    { "ep1", new BackendEndpoint {
                          Address = TestAddress
                      } }
                }
            };
            var route = new ParsedRoute
            {
                RouteId   = "route1",
                BackendId = "backend1",
            };

            var dynamicConfigRoot = new DynamicConfigRoot
            {
                Backends = new Dictionary <string, Backend> {
                    { "backend1", backend }
                },
                Routes = new[] { route },
            };

            Mock <IDynamicConfigBuilder>()
            .Setup(d => d.BuildConfigAsync(It.IsAny <IConfigErrorReporter>(), It.IsAny <CancellationToken>()))
            .ReturnsAsync(Result.Success(dynamicConfigRoot));

            var errorReporter = new TestConfigErrorReporter();
            var proxyManager  = Create <ReverseProxyConfigManager>();

            // Act
            var result = await proxyManager.ApplyConfigurationsAsync(errorReporter, CancellationToken.None);

            // Assert
            Assert.True(result);

            var actualBackends = _backendManager.GetItems();

            Assert.Single(actualBackends);
            Assert.Equal("backend1", actualBackends[0].BackendId);
            Assert.NotNull(actualBackends[0].EndpointManager);
            Assert.NotNull(actualBackends[0].Config.Value);

            var actualEndpoints = actualBackends[0].EndpointManager.GetItems();

            Assert.Single(actualEndpoints);
            Assert.Equal("ep1", actualEndpoints[0].EndpointId);
            Assert.NotNull(actualEndpoints[0].Config.Value);
            Assert.Equal(TestAddress, actualEndpoints[0].Config.Value.Address);

            var actualRoutes = _routeManager.GetItems();

            Assert.Single(actualRoutes);
            Assert.Equal("route1", actualRoutes[0].RouteId);
            Assert.NotNull(actualRoutes[0].Config.Value);
            Assert.Same(actualBackends[0], actualRoutes[0].Config.Value.BackendOrNull);
        }
        private void UpdateRuntimeBackends(DynamicConfigRoot config)
        {
            var desiredBackends = new HashSet <string>(StringComparer.Ordinal);

            foreach (var configBackendPair in config.Backends)
            {
                var configBackend = configBackendPair.Value;
                desiredBackends.Add(configBackendPair.Key);

                _backendManager.GetOrCreateItem(
                    itemId: configBackendPair.Key,
                    setupAction: backend =>
                {
                    UpdateRuntimeDestinations(configBackend.Destinations, backend.DestinationManager);

                    var newConfig = new BackendConfig(
                        new BackendConfig.BackendHealthCheckOptions(
                            enabled: configBackend.HealthCheckOptions?.Enabled ?? false,
                            interval: configBackend.HealthCheckOptions?.Interval ?? TimeSpan.FromSeconds(0),
                            timeout: configBackend.HealthCheckOptions?.Timeout ?? TimeSpan.FromSeconds(0),
                            port: configBackend.HealthCheckOptions?.Port ?? 0,
                            path: configBackend.HealthCheckOptions?.Path ?? string.Empty),
                        new BackendConfig.BackendLoadBalancingOptions(
                            mode: configBackend.LoadBalancing?.Mode ?? default));

                    var currentBackendConfig = backend.Config.Value;
                    if (currentBackendConfig == null ||
                        currentBackendConfig.HealthCheckOptions.Enabled != newConfig.HealthCheckOptions.Enabled ||
                        currentBackendConfig.HealthCheckOptions.Interval != newConfig.HealthCheckOptions.Interval ||
                        currentBackendConfig.HealthCheckOptions.Timeout != newConfig.HealthCheckOptions.Timeout ||
                        currentBackendConfig.HealthCheckOptions.Port != newConfig.HealthCheckOptions.Port ||
                        currentBackendConfig.HealthCheckOptions.Path != newConfig.HealthCheckOptions.Path)
                    {
                        if (currentBackendConfig == null)
                        {
                            Log.BackendAdded(_logger, configBackendPair.Key);
                        }
                        else
                        {
                            Log.BackendChanged(_logger, configBackendPair.Key);
                        }

                        // Config changed, so update runtime backend
                        backend.Config.Value = newConfig;
                    }
                });
            }

            foreach (var existingBackend in _backendManager.GetItems())
            {
                if (!desiredBackends.Contains(existingBackend.BackendId))
                {
                    // NOTE 1: This is safe to do within the `foreach` loop
                    // because `IBackendManager.GetItems` returns a copy of the list of backends.
                    //
                    // NOTE 2: Removing the backend from `IBackendManager` is safe and existing
                    // ASP .NET Core endpoints will continue to work with their existing behavior (until those endpoints are updated)
                    // and the Garbage Collector won't destroy this backend object while it's referenced elsewhere.
                    Log.BackendRemoved(_logger, existingBackend.BackendId);
                    _backendManager.TryRemoveItem(existingBackend.BackendId);
                }
            }
        }
        private void UpdateRuntimeRoutes(DynamicConfigRoot config)
        {
            var desiredRoutes = new HashSet <string>(StringComparer.Ordinal);
            var changed       = false;

            foreach (var configRoute in config.Routes)
            {
                desiredRoutes.Add(configRoute.RouteId);

                // Note that this can be null, and that is fine. The resulting route may match
                // but would then fail to route, which is exactly what we were instructed to do in this case
                // since no valid backend was specified.
                var backendOrNull = _backendManager.TryGetItem(configRoute.BackendId);

                _routeManager.GetOrCreateItem(
                    itemId: configRoute.RouteId,
                    setupAction: route =>
                {
                    var currentRouteConfig = route.Config.Value;
                    if (currentRouteConfig == null ||
                        currentRouteConfig.HasConfigChanged(configRoute, backendOrNull))
                    {
                        // Config changed, so update runtime route
                        changed = true;
                        if (currentRouteConfig == null)
                        {
                            Log.RouteAdded(_logger, configRoute.RouteId);
                        }
                        else
                        {
                            Log.RouteChanged(_logger, configRoute.RouteId);
                        }

                        var newConfig      = _routeEndpointBuilder.Build(configRoute, backendOrNull, route);
                        route.Config.Value = newConfig;
                    }
                });
            }

            foreach (var existingRoute in _routeManager.GetItems())
            {
                if (!desiredRoutes.Contains(existingRoute.RouteId))
                {
                    // NOTE 1: This is safe to do within the `foreach` loop
                    // because `IRouteManager.GetItems` returns a copy of the list of routes.
                    //
                    // NOTE 2: Removing the route from `IRouteManager` is safe and existing
                    // ASP .NET Core endpoints will continue to work with their existing behavior since
                    // their copy of `RouteConfig` is immutable and remains operational in whichever state is was in.
                    Log.RouteRemoved(_logger, existingRoute.RouteId);
                    _routeManager.TryRemoveItem(existingRoute.RouteId);
                    changed = true;
                }
            }

            if (changed)
            {
                var endpoints = new List <Endpoint>();
                foreach (var existingRoute in _routeManager.GetItems())
                {
                    var runtimeConfig = existingRoute.Config.Value;
                    if (runtimeConfig?.Endpoints != null)
                    {
                        endpoints.AddRange(runtimeConfig.Endpoints);
                    }
                }

                // This is where the new routes take effect!
                _dynamicEndpointDataSource.Update(endpoints);
            }
        }
        private void UpdateRuntimeClusters(DynamicConfigRoot config)
        {
            var desiredClusters = new HashSet <string>(StringComparer.Ordinal);

            foreach (var configClusterPair in config.Clusters)
            {
                var configCluster = configClusterPair.Value;
                desiredClusters.Add(configClusterPair.Key);

                _clusterManager.GetOrCreateItem(
                    itemId: configClusterPair.Key,
                    setupAction: cluster =>
                {
                    UpdateRuntimeDestinations(configCluster.Destinations, cluster.DestinationManager);

                    var newConfig = new ClusterConfig(
                        new ClusterConfig.ClusterHealthCheckOptions(
                            enabled: configCluster.HealthCheckOptions?.Enabled ?? false,
                            interval: configCluster.HealthCheckOptions?.Interval ?? TimeSpan.FromSeconds(0),
                            timeout: configCluster.HealthCheckOptions?.Timeout ?? TimeSpan.FromSeconds(0),
                            port: configCluster.HealthCheckOptions?.Port ?? 0,
                            path: configCluster.HealthCheckOptions?.Path ?? string.Empty),
                        new ClusterConfig.ClusterLoadBalancingOptions(
                            mode: configCluster.LoadBalancing?.Mode ?? default),
                        new ClusterConfig.ClusterSessionAffinityOptions(
                            enabled: configCluster.SessionAffinity?.Enabled ?? false,
                            mode: configCluster.SessionAffinity?.Mode,
                            failurePolicy: configCluster.SessionAffinity?.FailurePolicy,
                            settings: configCluster.SessionAffinity?.Settings as IReadOnlyDictionary <string, string>));

                    var currentClusterConfig = cluster.Config.Value;
                    if (currentClusterConfig == null ||
                        currentClusterConfig.HealthCheckOptions.Enabled != newConfig.HealthCheckOptions.Enabled ||
                        currentClusterConfig.HealthCheckOptions.Interval != newConfig.HealthCheckOptions.Interval ||
                        currentClusterConfig.HealthCheckOptions.Timeout != newConfig.HealthCheckOptions.Timeout ||
                        currentClusterConfig.HealthCheckOptions.Port != newConfig.HealthCheckOptions.Port ||
                        currentClusterConfig.HealthCheckOptions.Path != newConfig.HealthCheckOptions.Path)
                    {
                        if (currentClusterConfig == null)
                        {
                            Log.ClusterAdded(_logger, configClusterPair.Key);
                        }
                        else
                        {
                            Log.ClusterChanged(_logger, configClusterPair.Key);
                        }

                        // Config changed, so update runtime cluster
                        cluster.Config.Value = newConfig;
                    }
                });
            }

            foreach (var existingCluster in _clusterManager.GetItems())
            {
                if (!desiredClusters.Contains(existingCluster.ClusterId))
                {
                    // NOTE 1: This is safe to do within the `foreach` loop
                    // because `IClusterManager.GetItems` returns a copy of the list of clusters.
                    //
                    // NOTE 2: Removing the cluster from `IClusterManager` is safe and existing
                    // ASP .NET Core endpoints will continue to work with their existing behavior (until those endpoints are updated)
                    // and the Garbage Collector won't destroy this cluster object while it's referenced elsewhere.
                    Log.ClusterRemoved(_logger, existingCluster.ClusterId);
                    _clusterManager.TryRemoveItem(existingCluster.ClusterId);
                }
            }
        }