public void BuildRoutes_InvalidRouteName_Throws(string invalidKey, string value)
        {
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Priority", "2" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
            };

            labels[invalidKey] = value;

            Func <List <ProxyRoute> > func = () => LabelsParser.BuildRoutes(_testServiceName, labels);

            func.Should()
            .Throw <ConfigException>()
            .WithMessage($"Invalid route name '*', should only contain alphanumerical characters, underscores or hyphens.");
        }
        public void BuildRoutes_InvalidTransformIndex_Throws(string invalidKey, string value)
        {
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Priority", "2" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
            };

            labels[invalidKey] = value;

            Func <List <ProxyRoute> > func = () => LabelsParser.BuildRoutes(_testServiceName, labels);

            func.Should()
            .Throw <ConfigException>()
            .WithMessage($"Invalid transform index '*', should be transform index wrapped in square brackets.");
        }
Exemple #3
0
        public void BuildRoutes_InvalidOrder_Throws()
        {
            // Arrange
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Order", "this is no number" },
            };

            // Act
            Func <List <ProxyRoute> > func = () => LabelsParser.BuildRoutes(_testServiceName, labels);

            // Assert
            func.Should()
            .Throw <ConfigException>()
            .WithMessage("Could not convert label YARP.Routes.MyRoute.Order='this is no number' *");
        }
        public void BuildRoutes_MatchHeadersWithCSVs_Works(string invalidKey, string value, string[] expected)
        {
            // Arrange
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute0.Hosts", "example0.com" },
                { "YARP.Routes.MyRoute0.Metadata.Foo", "bar" },
                { "YARP.Routes.MyRoute0.MatchHeaders.[0].Name", "x-test-header" },
                { "YARP.Routes.MyRoute0.MatchHeaders.[0].Mode", "ExactHeader" },
            };

            labels[invalidKey] = value;

            // Act
            var routes = LabelsParser.BuildRoutes(_testServiceName, labels);

            // Assert
            var expectedRoutes = new List <ProxyRoute>
            {
                new ProxyRoute
                {
                    RouteId = $"MyCoolClusterId:MyRoute0",
                    Match   =
                    {
                        Hosts   = new[] { "example0.com" },
                        Headers = new List <RouteHeader>()
                        {
                            new RouteHeader()
                            {
                                Name = "x-test-header", Mode = HeaderMatchMode.ExactHeader, Values = expected
                            },
                        }
                    },
                    Metadata = new Dictionary <string, string>()
                    {
                        { "Foo", "bar" }
                    },
                    ClusterId = "MyCoolClusterId",
                }
            };

            routes.Should().BeEquivalentTo(expectedRoutes);
        }
        public void BuildRoutes_InvalidHeaderMatchProperty_Throws(string invalidKey, string value)
        {
            // Arrange
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Priority", "2" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
            };

            labels[invalidKey] = value;

            // Act
            Func <List <RouteConfig> > func = () => LabelsParser.BuildRoutes(_testServiceName, labels);

            // Assert
            func.Should()
            .Throw <ConfigException>()
            .WithMessage($"Invalid header matching property '*', only valid values are Name, Values, IsCaseSensitive and Mode.");
        }
        public async void ExecuteAsync_SingleServiceWithGatewayEnabled_OneClusterFound()
        {
            // Setup
            _scenarioOptions = new ServiceFabricDiscoveryOptions {
                ReportReplicasHealth = true
            };
            const string       TestClusterId = "MyService123";
            var                labels = SFTestHelpers.DummyLabels(TestClusterId);
            ApplicationWrapper application, anotherApplication;

            Mock_AppsResponse(
                application        = CreateApp_1StatelessService_2Partition_2ReplicasEach("MyApp", "MYService", out var service, out var replicas),
                anotherApplication = CreateApp_1StatelessService_2Partition_2ReplicasEach("AnotherApp", "AnotherService", out var anotherService, out var otherReplicas));
            Mock_ServiceLabels(application, service, labels);
            Mock_ServiceLabels(anotherApplication, anotherService, new Dictionary <string, string>());

            // Act
            var(routes, clusters) = await RunScenarioAsync();

            // Assert
            var expectedClusters = new[]
            {
                ClusterWithDestinations(
                    LabelsParser.BuildCluster(_testServiceName, labels),
                    SFTestHelpers.BuildDestinationFromReplica(replicas[0]),
                    SFTestHelpers.BuildDestinationFromReplica(replicas[1]),
                    SFTestHelpers.BuildDestinationFromReplica(replicas[2]),
                    SFTestHelpers.BuildDestinationFromReplica(replicas[3])),
            };
            var expectedRoutes = LabelsParser.BuildRoutes(_testServiceName, labels);

            routes.Should().BeEquivalentTo(expectedRoutes);
            clusters.Should().BeEquivalentTo(expectedClusters);
            AssertServiceHealthReported(service, HealthState.Ok);
            foreach (var replica in replicas)
            {
                AssertStatelessServiceInstanceHealthReported(replica, HealthState.Ok);
            }
            _healthReports.Should().HaveCount(5);
        }
        public async void ExecuteAsync_OneServiceWithGatewayEnabledAndOneNotEnabled_OnlyTheOneEnabledFound()
        {
            // Setup
            _scenarioOptions = new ServiceFabricDiscoveryOptions {
                ReportReplicasHealth = true
            };
            const string       TestClusterIdApp1Sv1 = "MyService123";
            const string       TestClusterIdApp2Sv2 = "MyService234";
            var                gatewayEnabledLabels = SFTestHelpers.DummyLabels(TestClusterIdApp1Sv1);
            var                gatewayNotEnabledLabels = SFTestHelpers.DummyLabels(TestClusterIdApp2Sv2, false);
            ApplicationWrapper application1, application2;

            Mock_AppsResponse(
                application1 = CreateApp_1Service_SingletonPartition_1Replica("MyApp", "MyService1", out var service1, out var replica1),
                application2 = CreateApp_1Service_SingletonPartition_1Replica("MyApp2", "MyService2", out var service2, out var replica2));

            Mock_ServiceLabels(application1, service1, gatewayEnabledLabels);
            Mock_ServiceLabels(application2, service2, gatewayNotEnabledLabels);

            // Act
            var(routes, clusters) = await RunScenarioAsync();

            // Assert
            var expectedClusters = new[]
            {
                ClusterWithDestinations(
                    LabelsParser.BuildCluster(_testServiceName, gatewayEnabledLabels),
                    SFTestHelpers.BuildDestinationFromReplica(replica1)),
            };
            var expectedRoutes = new List <ProxyRoute>();

            expectedRoutes.AddRange(LabelsParser.BuildRoutes(_testServiceName, gatewayEnabledLabels));

            clusters.Should().BeEquivalentTo(expectedClusters);
            routes.Should().BeEquivalentTo(expectedRoutes);
            AssertServiceHealthReported(service1, HealthState.Ok);
            AssertStatelessServiceInstanceHealthReported(replica1, HealthState.Ok);
            _healthReports.Should().HaveCount(2);
        }
        public async void ExecuteAsync_NotHttpsSchemeForStatelessService_NoEndpointsAndBadHealthReported()
        {
            // Setup
            _scenarioOptions = new ServiceFabricDiscoveryOptions {
                ReportReplicasHealth = true
            };
            const string TestClusterId = "MyService123";
            const string ServiceName   = "fabric:/MyApp/MyService";
            var          labels        = SFTestHelpers.DummyLabels(TestClusterId);

            labels["YARP.Backend.ServiceFabric.ListenerName"] = "ExampleTeamEndpoint";
            ApplicationWrapper application;

            Mock_AppsResponse(
                application = CreateApp_1Service_SingletonPartition_1Replica("MyApp", "MyService", out var service, out var replica, serviceKind: ServiceKind.Stateless));
            var nonHttpAddress = $"http://127.0.0.1/{ServiceName}/0";

            replica.ReplicaAddress = $"{{'Endpoints': {{'ExampleTeamEndpoint': '{nonHttpAddress}' }} }}".Replace("'", "\"");
            Mock_ServiceLabels(application, service, labels);

            // Act
            var(routes, clusters) = await RunScenarioAsync();

            // Assert
            var expectedClusters = new[]
            {
                LabelsParser.BuildCluster(_testServiceName, labels),
            };
            var expectedRoutes = LabelsParser.BuildRoutes(_testServiceName, labels);

            clusters.Should().BeEquivalentTo(expectedClusters);
            routes.Should().BeEquivalentTo(expectedRoutes);
            AssertServiceHealthReported(service, HealthState.Ok);
            AssertStatelessServiceInstanceHealthReported(replica, HealthState.Warning, (description) =>
                                                         description.StartsWith("Could not build service endpoint") &&
                                                         description.Contains("ExampleTeamEndpoint"));
            _healthReports.Should().HaveCount(2);
        }
        public async void ExecuteAsync_ValidListenerNameForStatelessService_Work()
        {
            // Setup
            _scenarioOptions = new ServiceFabricDiscoveryOptions {
                ReportReplicasHealth = true
            };
            const string TestClusterId = "MyService123";
            var          labels        = SFTestHelpers.DummyLabels(TestClusterId);

            labels["YARP.Backend.ServiceFabric.ListenerName"] = "ExampleTeamEndpoint";
            labels["YARP.Backend.Healthcheck.Active.ServiceFabric.ListenerName"] = "ExampleTeamHealthEndpoint";
            ApplicationWrapper application;

            Mock_AppsResponse(
                application        = CreateApp_1Service_SingletonPartition_1Replica("MyApp", "MyService", out var service, out var replica, serviceKind: ServiceKind.Stateless));
            replica.ReplicaAddress = MockReplicaAdressWithListenerName("MyApp", "MyService", new string[] { "ExampleTeamEndpoint", "ExampleTeamHealthEndpoint" });
            Mock_ServiceLabels(application, service, labels);

            // Act
            var(routes, clusters) = await RunScenarioAsync();

            // Assert
            var expectedClusters = new[]
            {
                ClusterWithDestinations(
                    LabelsParser.BuildCluster(_testServiceName, labels),
                    SFTestHelpers.BuildDestinationFromReplica(replica, "ExampleTeamHealthEndpoint")),
            };
            var expectedRoutes = LabelsParser.BuildRoutes(_testServiceName, labels);

            clusters.Should().BeEquivalentTo(expectedClusters);
            routes.Should().BeEquivalentTo(expectedRoutes);
            AssertServiceHealthReported(service, HealthState.Ok);
            AssertStatelessServiceInstanceHealthReported(replica, HealthState.Ok, (description) =>
                                                         description.StartsWith("Successfully built"));
            _healthReports.Should().HaveCount(2);
        }
        public void BuildRoutes_InvalidLabelKeys_IgnoresAndDoesNotThrow(string invalidKey, string value)
        {
            // Arrange
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Order", "2" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
            };

            labels[invalidKey] = value;

            // Act
            var routes = LabelsParser.BuildRoutes(_testServiceName, labels);

            // Assert
            var expectedRoutes = new List <ProxyRoute>
            {
                new ProxyRoute
                {
                    RouteId = "MyCoolClusterId:MyRoute",
                    Match   =
                    {
                        Hosts = new[] { "example.com" },
                    },
                    Order     = 2,
                    ClusterId = "MyCoolClusterId",
                    Metadata  = new Dictionary <string, string>
                    {
                        { "Foo", "Bar" },
                    },
                },
            };

            routes.Should().BeEquivalentTo(expectedRoutes);
        }
        public void BuildRoutes_SingleRoute_Works()
        {
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Order", "2" },
                { "YARP.Routes.MyRoute.MatchHeaders.[0].Mode", "ExactHeader" },
                { "YARP.Routes.MyRoute.MatchHeaders.[0].Name", "x-company-key" },
                { "YARP.Routes.MyRoute.MatchHeaders.[0].Values", "contoso" },
                { "YARP.Routes.MyRoute.MatchHeaders.[0].IsCaseSensitive", "true" },
                { "YARP.Routes.MyRoute.MatchHeaders.[1].Mode", "ExactHeader" },
                { "YARP.Routes.MyRoute.MatchHeaders.[1].Name", "x-environment" },
                { "YARP.Routes.MyRoute.MatchHeaders.[1].Values", "dev, uat" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
                { "YARP.Routes.MyRoute.Transforms.[0].ResponseHeader", "X-Foo" },
                { "YARP.Routes.MyRoute.Transforms.[0].Append", "Bar" },
                { "YARP.Routes.MyRoute.Transforms.[0].When", "Always" },
                { "YARP.Routes.MyRoute.Transforms.[1].ResponseHeader", "X-Ping" },
                { "YARP.Routes.MyRoute.Transforms.[1].Append", "Pong" },
                { "YARP.Routes.MyRoute.Transforms.[1].When", "Success" },
            };

            var routes = LabelsParser.BuildRoutes(_testServiceName, labels);

            var expectedRoutes = new List <RouteConfig>
            {
                new RouteConfig
                {
                    RouteId = "MyCoolClusterId:MyRoute",
                    Match   = new RouteMatch
                    {
                        Hosts   = new[] { "example.com" },
                        Headers = new List <RouteHeader>
                        {
                            new RouteHeader()
                            {
                                Mode            = HeaderMatchMode.ExactHeader,
                                Name            = "x-company-key",
                                Values          = new string[] { "contoso" },
                                IsCaseSensitive = true
                            },
                            new RouteHeader()
                            {
                                Mode            = HeaderMatchMode.ExactHeader,
                                Name            = "x-environment",
                                Values          = new string[] { "dev", "uat" },
                                IsCaseSensitive = false
                            }
                        }
                    },
                    Order     = 2,
                    ClusterId = "MyCoolClusterId",
                    Metadata  = new Dictionary <string, string>
                    {
                        { "Foo", "Bar" },
                    },
                    Transforms = new List <IReadOnlyDictionary <string, string> >
                    {
                        new Dictionary <string, string>
                        {
                            { "ResponseHeader", "X-Foo" },
                            { "Append", "Bar" },
                            { "When", "Always" }
                        },
                        new Dictionary <string, string>
                        {
                            { "ResponseHeader", "X-Ping" },
                            { "Append", "Pong" },
                            { "When", "Success" }
                        }
                    }
                },
            };

            routes.Should().BeEquivalentTo(expectedRoutes);
        }
        public void BuildRoutes_MultipleRoutes_Works()
        {
            // Arrange
            var labels = new Dictionary <string, string>()
            {
                { "YARP.Backend.BackendId", "MyCoolClusterId" },
                { "YARP.Routes.MyRoute.Hosts", "example.com" },
                { "YARP.Routes.MyRoute.Path", "v2/{**rest}" },
                { "YARP.Routes.MyRoute.Order", "1" },
                { "YARP.Routes.MyRoute.Metadata.Foo", "Bar" },
                { "YARP.Routes.CoolRoute.Hosts", "example.net" },
                { "YARP.Routes.CoolRoute.Order", "2" },
                { "YARP.Routes.EvenCoolerRoute.Hosts", "example.org" },
                { "YARP.Routes.EvenCoolerRoute.Order", "3" },
            };

            // Act
            var routes = LabelsParser.BuildRoutes(_testServiceName, labels);

            // Assert
            var expectedRoutes = new List <ProxyRoute>
            {
                new ProxyRoute
                {
                    RouteId = "MyCoolClusterId:MyRoute",
                    Match   =
                    {
                        Hosts = new[] { "example.com" },
                        Path  = "v2/{**rest}",
                    },
                    Order     = 1,
                    ClusterId = "MyCoolClusterId",
                    Metadata  = new Dictionary <string, string> {
                        { "Foo", "Bar" }
                    },
                },
                new ProxyRoute
                {
                    RouteId = "MyCoolClusterId:CoolRoute",
                    Match   =
                    {
                        Hosts = new[] { "example.net" },
                    },
                    Order     = 2,
                    ClusterId = "MyCoolClusterId",
                    Metadata  = new Dictionary <string, string>(),
                },
                new ProxyRoute
                {
                    RouteId = "MyCoolClusterId:EvenCoolerRoute",
                    Match   =
                    {
                        Hosts = new[] { "example.org" },
                    },
                    Order     = 3,
                    ClusterId = "MyCoolClusterId",
                    Metadata  = new Dictionary <string, string>(),
                },
            };

            routes.Should().BeEquivalentTo(expectedRoutes);
        }
