Feature/poll consul (#392)

* WIP - implement a consul service discovery poller, lots of shared code with existing, refactor next and a todo in the docs to finish

* #374 implement polling for consul as option

* #374 updated docs to remove todo

* #374 fixed failing unit test

* #374 fixed failing unit test

* #374 fixed failing acceptance test
This commit is contained in:
Tom Pallister 2018-06-12 00:58:08 +03:00 committed by GitHub
parent 14308ff5fb
commit 0f2a9c1d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 649 additions and 369 deletions

View File

@ -1,156 +1,171 @@
.. service-discovery: .. service-discovery:
Service Discovery Service Discovery
================= =================
Ocelot allows you to specify a service discovery provider and will use this to find the host and port Ocelot allows you to specify a service discovery provider and will use this to find the host and port
for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the
GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes
you specify a ServiceName for at ReRoute level. you specify a ServiceName for at ReRoute level.
Consul Consul
^^^^^^ ^^^^^^
The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default
will be used. will be used.
.. code-block:: json .. code-block:: json
"ServiceDiscoveryProvider": { "ServiceDiscoveryProvider": {
"Host": "localhost", "Host": "localhost",
"Port": 9500 "Port": 9500
} }
In the future we can add a feature that allows ReRoute specfic configuration. In the future we can add a feature that allows ReRoute specfic configuration.
In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the
ServiceName, UseServiceDiscovery and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin ServiceName, UseServiceDiscovery and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin
and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests.
.. code-block:: json .. code-block:: json
{ {
"DownstreamPathTemplate": "/api/posts/{postId}", "DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"UpstreamPathTemplate": "/posts/{postId}", "UpstreamPathTemplate": "/posts/{postId}",
"UpstreamHttpMethod": [ "Put" ], "UpstreamHttpMethod": [ "Put" ],
"ServiceName": "product", "ServiceName": "product",
"LoadBalancerOptions": { "LoadBalancerOptions": {
"Type": "LeastConnection" "Type": "LeastConnection"
}, },
"UseServiceDiscovery": true "UseServiceDiscovery": true
} }
When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services.
ACL Token A lot of people have asked me to implement a feature where Ocelot polls consul for latest service information rather than per request. If you want to poll consul for the latest services rather than per request (default behaviour) then you need to set the following configuration.
---------
.. code-block:: json
If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below.
"ServiceDiscoveryProvider": {
.. code-block:: json "Host": "localhost",
"Port": 9500,
"ServiceDiscoveryProvider": { "Type": "PollConsul",
"Host": "localhost", "PollingInteral": 100
"Port": 9500, }
"Token": "footoken"
} The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration.
Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volitile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling consul per request (as sidecar agent). If you are calling a remote consul agent then polling will be a good performance improvement.
Eureka ACL Token
^^^^^^ ---------
This feature was requested as part of `Issue 262 <https://github.com/TomPallister/Ocelot/issue/262>`_ . to add support for Netflix's If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below.
Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something
to do with `Pivotal <https://pivotal.io/platform>`_! Anyway enough of the background. .. code-block:: json
In order to get this working add the following to ocelot.json.. "ServiceDiscoveryProvider": {
"Host": "localhost",
.. code-block:: json "Port": 9500,
"Token": "footoken"
"ServiceDiscoveryProvider": { }
"Type": "Eureka"
} Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request.
And following the guide `Here <https://steeltoe.io/docs/steeltoe-discovery/>`_ 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. Eureka
^^^^^^
.. code-block:: json
This feature was requested as part of `Issue 262 <https://github.com/TomPallister/Ocelot/issue/262>`_ . to add support for Netflix's
"eureka": { Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something
"client": { to do with `Pivotal <https://pivotal.io/platform>`_! Anyway enough of the background.
"serviceUrl": "http://localhost:8761/eureka/",
"shouldRegisterWithEureka": false, In order to get this working add the following to ocelot.json..
"shouldFetchRegistry": true
} .. code-block:: json
}
"ServiceDiscoveryProvider": {
I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. "Type": "Eureka"
}
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. And following the guide `Here <https://steeltoe.io/docs/steeltoe-discovery/>`_ 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.
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. .. code-block:: json
Dynamic Routing "eureka": {
^^^^^^^^^^^^^^^ "client": {
"serviceUrl": "http://localhost:8761/eureka/",
This feature was requested in `issue 340 <https://github.com/TomPallister/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using "shouldRegisterWithEureka": false,
a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segmentof the upstream path to lookup the "shouldFetchRegistry": true
downstream service with the service discovery provider. }
}
An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of
the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there.
port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products.
Ocelot will apprend any query string to the downstream url as normal. 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.
In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code
need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work.
In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but Dynamic Routing
talk to private services over http) that will be applied to all of the dynamic ReRoutes. ^^^^^^^^^^^^^^^
The config might look something like This feature was requested in `issue 340 <https://github.com/TomPallister/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using
a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segmentof the upstream path to lookup the
.. code-block:: json downstream service with the service discovery provider.
{ An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of
"ReRoutes": [], the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and
"Aggregates": [], port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products.
"GlobalConfiguration": { Ocelot will apprend any query string to the downstream url as normal.
"RequestIdKey": null,
"ServiceDiscoveryProvider": { In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you
"Host": "localhost", need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme.
"Port": 8510,
"Type": null, In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but
"Token": null, talk to private services over http) that will be applied to all of the dynamic ReRoutes.
"ConfigurationKey": null
}, The config might look something like
"RateLimitOptions": {
"ClientIdHeader": "ClientId", .. code-block:: json
"QuotaExceededMessage": null,
"RateLimitCounterPrefix": "ocelot", {
"DisableRateLimitHeaders": false, "ReRoutes": [],
"HttpStatusCode": 429 "Aggregates": [],
}, "GlobalConfiguration": {
"QoSOptions": { "RequestIdKey": null,
"ExceptionsAllowedBeforeBreaking": 0, "ServiceDiscoveryProvider": {
"DurationOfBreak": 0, "Host": "localhost",
"TimeoutValue": 0 "Port": 8510,
}, "Type": null,
"BaseUrl": null, "Token": null,
"LoadBalancerOptions": { "ConfigurationKey": null
"Type": "LeastConnection", },
"Key": null, "RateLimitOptions": {
"Expiry": 0 "ClientIdHeader": "ClientId",
}, "QuotaExceededMessage": null,
"DownstreamScheme": "http", "RateLimitCounterPrefix": "ocelot",
"HttpHandlerOptions": { "DisableRateLimitHeaders": false,
"AllowAutoRedirect": false, "HttpStatusCode": 429
"UseCookieContainer": false, },
"UseTracing": false "QoSOptions": {
} "ExceptionsAllowedBeforeBreaking": 0,
} "DurationOfBreak": 0,
} "TimeoutValue": 0
},
Please take a look through all of the docs to understand these options. "BaseUrl": null,
"LoadBalancerOptions": {
"Type": "LeastConnection",
"Key": null,
"Expiry": 0
},
"DownstreamScheme": "http",
"HttpHandlerOptions": {
"AllowAutoRedirect": false,
"UseCookieContainer": false,
"UseTracing": false
}
}
}
Please take a look through all of the docs to understand these options.

View File

@ -7,6 +7,7 @@ namespace Ocelot.Configuration.Builder
private string _type; private string _type;
private string _token; private string _token;
private string _configurationKey; private string _configurationKey;
private int _pollingInterval;
public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost)
{ {
@ -38,9 +39,15 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ServiceProviderConfigurationBuilder WithPollingInterval(int pollingInterval)
{
_pollingInterval = pollingInterval;
return this;
}
public ServiceProviderConfiguration Build() public ServiceProviderConfiguration Build()
{ {
return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey); return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval);
} }
} }
} }

