From 4f061f2b741e5de2fe2f22236be89cd7531f1df1 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 20 Apr 2018 21:28:49 +0100 Subject: [PATCH] Feature/steeltoe (#324) * #262 - Integrated Steeltoe Service Discovery with Ocelot for review. * messing around * seems to be working with eureka * acceptance test passing with external lib references * #262 support for netflix eureka service discovery thanks to pivotal * #262 fixed warnings --- README.md | 3 +- docs/features/servicediscovery.rst | 35 +- .../DependencyInjection/IOcelotBuilder.cs | 2 +- .../DependencyInjection/OcelotBuilder.cs | 21 ++ .../Middleware/OcelotMiddlewareExtensions.cs | 11 + src/Ocelot/Ocelot.csproj | 1 + .../EurekaServiceDiscoveryProvider.cs | 34 ++ .../Providers/FakeEurekaDiscoveryClient.cs | 27 ++ .../ServiceDiscoveryProviderFactory.cs | 19 +- .../Ocelot.AcceptanceTests.csproj | 3 + .../ServiceDiscoveryTests.cs | 324 ++++++++++++++++-- test/Ocelot.AcceptanceTests/appsettings.json | 14 + .../appsettings.product.json | 24 ++ test/Ocelot.ManualTest/Program.cs | 40 +-- test/Ocelot.ManualTest/appsettings.json | 8 +- .../OcelotPipelineExtensionsTests.cs | 28 +- .../EurekaServiceDiscoveryProviderTests.cs | 117 +++++++ .../ServiceProviderFactoryTests.cs | 26 +- 18 files changed, 665 insertions(+), 72 deletions(-) create mode 100644 src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs create mode 100644 src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs create mode 100644 test/Ocelot.AcceptanceTests/appsettings.product.json create mode 100644 test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs diff --git a/README.md b/README.md index f8ad7b22..edd28eaf 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ A quick list of Ocelot's capabilities for more information see the [documentatio * Routing * Request Aggregation -* Service Discovery with Consul +* Service Discovery with Consul & Eureka * Service Fabric * WebSockets * Authentication @@ -52,6 +52,7 @@ A quick list of Ocelot's capabilities for more information see the [documentatio * Headers / Query String / Claims Transformation * Custom Middleware / Delegating Handlers * Configuration / Administration REST API +* Platform / Cloud agnostic ## How to install diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 3add60f1..88b13057 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -52,4 +52,37 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade "Token": "footoken" } -Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. \ No newline at end of file +Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. + +Eureka +^^^^^^ + +This feature was requested as part of `Issue 262 `_ . to add support for Netflix's +Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something +to do with `Pivotal `_! Anyway enough of the background. + +In order to get this working add the following to ocelot.json.. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Type": "Eureka" + } + +And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below +tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. + +.. code-block:: json + + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true + } + } + +Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with +Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. +When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code +is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. + diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index a513e19e..7cf514cb 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -21,7 +21,7 @@ namespace Ocelot.DependencyInjection IOcelotBuilder AddSingletonDelegatingHandler(bool global = false) where T : DelegatingHandler; - + IOcelotBuilder AddTransientDelegatingHandler(bool global = false) where T : DelegatingHandler; diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 5373cc1b..672e8540 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -45,6 +45,8 @@ namespace Ocelot.DependencyInjection using Ocelot.Infrastructure.Consul; using Butterfly.Client.Tracing; using Ocelot.Middleware.Multiplexer; + using Pivotal.Discovery.Client; + using ServiceDiscovery.Providers; public class OcelotBuilder : IOcelotBuilder { @@ -112,6 +114,17 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); + if (UsingEurekaServiceDiscoveryProvider(configurationRoot)) + { + _services.AddDiscoveryClient(configurationRoot); + } + else + { + _services.TryAddSingleton(); + } + + _services.TryAddSingleton(); + // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository _services.TryAddSingleton(); @@ -346,5 +359,13 @@ namespace Ocelot.DependencyInjection } }; } + + private static bool UsingEurekaServiceDiscoveryProvider(IConfiguration configurationRoot) + { + var type = configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Type", + string.Empty); + + return type.ToLower() == "eureka"; + } } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 25d818c5..4fc3db7a 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -17,6 +17,7 @@ using Rafty.Concensus; using Rafty.Infrastructure; using Ocelot.Middleware.Pipeline; + using Pivotal.Discovery.Client; public static class OcelotMiddlewareExtensions { @@ -38,6 +39,11 @@ SetUpRafty(builder); } + if (UsingEurekaServiceDiscoveryProvider(configuration)) + { + builder.UseDiscoveryClient(); + } + ConfigureDiagnosticListener(builder); var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices); @@ -63,6 +69,11 @@ return builder; } + private static bool UsingEurekaServiceDiscoveryProvider(IInternalConfiguration configuration) + { + return configuration?.ServiceProviderConfiguration != null && configuration.ServiceProviderConfiguration.Type?.ToLower() == "eureka"; + } + private static bool UsingRafty(IApplicationBuilder builder) { var possible = builder.ApplicationServices.GetService(typeof(INode)) as INode; diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 68f1dc9f..833887a2 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs new file mode 100644 index 00000000..ab78db1b --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs @@ -0,0 +1,34 @@ +namespace Ocelot.ServiceDiscovery.Providers +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Pivotal.Discovery.Client; + using Values; + + public class EurekaServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly IDiscoveryClient _client; + private readonly string _serviceName; + + public EurekaServiceDiscoveryProvider(string serviceName, IDiscoveryClient client) + { + _client = client; + _serviceName = serviceName; + } + + public Task> Get() + { + var services = new List(); + + var instances = _client.GetInstances(_serviceName); + + if (instances != null && instances.Any()) + { + services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port), "", "", new List()))); + } + + return Task.FromResult(services); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs new file mode 100644 index 00000000..78612148 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs @@ -0,0 +1,27 @@ +namespace Ocelot.ServiceDiscovery.Providers +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Pivotal.Discovery.Client; + + public class FakeEurekaDiscoveryClient : IDiscoveryClient + { + public IServiceInstance GetLocalServiceInstance() + { + throw new System.NotImplementedException(); + } + + public IList GetInstances(string serviceId) + { + throw new System.NotImplementedException(); + } + + public Task ShutdownAsync() + { + throw new System.NotImplementedException(); + } + + public string Description { get; } + public IList Services { get; } + } +} diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index d13c333a..cd678c4f 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -8,15 +8,19 @@ using Ocelot.Values; namespace Ocelot.ServiceDiscovery { + using Pivotal.Discovery.Client; + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { private readonly IOcelotLoggerFactory _factory; - private readonly IConsulClientFactory _clientFactory; + private readonly IConsulClientFactory _consulFactory; + private readonly IDiscoveryClient _eurekaClient; - public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient) { _factory = factory; - _clientFactory = clientFactory; + _consulFactory = consulFactory; + _eurekaClient = eurekaClient; } public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) @@ -40,14 +44,19 @@ namespace Ocelot.ServiceDiscovery private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName) { - if (serviceConfig.Type == "ServiceFabric") + if (serviceConfig.Type?.ToLower() == "servicefabric") { var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); return new ServiceFabricServiceDiscoveryProvider(config); } + if (serviceConfig.Type?.ToLower() == "eureka") + { + return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient); + } + var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token); - return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _clientFactory); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory); } } } diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 18ff45f1..1824df0d 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -15,6 +15,9 @@ + + PreserveNewest + PreserveNewest diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 972b165f..151bb99c 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -1,26 +1,29 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using Consul; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - namespace Ocelot.AcceptanceTests { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using Consul; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using Ocelot.Configuration.File; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Newtonsoft.Json; + using Pivotal.Discovery.Client; + public class ServiceDiscoveryTests : IDisposable { private IWebHost _builderOne; private IWebHost _builderTwo; private IWebHost _fakeConsulBuilder; private readonly Steps _steps; - private readonly List _serviceEntries; + private readonly List _consulServices; + private readonly List _eurekaInstances; private int _counterOne; private int _counterTwo; private static readonly object SyncLock = new object(); @@ -31,11 +34,59 @@ namespace Ocelot.AcceptanceTests public ServiceDiscoveryTests() { _steps = new Steps(); - _serviceEntries = new List(); + _consulServices = new List(); + _eurekaInstances = new List(); } [Fact] - public void should_use_service_discovery_and_load_balance_request() + public void should_use_eureka_service_discovery_and_make_request() + { + var eurekaPort = 8761; + var serviceName = "product"; + var downstreamServicePort = 50371; + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancer = "LeastConnection", + UseServiceDiscovery = true, + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Type = "Eureka" + } + } + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(ServiceDiscoveryTests))) + .BDDfy(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request() { var consulPort = 8502; var serviceName = "product"; @@ -102,7 +153,6 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - //test from issue #213 [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request() { @@ -158,7 +208,6 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - //test from issue #295 [Fact] public void should_use_token_to_make_request_to_consul() { @@ -218,7 +267,7 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_send_request_to_service_after_it_becomes_available() + public void should_send_request_to_service_after_it_becomes_available_in_consul() { var consulPort = 8501; var serviceName = "product"; @@ -296,7 +345,7 @@ namespace Ocelot.AcceptanceTests private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) { - _serviceEntries.Add(serviceEntryTwo); + _consulServices.Add(serviceEntryTwo); } private void ThenOnlyOneServiceHasBeenCalled() @@ -307,7 +356,7 @@ namespace Ocelot.AcceptanceTests private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) { - _serviceEntries.Remove(serviceEntryTwo); + _consulServices.Remove(serviceEntryTwo); } private void GivenIResetCounters() @@ -332,10 +381,100 @@ namespace Ocelot.AcceptanceTests { foreach(var serviceEntry in serviceEntries) { - _serviceEntries.Add(serviceEntry); + _consulServices.Add(serviceEntry); } } + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new Instance + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223 + }, + metadata = new Metadata + { + value = "java.util.Collections$EmptyMap" + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED" + } + } + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1" + } + }; + + await context.Response.WriteJsonAsync(applications); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) { _fakeConsulBuilder = new WebHostBuilder() @@ -355,7 +494,7 @@ namespace Ocelot.AcceptanceTests _receivedToken = values.First(); } - await context.Response.WriteJsonAsync(_serviceEntries); + await context.Response.WriteJsonAsync(_consulServices); } }); }) @@ -433,6 +572,34 @@ namespace Ocelot.AcceptanceTests _builderTwo.Start(); } + private void GivenEurekaProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(ServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) { _builder = new WebHostBuilder() @@ -471,4 +638,113 @@ namespace Ocelot.AcceptanceTests _steps.Dispose(); } } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class Metadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public Metadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } } diff --git a/test/Ocelot.AcceptanceTests/appsettings.json b/test/Ocelot.AcceptanceTests/appsettings.json index 6d9b7c55..36499f4d 100644 --- a/test/Ocelot.AcceptanceTests/appsettings.json +++ b/test/Ocelot.AcceptanceTests/appsettings.json @@ -6,5 +6,19 @@ "System": "Error", "Microsoft": "Error" } + }, + "spring": { + "application": { + "name": "ocelot" + } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true, + "shouldFetchRegistry": true, + "port": 5000, + "hostName": "localhost" + } } } diff --git a/test/Ocelot.AcceptanceTests/appsettings.product.json b/test/Ocelot.AcceptanceTests/appsettings.product.json new file mode 100644 index 00000000..8d8f06c7 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/appsettings.product.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Error", + "System": "Error", + "Microsoft": "Error" + } + }, + "spring": { + "application": { + "name": "product" + } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true + }, + "instance": { + "port": 50371 + } + } +} diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 966b1097..dcc7676d 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,13 +1,13 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; - -namespace Ocelot.ManualTest +namespace Ocelot.ManualTest { + using System.IO; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.DependencyInjection; + using Ocelot.Middleware; + public class Program { public static void Main(string[] args) @@ -32,17 +32,17 @@ namespace Ocelot.ManualTest x.Audience = "test"; }); - s.AddOcelot() - .AddCacheManager(x => - { - x.WithDictionaryHandle(); - }) - /* .AddOpenTracing(option => - { - option.CollectorUrl = "http://localhost:9618"; - option.Service = "Ocelot.ManualTest"; - })*/ - .AddAdministration("/administration", "secret"); + s.AddOcelot() + .AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) + /*.AddOpenTracing(option => + { + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot.ManualTest"; + })*/ + .AddAdministration("/administration", "secret"); }) .ConfigureLogging((hostingContext, logging) => { diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index 584fc4fc..16fe363b 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -2,9 +2,15 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Trace", + "Default": "Error", "System": "Error", "Microsoft": "Error" } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true + } } } diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index d61124ba..17f9e3e8 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -1,20 +1,15 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Ocelot.Middleware.Pipeline; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - namespace Ocelot.UnitTests.Middleware { + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.DependencyInjection; + using Ocelot.Middleware; + using Ocelot.Middleware.Pipeline; + using Pivotal.Discovery.Client; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + public class OcelotPipelineExtensionsTests { private OcelotPipelineBuilder _builder; @@ -45,9 +40,10 @@ namespace Ocelot.UnitTests.Middleware var root = test.Build(); var services = new ServiceCollection(); services.AddSingleton(root); + services.AddDiscoveryClient(new DiscoveryOptions {ClientType = DiscoveryClientType.EUREKA}); services.AddOcelot(); var provider = services.BuildServiceProvider(); _builder = new OcelotPipelineBuilder(provider); } } -} \ No newline at end of file +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..0ec071d1 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs @@ -0,0 +1,117 @@ +namespace Ocelot.UnitTests.ServiceDiscovery +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Moq; + using Ocelot.ServiceDiscovery.Providers; + using Pivotal.Discovery.Client; + using Shouldly; + using TestStack.BDDfy; + using Values; + using Xunit; + + public class EurekaServiceDiscoveryProviderTests + { + private readonly EurekaServiceDiscoveryProvider _provider; + private readonly Mock _client; + private readonly string _serviceId; + private List _instances; + private List _result; + + public EurekaServiceDiscoveryProviderTests() + { + _serviceId = "Laura"; + _client = new Mock(); + _provider = new EurekaServiceDiscoveryProvider(_serviceId, _client.Object); + } + + [Fact] + public void should_return_empty_services() + { + this.When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(0)) + .BDDfy(); + } + + [Fact] + public void should_return_service_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(1)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .And(_ => ThenTheServiceIsMapped()) + .BDDfy(); + } + + [Fact] + public void should_return_services_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(2)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheServiceIsMapped() + { + _result[0].HostAndPort.DownstreamHost.ShouldBe("somehost"); + _result[0].HostAndPort.DownstreamPort.ShouldBe(801); + _result[0].Name.ShouldBe(_serviceId); + } + + private void ThenTheCountIs(int expected) + { + _result.Count.ShouldBe(expected); + } + + private void ThenTheClientIsCalledCorrectly() + { + _client.Verify(x => x.GetInstances(_serviceId), Times.Once); + } + + private async Task WhenIGet() + { + _result = await _provider.Get(); + } + + private void GivenThe(List instances) + { + _instances = instances; + _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); + } + } + + public class EurekaService : IServiceInstance + { + public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index 3e998431..4621913a 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -13,6 +13,8 @@ using Xunit; namespace Ocelot.UnitTests.ServiceDiscovery { + using Pivotal.Discovery.Client; + public class ServiceProviderFactoryTests { private ServiceProviderConfiguration _serviceConfig; @@ -20,13 +22,13 @@ namespace Ocelot.UnitTests.ServiceDiscovery private readonly ServiceDiscoveryProviderFactory _factory; private DownstreamReRoute _reRoute; private Mock _loggerFactory; - private IConsulClientFactory _clientFactory; + private Mock _discoveryClient; public ServiceProviderFactoryTests() { _loggerFactory = new Mock(); - _clientFactory = new ConsulClientFactory(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _clientFactory); + _discoveryClient = new Mock(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, new ConsulClientFactory(), _discoveryClient.Object); } [Fact] @@ -99,6 +101,24 @@ namespace Ocelot.UnitTests.ServiceDiscovery .BDDfy(); } + [Fact] + public void should_return_eureka_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("Eureka") + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) { var result = (ConfigurationServiceProvider)_result;