Exemple #13
0
    /// <inheritdoc/>
    public async Task<(IReadOnlyList<RouteConfig> Routes, IReadOnlyList<ClusterConfig> Clusters)> DiscoverAsync(CancellationToken cancellation)
    {
        // Take a snapshot of current options and use that consistently for this execution.
        var options = _optionsMonitor.CurrentValue;

        _serviceFabricCaller.CleanUpExpired();

        var discoveredBackends = new Dictionary<string, ClusterConfig>(StringComparer.Ordinal);
        var discoveredRoutes = new List<RouteConfig>();
        IEnumerable<ApplicationWrapper> applications;

        try
        {
            applications = await _serviceFabricCaller.GetApplicationListAsync(cancellation);
        }
        catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception ex) // TODO: davidni: not fatal?
        {
            // The serviceFabricCaller does their best effort to use LKG information, nothing we can do at this point
            Log.GettingApplicationFailed(_logger, ex);
            applications = Enumerable.Empty<ApplicationWrapper>();
        }

        foreach (var application in applications)
        {
            IEnumerable<ServiceWrapper> services;

            try
            {
                services = await _serviceFabricCaller.GetServiceListAsync(application.ApplicationName, cancellation);
            }
            catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
            {
                throw;
            }
            catch (Exception ex) // TODO: davidni: not fatal?
            {
                Log.GettingServiceFailed(_logger, application.ApplicationName, ex);
                continue;
            }

            foreach (var service in services)
            {
                try
                {
                    var serviceExtensionLabels = await _serviceFabricExtensionConfigProvider.GetExtensionLabelsAsync(application, service, cancellation);

                    // If this service wants to use us as the proxy
                    if (serviceExtensionLabels.GetValueOrDefault("YARP.Enable", null) != "true")
                    {
                        // Skip this service
                        continue;
                    }

                    var destinations = await DiscoverDestinationsAsync(options, service, serviceExtensionLabels, cancellation);
                    var cluster = LabelsParser.BuildCluster(service.ServiceName, serviceExtensionLabels, destinations);
                    var clusterValidationErrors = await _configValidator.ValidateClusterAsync(cluster);
                    if (clusterValidationErrors.Count > 0)
                    {
                        throw new ConfigException($"Skipping cluster id '{cluster.ClusterId} due to validation errors.", new AggregateException(clusterValidationErrors));
                    }

                    if (!discoveredBackends.TryAdd(cluster.ClusterId, cluster))
                    {
                        throw new ConfigException($"Duplicated cluster id '{cluster.ClusterId}'. Skipping repeated definition, service '{service.ServiceName}'");
                    }

                    var routes = LabelsParser.BuildRoutes(service.ServiceName, serviceExtensionLabels);
                    var routeValidationErrors = new List<Exception>();
                    foreach (var route in routes)
                    {
                        routeValidationErrors.AddRange(await _configValidator.ValidateRouteAsync(route));
                    }

                    if (routeValidationErrors.Count > 0)
                    {
                        // Don't add ANY routes if even a single one is bad. Trying to add partial routes
                        // could lead to unexpected results (e.g. a typo in the configuration of higher-priority route
                        // could lead to a lower-priority route being selected for requests it should not be handling).
                        throw new ConfigException($"Skipping ALL routes for cluster id '{cluster.ClusterId} due to validation errors.", new AggregateException(routeValidationErrors));
                    }

                    discoveredRoutes.AddRange(routes);

                    ReportServiceHealth(options, service.ServiceName, HealthState.Ok, $"Successfully built cluster '{cluster.ClusterId}' with {routes.Count} routes.");
                }
                catch (ConfigException ex)
                {
                    // User error
                    Log.InvalidServiceConfig(_logger, service.ServiceName, ex);

                    // TODO: emit Error health report once we are able to detect config issues *during* (as opposed to *after*) a target service upgrade.
                    // Proactive Error health report would trigger a rollback of the target service as desired. However, an Error report after rhe fact
                    // will NOT cause a rollback and will prevent the target service from performing subsequent monitored upgrades to mitigate, making things worse.
                    ReportServiceHealth(options, service.ServiceName, HealthState.Warning, $"Could not load service configuration: {ex.Message}.");
                }
                catch (Exception ex) // TODO: davidni: not fatal?
                {
                    // Not user's problem
                    Log.ErrorLoadingServiceConfig(_logger, service.ServiceName, ex);
                }
            }
        }

        Log.ServiceDiscovered(_logger, discoveredBackends.Count, discoveredRoutes.Count);
        return (discoveredRoutes, discoveredBackends.Values.ToList());
    }