View File

@ -9,6 +9,7 @@ namespace Ocelot.Configuration.Creator
{ {
var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? "consul"; var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? "consul";
var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0;
return new ServiceProviderConfigurationBuilder() return new ServiceProviderConfigurationBuilder()
.WithHost(host) .WithHost(host)
@ -16,6 +17,7 @@ namespace Ocelot.Configuration.Creator
.WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type) .WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type)
.WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token)
.WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey) .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey)
.WithPollingInterval(pollingInterval)
.Build(); .Build();
} }
} }

View File

@ -7,5 +7,6 @@ namespace Ocelot.Configuration.File
public string Type { get; set; } public string Type { get; set; }
public string Token { get; set; } public string Token { get; set; }
public string ConfigurationKey { get; set; } public string ConfigurationKey { get; set; }
public int PollingInterval { get; set; }
} }
} }

View File

@ -2,13 +2,14 @@
{ {
public class ServiceProviderConfiguration public class ServiceProviderConfiguration
{ {
public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey) public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey, int pollingInterval)
{ {
ConfigurationKey = configurationKey; ConfigurationKey = configurationKey;
Host = host; Host = host;
Port = port; Port = port;
Token = token; Token = token;
Type = type; Type = type;
PollingInterval = pollingInterval;
} }
public string Host { get; } public string Host { get; }
@ -16,5 +17,6 @@
public string Type { get; } public string Type { get; }
public string Token { get; } public string Token { get; }
public string ConfigurationKey { get; } public string ConfigurationKey { get; }
public int PollingInterval { get; }
} }
} }

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Consul;
using Ocelot.Infrastructure.Consul;
using Ocelot.Infrastructure.Extensions;
using Ocelot.Logging;
using Ocelot.ServiceDiscovery.Configuration;
using Ocelot.Values;
namespace Ocelot.ServiceDiscovery.Providers
{
public class PollingConsulServiceDiscoveryProvider : IServiceDiscoveryProvider
{
private readonly IOcelotLogger _logger;
private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider;
private readonly Timer _timer;
private bool _polling;
private List<Service> _services;
private string _keyOfServiceInConsul;
public PollingConsulServiceDiscoveryProvider(int pollingInterval, string keyOfServiceInConsul, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider)
{;
_logger = factory.CreateLogger<PollingConsulServiceDiscoveryProvider>();
_keyOfServiceInConsul = keyOfServiceInConsul;
_consulServiceDiscoveryProvider = consulServiceDiscoveryProvider;
_services = new List<Service>();
_timer = new Timer(async x =>
{
if(_polling)
{
return;
}
_polling = true;
await Poll();
_polling = false;
}, null, pollingInterval, pollingInterval);
}
public Task<List<Service>> Get()
{
return Task.FromResult(_services);
}
private async Task Poll()
{
_services = await _consulServiceDiscoveryProvider.Get();
}
}
}

