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."); }
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); }
/// <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()); }