diff --git a/docs/features/servicefabric.rst b/docs/features/servicefabric.rst index e6f69983..d3faa40f 100644 --- a/docs/features/servicefabric.rst +++ b/docs/features/servicefabric.rst @@ -33,3 +33,11 @@ The example below is taken from the samples folder so please check it if this do } } } + +If you are using stateless / guest exe services ocelot will be able to proxy through the naming service without anything else. However +if you are using statefull / actor services you must send the PartitionKind and PartitionKey query string values with the client +request e.g. + +GET http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx + +There is no way for Ocelot to work these out for you. diff --git a/samples/OcelotServiceFabric/README.md b/samples/OcelotServiceFabric/README.md index 099348c6..a601e9f4 100644 --- a/samples/OcelotServiceFabric/README.md +++ b/samples/OcelotServiceFabric/README.md @@ -9,7 +9,7 @@ author: raunakpandya edited by Tom Pallister for Ocelot This shows a service fabric cluster with Ocelot exposed over HTTP accessing services in the cluster via the naming service. If you want to try and use Ocelot with Service Fabric I reccomend using this as a starting point. -Ocelot does not support partitioned services (stateful & actors) at the moment. +If you want to use statefull / actors you must send the PartitionKind and PartitionKey to Ocelot as query string parameters. I have not tested this sample on Service Fabric hosted on Linux just a Windows dev cluster. This sample assumes a good understanding of Service Fabric. diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index dc02a20e..308daea5 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -7,6 +7,8 @@ using Ocelot.Middleware; using System; using System.Linq; using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.Responses; +using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator.Middleware { @@ -40,21 +42,9 @@ namespace Ocelot.DownstreamUrlCreator.Middleware UriBuilder uriBuilder; - //todo - feel this is a bit crap the way we build the url dont see why we need this builder thing..maybe i blew my own brains out - // when i originally wrote it.. - if (context.ServiceProviderConfiguration.Type == "ServiceFabric" && context.DownstreamReRoute.UseServiceDiscovery) + if (ServiceFabricRequest(context)) { - _logger.LogInformation("DownstreamUrlCreatorMiddleware - going to try set service fabric path"); - - var scheme = context.DownstreamReRoute.DownstreamScheme; - var host = context.DownstreamRequest.RequestUri.Host; - var port = context.DownstreamRequest.RequestUri.Port; - var serviceFabricPath = $"/{context.DownstreamReRoute.ServiceName + dsPath.Data.Value}"; - - _logger.LogInformation("DownstreamUrlCreatorMiddleware - service fabric path is {proxyUrl}", serviceFabricPath); - - var uri = new Uri($"{scheme}://{host}:{port}{serviceFabricPath}?cmd=instance"); - uriBuilder = new UriBuilder(uri); + uriBuilder = CreateServiceFabricUri(context, dsPath); } else { @@ -71,5 +61,38 @@ namespace Ocelot.DownstreamUrlCreator.Middleware await _next.Invoke(context); } + + private UriBuilder CreateServiceFabricUri(DownstreamContext context, Response dsPath) + { + var query = context.DownstreamRequest.RequestUri.Query; + var scheme = context.DownstreamReRoute.DownstreamScheme; + var host = context.DownstreamRequest.RequestUri.Host; + var port = context.DownstreamRequest.RequestUri.Port; + var serviceFabricPath = $"/{context.DownstreamReRoute.ServiceName + dsPath.Data.Value}"; + + Uri uri; + + if (RequestForStatefullService(query)) + { + uri = new Uri($"{scheme}://{host}:{port}{serviceFabricPath}{query}"); + } + else + { + var split = string.IsNullOrEmpty(query) ? "?" : "&"; + uri = new Uri($"{scheme}://{host}:{port}{serviceFabricPath}{query}{split}cmd=instance"); + } + + return new UriBuilder(uri); + } + + private static bool ServiceFabricRequest(DownstreamContext context) + { + return context.ServiceProviderConfiguration.Type == "ServiceFabric" && context.DownstreamReRoute.UseServiceDiscovery; + } + + private static bool RequestForStatefullService(string query) + { + return query.Contains("PartitionKind") && query.Contains("PartitionKey"); + } } } diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs index bfe14103..3f79a801 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs @@ -27,7 +27,7 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_support_service_fabric_naming_and_dns_service() + public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() { var configuration = new FileConfiguration { @@ -54,7 +54,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "cmd=instance")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces")) @@ -63,7 +63,44 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + [Fact] + public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + UseServiceDiscovery = true, + ServiceName = "OcelotServiceApplication/OcelotApplicationService" + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Host = "localhost", + Port = 19081, + Type = "ServiceFabric" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) { _builder = new WebHostBuilder() .UseUrls(baseUrl) @@ -84,9 +121,8 @@ namespace Ocelot.AcceptanceTests } else { - if (context.Request.Query.TryGetValue("cmd", out var values)) + if (context.Request.QueryString.Value.Contains(expectedQueryString)) { - values.First().ShouldBe("instance"); context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index d0926fc9..0d678a7b 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -135,6 +135,66 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator .BDDfy(); } + [Fact] + public void should_create_service_fabric_url_with_query_string_for_stateless_service() + { + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + + var downstreamRoute = new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .Build()); + + var config = new ServiceProviderConfigurationBuilder() + .WithServiceDiscoveryProviderType("ServiceFabric") + .WithServiceDiscoveryProviderHost("localhost") + .WithServiceDiscoveryProviderPort(19081) + .Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1")) + .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1&cmd=instance")) + .BDDfy(); + } + + [Fact] + public void should_create_service_fabric_url_with_query_string_for_stateful_service() + { + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + + var downstreamRoute = new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .Build()); + + var config = new ServiceProviderConfigurationBuilder() + .WithServiceDiscoveryProviderType("ServiceFabric") + .WithServiceDiscoveryProviderHost("localhost") + .WithServiceDiscoveryProviderPort(19081) + .Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) + .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1")) + .BDDfy(); + } + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { _downstreamContext.ServiceProviderConfiguration = config;