View File

@ -1,62 +1,70 @@
using System.Collections.Generic; using System.Collections.Generic;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Infrastructure.Consul; using Ocelot.Infrastructure.Consul;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.ServiceDiscovery.Configuration; using Ocelot.ServiceDiscovery.Configuration;
using Ocelot.ServiceDiscovery.Providers; using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values; using Ocelot.Values;
namespace Ocelot.ServiceDiscovery namespace Ocelot.ServiceDiscovery
{ {
using Steeltoe.Common.Discovery; using Steeltoe.Common.Discovery;
public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory
{ {
private readonly IOcelotLoggerFactory _factory; private readonly IOcelotLoggerFactory _factory;
private readonly IConsulClientFactory _consulFactory; private readonly IConsulClientFactory _consulFactory;
private readonly IDiscoveryClient _eurekaClient; private readonly IDiscoveryClient _eurekaClient;
public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient) public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient)
{ {
_factory = factory; _factory = factory;
_consulFactory = consulFactory; _consulFactory = consulFactory;
_eurekaClient = eurekaClient; _eurekaClient = eurekaClient;
} }
public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute)
{ {
if (reRoute.UseServiceDiscovery) if (reRoute.UseServiceDiscovery)
{ {
return GetServiceDiscoveryProvider(serviceConfig, reRoute.ServiceName); return GetServiceDiscoveryProvider(serviceConfig, reRoute.ServiceName);
} }
var services = new List<Service>(); var services = new List<Service>();
foreach (var downstreamAddress in reRoute.DownstreamAddresses) foreach (var downstreamAddress in reRoute.DownstreamAddresses)
{ {
var service = new Service(reRoute.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port), string.Empty, string.Empty, new string[0]); var service = new Service(reRoute.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port), string.Empty, string.Empty, new string[0]);
services.Add(service); services.Add(service);
} }
return new ConfigurationServiceProvider(services); return new ConfigurationServiceProvider(services);
} }
private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName) private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName)
{ {
if (serviceConfig.Type?.ToLower() == "servicefabric") if (serviceConfig.Type?.ToLower() == "servicefabric")
{ {
var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName);
return new ServiceFabricServiceDiscoveryProvider(config); return new ServiceFabricServiceDiscoveryProvider(config);
} }
if (serviceConfig.Type?.ToLower() == "eureka") if (serviceConfig.Type?.ToLower() == "eureka")
{ {
return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient); return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient);
} }
var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token); var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token);
return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory);
} var consulServiceDiscoveryProvider = new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory);
}
} if (serviceConfig.Type?.ToLower() == "pollconsul")
{
return new PollingConsulServiceDiscoveryProvider(serviceConfig.PollingInterval, consulRegistryConfiguration.KeyOfServiceInConsul, _factory, consulServiceDiscoveryProvider);
}
return consulServiceDiscoveryProvider;
}
}
}

View File

@ -458,6 +458,64 @@ namespace Ocelot.AcceptanceTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request()
{
const int consulPort = 8518;
const string serviceName = "web";
const int downstreamServicePort = 8082;
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
var serviceEntryOne = new ServiceEntry()
{
Service = new AgentService()
{
Service = serviceName,
Address = "localhost",
Port = downstreamServicePort,
ID = $"web_90_0_2_224_{downstreamServicePort}",
Tags = new[] {"version-v1"}
},
};
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/home",
DownstreamScheme = "http",
UpstreamPathTemplate = "/home",
UpstreamHttpMethod = new List<string> { "Get", "Options" },
ServiceName = serviceName,
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
UseServiceDiscovery = true,
}
},
GlobalConfiguration = new FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
{
Host = "localhost",
Port = consulPort,
Type = "PollConsul",
PollingInterval = 0
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo)
{ {
_consulServices.Add(serviceEntryTwo); _consulServices.Add(serviceEntryTwo);

View File

@ -27,6 +27,7 @@ using System.Text;
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
using Ocelot.Requester; using Ocelot.Requester;
using Ocelot.Middleware.Multiplexer; using Ocelot.Middleware.Multiplexer;
using static Ocelot.Infrastructure.Wait;
namespace Ocelot.AcceptanceTests namespace Ocelot.AcceptanceTests
{ {
@ -675,6 +676,24 @@ namespace Ocelot.AcceptanceTests
_response = _ocelotClient.GetAsync(url).Result; _response = _ocelotClient.GetAsync(url).Result;
} }
public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url)
{
var result = WaitFor(2000).Until(() => {
try
{
_response = _ocelotClient.GetAsync(url).Result;
_response.EnsureSuccessStatusCode();
return true;
}
catch(Exception)
{
return false;
}
});
result.ShouldBeTrue();
}
public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value)
{ {
var request = _ocelotServer.CreateRequest(url); var request = _ocelotServer.CreateRequest(url);

View File

@ -26,7 +26,7 @@ namespace Ocelot.UnitTests.LoadBalancer
{ {
_factory = new Mock<ILoadBalancerFactory>(); _factory = new Mock<ILoadBalancerFactory>();
_loadBalancerHouse = new LoadBalancerHouse(_factory.Object); _loadBalancerHouse = new LoadBalancerHouse(_factory.Object);
_serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty, "configKey"); _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty, "configKey", 0);
} }
[Fact] [Fact]

View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Moq;
using Ocelot.Infrastructure.Consul;
using Ocelot.Logging;
using Ocelot.ServiceDiscovery.Configuration;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using Xunit;
using TestStack.BDDfy;
using Shouldly;
using static Ocelot.Infrastructure.Wait;
namespace Ocelot.UnitTests.ServiceDiscovery
{
public class PollingConsulServiceDiscoveryProviderTests
{
private readonly int _delay;
private PollingConsulServiceDiscoveryProvider _provider;
private readonly string _serviceName;
private List<Service> _services;
private readonly Mock<IOcelotLoggerFactory> _factory;
private readonly Mock<IOcelotLogger> _logger;
private Mock<IServiceDiscoveryProvider> _consulServiceDiscoveryProvider;
private List<Service> _result;
public PollingConsulServiceDiscoveryProviderTests()
{
_services = new List<Service>();
_delay = 1;
_factory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>();
_factory.Setup(x => x.CreateLogger<PollingConsulServiceDiscoveryProvider>()).Returns(_logger.Object);
_consulServiceDiscoveryProvider = new Mock<IServiceDiscoveryProvider>();
}
[Fact]
public void should_return_service_from_consul()
{
var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List<string>());
this.Given(x => GivenConsulReturns(service))
.When(x => WhenIGetTheServices(1))
.Then(x => ThenTheCountIs(1))
.BDDfy();
}
private void GivenConsulReturns(Service service)
{
_services.Add(service);
_consulServiceDiscoveryProvider.Setup(x => x.Get()).ReturnsAsync(_services);
}
private void ThenTheCountIs(int count)
{
_result.Count.ShouldBe(count);
}
private void WhenIGetTheServices(int expected)
{
_provider = new PollingConsulServiceDiscoveryProvider(_delay, _serviceName, _factory.Object, _consulServiceDiscoveryProvider.Object);
var result = WaitFor(3000).Until(() => {
try
{
_result = _provider.Get().GetAwaiter().GetResult();
if(_result.Count == expected)
{
return true;
}
return false;
}
catch(Exception)
{
return false;
}
});
result.ShouldBeTrue();
}
}
}

View File

@ -1,154 +1,177 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Moq; using Moq;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Configuration.Builder; using Ocelot.Configuration.Builder;
using Ocelot.Infrastructure.Consul; using Ocelot.Infrastructure.Consul;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
using Ocelot.ServiceDiscovery.Providers; using Ocelot.ServiceDiscovery.Providers;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests.ServiceDiscovery namespace Ocelot.UnitTests.ServiceDiscovery
{ {
using Pivotal.Discovery.Client; using Pivotal.Discovery.Client;
using Steeltoe.Common.Discovery; using Steeltoe.Common.Discovery;
public class ServiceProviderFactoryTests public class ServiceProviderFactoryTests
{ {
private ServiceProviderConfiguration _serviceConfig; private ServiceProviderConfiguration _serviceConfig;
private IServiceDiscoveryProvider _result; private IServiceDiscoveryProvider _result;
private readonly ServiceDiscoveryProviderFactory _factory; private readonly ServiceDiscoveryProviderFactory _factory;
private DownstreamReRoute _reRoute; private DownstreamReRoute _reRoute;
private Mock<IOcelotLoggerFactory> _loggerFactory; private Mock<IOcelotLoggerFactory> _loggerFactory;
private Mock<IDiscoveryClient> _discoveryClient; private Mock<IDiscoveryClient> _discoveryClient;
private Mock<IOcelotLogger> _logger;
public ServiceProviderFactoryTests()
{ public ServiceProviderFactoryTests()
_loggerFactory = new Mock<IOcelotLoggerFactory>(); {
_discoveryClient = new Mock<IDiscoveryClient>(); _loggerFactory = new Mock<IOcelotLoggerFactory>();
_factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, new ConsulClientFactory(), _discoveryClient.Object); _logger = new Mock<IOcelotLogger>();
} _loggerFactory.Setup(x => x.CreateLogger<PollingConsulServiceDiscoveryProvider>()).Returns(_logger.Object);
_discoveryClient = new Mock<IDiscoveryClient>();
[Fact] var consulClient = new Mock<IConsulClientFactory>();
public void should_return_no_service_provider() _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, consulClient.Object, _discoveryClient.Object);
{ }
var serviceConfig = new ServiceProviderConfigurationBuilder()
.Build(); [Fact]
public void should_return_no_service_provider()
var reRoute = new DownstreamReRouteBuilder().Build(); {
var serviceConfig = new ServiceProviderConfigurationBuilder()
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) .Build();
.When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>()) var reRoute = new DownstreamReRouteBuilder().Build();
.BDDfy();
} this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
.When(x => x.WhenIGetTheServiceProvider())
[Fact] .Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>())
public void should_return_list_of_configuration_services() .BDDfy();
{ }
var serviceConfig = new ServiceProviderConfigurationBuilder()
.Build(); [Fact]
public void should_return_list_of_configuration_services()
var downstreamAddresses = new List<DownstreamHostAndPort>() {
{ var serviceConfig = new ServiceProviderConfigurationBuilder()
new DownstreamHostAndPort("asdf.com", 80), .Build();
new DownstreamHostAndPort("abc.com", 80)
}; var downstreamAddresses = new List<DownstreamHostAndPort>()
{
var reRoute = new DownstreamReRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); new DownstreamHostAndPort("asdf.com", 80),
new DownstreamHostAndPort("abc.com", 80)
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) };
.When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>()) var reRoute = new DownstreamReRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build();
.Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses))
.BDDfy(); this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
} .When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>())
[Fact] .Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses))
public void should_return_consul_service_provider() .BDDfy();
{ }
var reRoute = new DownstreamReRouteBuilder()
.WithServiceName("product") [Fact]
.WithUseServiceDiscovery(true) public void should_return_consul_service_provider()
.Build(); {
var reRoute = new DownstreamReRouteBuilder()
var serviceConfig = new ServiceProviderConfigurationBuilder() .WithServiceName("product")
.Build(); .WithUseServiceDiscovery(true)
.Build();
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
.When(x => x.WhenIGetTheServiceProvider()) var serviceConfig = new ServiceProviderConfigurationBuilder()
.Then(x => x.ThenTheServiceProviderIs<ConsulServiceDiscoveryProvider>()) .Build();
.BDDfy();
} this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
.When(x => x.WhenIGetTheServiceProvider())
[Fact] .Then(x => x.ThenTheServiceProviderIs<ConsulServiceDiscoveryProvider>())
public void should_return_service_fabric_provider() .BDDfy();
{ }
var reRoute = new DownstreamReRouteBuilder()
.WithServiceName("product") [Fact]
.WithUseServiceDiscovery(true) public void should_return_polling_consul_service_provider()
.Build(); {
var reRoute = new DownstreamReRouteBuilder()
var serviceConfig = new ServiceProviderConfigurationBuilder() .WithServiceName("product")
.WithType("ServiceFabric") .WithUseServiceDiscovery(true)
.Build(); .Build();
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) var serviceConfig = new ServiceProviderConfigurationBuilder()
.When(x => x.WhenIGetTheServiceProvider()) .WithType("PollConsul")
.Then(x => x.ThenTheServiceProviderIs<ServiceFabricServiceDiscoveryProvider>()) .WithPollingInterval(100000)
.BDDfy(); .Build();
}
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
[Fact] .When(x => x.WhenIGetTheServiceProvider())
public void should_return_eureka_provider() .Then(x => x.ThenTheServiceProviderIs<PollingConsulServiceDiscoveryProvider>())
{ .BDDfy();
var reRoute = new DownstreamReRouteBuilder() }
.WithServiceName("product")
.WithUseServiceDiscovery(true) [Fact]
.Build(); public void should_return_service_fabric_provider()
{
var serviceConfig = new ServiceProviderConfigurationBuilder() var reRoute = new DownstreamReRouteBuilder()
.WithType("Eureka") .WithServiceName("product")
.Build(); .WithUseServiceDiscovery(true)
.Build();
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
.When(x => x.WhenIGetTheServiceProvider()) var serviceConfig = new ServiceProviderConfigurationBuilder()
.Then(x => x.ThenTheServiceProviderIs<EurekaServiceDiscoveryProvider>()) .WithType("ServiceFabric")
.BDDfy(); .Build();
}
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
private void ThenTheFollowingServicesAreReturned(List<DownstreamHostAndPort> downstreamAddresses) .When(x => x.WhenIGetTheServiceProvider())
{ .Then(x => x.ThenTheServiceProviderIs<ServiceFabricServiceDiscoveryProvider>())
var result = (ConfigurationServiceProvider)_result; .BDDfy();
var services = result.Get().Result; }
for (int i = 0; i < services.Count; i++) [Fact]
{ public void should_return_eureka_provider()
var service = services[i]; {
var downstreamAddress = downstreamAddresses[i]; var reRoute = new DownstreamReRouteBuilder()
.WithServiceName("product")
service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); .WithUseServiceDiscovery(true)
service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); .Build();
}
} var serviceConfig = new ServiceProviderConfigurationBuilder()
.WithType("Eureka")
private void GivenTheReRoute(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) .Build();
{
_serviceConfig = serviceConfig; this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
_reRoute = reRoute; .When(x => x.WhenIGetTheServiceProvider())
} .Then(x => x.ThenTheServiceProviderIs<EurekaServiceDiscoveryProvider>())
.BDDfy();
private void WhenIGetTheServiceProvider() }
{
_result = _factory.Get(_serviceConfig, _reRoute); private void ThenTheFollowingServicesAreReturned(List<DownstreamHostAndPort> downstreamAddresses)
} {
var result = (ConfigurationServiceProvider)_result;
private void ThenTheServiceProviderIs<T>() var services = result.Get().Result;
{
_result.ShouldBeOfType<T>(); for (int i = 0; i < services.Count; i++)
} {
} var service = services[i];
} var downstreamAddress = downstreamAddresses[i];
service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host);
service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port);
}
}
private void GivenTheReRoute(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute)
{
_serviceConfig = serviceConfig;
_reRoute = reRoute;
}
private void WhenIGetTheServiceProvider()
{
_result = _factory.Get(_serviceConfig, _reRoute);
}
private void ThenTheServiceProviderIs<T>()
{
_result.ShouldBeOfType<T>();
}
}
}