Feature/should not start if specified using service discovery but no provider registered (#584)

* #580 added failing test

* #580 added failing test for dynamic reroutes

* #580 added failing test for validator

* #580 validation tests passing

* #580 acceptance tests passing

* #580 got rid of the list in sdp factory

* +semver: breaking #580 service discovery provider returned by delegate must be the same as name in config, this is a breaking change because you have to specify consul now

* #580 removed use servide discovery property from file config, we dont need it, just use the service name as indicator the user wants to use service discovery for the given reroute
This commit is contained in:
Tom Pallister 2018-08-31 18:28:43 +01:00 committed by GitHub
parent 0a7d81038e
commit 55277cac45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 821 additions and 255 deletions

View File

@ -64,7 +64,6 @@ Here is an example ReRoute configuration, You don't need to set all of these thi
"UseCookieContainer": true, "UseCookieContainer": true,
"UseTracing": true "UseTracing": true
}, },
"UseServiceDiscovery": false,
"DangerousAcceptAnyServerCertificateValidator": false "DangerousAcceptAnyServerCertificateValidator": false
} }

View File

@ -1,3 +1,223 @@
Ocelot can load balance across available downstream services for each ReRoute. This means you can scale your downstream services and Ocelot can use them effectively.
The type of load balancer available are:
LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot's.
RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot's.
NoLoadBalancer - takes the first available service from config or service discovery.
CookieStickySessions - uses a cookie to stick all requests to a specific server. More info below.
You must choose in your configuration which load balancer to use.
Configuration
^^^^^^^^^^^^^
The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeadConnection load balancer. This is the simplest way to get load balancing set up.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.1.10",
"Port": 5000,
},
{
"Host": "10.0.1.11",
"Port": 5000,
}
],
"UpstreamPathTemplate": "/posts/{postId}",
"LoadBalancerOptions": {
"Type": "LeastConnection"
},
"UpstreamHttpMethod": [ "Put", "Delete" ]
}
Service Discovery
^^^^^^^^^^^^^^^^^
The following shows how to set up a ReRoute using service discovery then select the LeadConnection load balancer.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"UpstreamPathTemplate": "/posts/{postId}",
"UpstreamHttpMethod": [ "Put" ],
"ServiceName": "product",
"LoadBalancerOptions": {
"Type": "LeastConnection"
},
}
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. If you add and remove services from the
service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added.
CookieStickySessions
^^^^^^^^^^^^^^^^^^^^
I've implemented a really basic sticky session type of load balancer. The scenario it is meant to support is you have a bunch of downstream
servers that don't share session state so if you get more than one request for one of these servers then it should go to the same box each
time or the session state might be incorrect for the given user. This feature was requested in `Issue #322 <https://github.com/ThreeMammals/Ocelot/issues/322>`_
though what the user wants is more complicated than just sticky sessions :) anyway I thought this would be a nice feature to have!
In order to set up CookieStickySessions load balancer you need to do something like the following.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.1.10",
"Port": 5000,
},
{
"Host": "10.0.1.11",
"Port": 5000,
}
],
"UpstreamPathTemplate": "/posts/{postId}",
"LoadBalancerOptions": {
"Type": "CookieStickySessions",
"Key": "ASP.NET_SessionId",
"Expiry": 1800000
},
"UpstreamHttpMethod": [ "Put", "Delete" ]
}
The LoadBalancerOptions are Type this needs to be CookieStickySessions, Key this is the key of the cookie you
wish to use for the sticky sessions, Expiry this is how long in milliseconds you want to the session to be stuck for. Remember this
refreshes on every request which is meant to mimick how sessions work usually.
If you have multiple ReRoutes with the same LoadBalancerOptions then all of those ReRoutes will use the same load balancer for there
subsequent requests. This means the sessions will be stuck across ReRoutes.
Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul
and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the
moment but could be changed.
||||||| merged common ancestors
Load Balancer
=============
Ocelot can load balance across available downstream services for each ReRoute. This means you can scale your downstream services and Ocelot can use them effectively.
The type of load balancer available are:
LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot's.
RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot's.
NoLoadBalancer - takes the first available service from config or service discovery.
CookieStickySessions - uses a cookie to stick all requests to a specific server. More info below.
You must choose in your configuration which load balancer to use.
Configuration
^^^^^^^^^^^^^
The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeadConnection load balancer. This is the simplest way to get load balancing set up.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.1.10",
"Port": 5000,
},
{
"Host": "10.0.1.11",
"Port": 5000,
}
],
"UpstreamPathTemplate": "/posts/{postId}",
"LoadBalancerOptions": {
"Type": "LeastConnection"
},
"UpstreamHttpMethod": [ "Put", "Delete" ]
}
Service Discovery
^^^^^^^^^^^^^^^^^
The following shows how to set up a ReRoute using service discovery then select the LeadConnection load balancer.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"UpstreamPathTemplate": "/posts/{postId}",
"UpstreamHttpMethod": [ "Put" ],
"ServiceName": "product",
"LoadBalancerOptions": {
"Type": "LeastConnection"
},
}
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. If you add and remove services from the
service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added.
CookieStickySessions
^^^^^^^^^^^^^^^^^^^^
I've implemented a really basic sticky session type of load balancer. The scenario it is meant to support is you have a bunch of downstream
servers that don't share session state so if you get more than one request for one of these servers then it should go to the same box each
time or the session state might be incorrect for the given user. This feature was requested in `Issue #322 <https://github.com/ThreeMammals/Ocelot/issues/322>`_
though what the user wants is more complicated than just sticky sessions :) anyway I thought this would be a nice feature to have!
In order to set up CookieStickySessions load balancer you need to do something like the following.
.. code-block:: json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.1.10",
"Port": 5000,
},
{
"Host": "10.0.1.11",
"Port": 5000,
}
],
"UpstreamPathTemplate": "/posts/{postId}",
"LoadBalancerOptions": {
"Type": "CookieStickySessions",
"Key": "ASP.NET_SessionId",
"Expiry": 1800000
},
"UpstreamHttpMethod": [ "Put", "Delete" ]
}
The LoadBalancerOptions are Type this needs to be CookieStickySessions, Key this is the key of the cookie you
wish to use for the sticky sessions, Expiry this is how long in milliseconds you want to the session to be stuck for. Remember this
refreshes on every request which is meant to mimick how sessions work usually.
If you have multiple ReRoutes with the same LoadBalancerOptions then all of those ReRoutes will use the same load balancer for there
subsequent requests. This means the sessions will be stuck across ReRoutes.
Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul
and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the
moment but could be changed.
=======
Load Balancer Load Balancer
============= =============
@ -59,7 +279,6 @@ The following shows how to set up a ReRoute using service discovery then select
"LoadBalancerOptions": { "LoadBalancerOptions": {
"Type": "LeastConnection" "Type": "LeastConnection"
}, },
"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. If you add and remove services from the 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. If you add and remove services from the

View File

@ -211,7 +211,6 @@ Ocelot allows you to specify a querystring as part of the DownstreamPathTemplate
} }
], ],
"GlobalConfiguration": { "GlobalConfiguration": {
"UseServiceDiscovery": false
} }
} }
@ -239,7 +238,6 @@ Ocelot will also allow you to put query string parameters in the UpstreamPathTem
} }
], ],
"GlobalConfiguration": { "GlobalConfiguration": {
"UseServiceDiscovery": false
} }
} }

View File

@ -36,7 +36,7 @@ will be used.
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 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
@ -50,7 +50,6 @@ and LeastConnection algorithm you can use. If no load balancer is specified Ocel
"LoadBalancerOptions": { "LoadBalancerOptions": {
"Type": "LeastConnection" "Type": "LeastConnection"
}, },
"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.
@ -105,7 +104,8 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade
"ServiceDiscoveryProvider": { "ServiceDiscoveryProvider": {
"Host": "localhost", "Host": "localhost",
"Port": 8500, "Port": 8500,
"Token": "footoken" "Token": "footoken",
"Type": "Consul"
} }
Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request.
@ -232,6 +232,7 @@ Ocelot also allows you to set DynamicReRoutes which lets you set rate limiting r
"ServiceDiscoveryProvider": { "ServiceDiscoveryProvider": {
"Host": "localhost", "Host": "localhost",
"Port": 8523, "Port": 8523,
"Type": "Consul"
}, },
"RateLimitOptions": { "RateLimitOptions": {
"ClientIdHeader": "ClientId", "ClientIdHeader": "ClientId",

View File

@ -4,7 +4,7 @@ Service Fabric
If you have services deployed in Service Fabric you will normally use the naming service to access them. If you have services deployed in Service Fabric you will normally use the naming service to access them.
The following example shows how to set up a ReRoute that will work in Service Fabric. The most important thing is the ServiceName which is made up of the The following example shows how to set up a ReRoute that will work in Service Fabric. The most important thing is the ServiceName which is made up of the
Service Fabric application name then the specific service name. We also need to set UseServiceDiscovery as true and set up the ServiceDiscoveryProvider in Service Fabric application name then the specific service name. We also need to set up the ServiceDiscoveryProvider in
GlobalConfiguration. The example here shows a typical configuration. It assumes service fabric is running on localhost and that the naming service is on port 19081. GlobalConfiguration. The example here shows a typical configuration. It assumes service fabric is running on localhost and that the naming service is on port 19081.
The example below is taken from the samples folder so please check it if this doesnt make sense! The example below is taken from the samples folder so please check it if this doesnt make sense!
@ -21,7 +21,6 @@ The example below is taken from the samples folder so please check it if this do
], ],
"DownstreamScheme": "http", "DownstreamScheme": "http",
"ServiceName": "OcelotServiceApplication/OcelotApplicationService", "ServiceName": "OcelotServiceApplication/OcelotApplicationService",
"UseServiceDiscovery" : true
} }
], ],
"GlobalConfiguration": { "GlobalConfiguration": {

View File

@ -4,7 +4,6 @@
"DownstreamPathTemplate": "/api/Category", "DownstreamPathTemplate": "/api/Category",
"DownstreamScheme": "http", "DownstreamScheme": "http",
"UpstreamPathTemplate": "/Category", "UpstreamPathTemplate": "/Category",
"UseServiceDiscovery": true,
"ServiceName": "ncore-rat", "ServiceName": "ncore-rat",
"UpstreamHttpMethod": [ "Get" ], "UpstreamHttpMethod": [ "Get" ],
"QoSOptions": { "QoSOptions": {

View File

@ -7,8 +7,7 @@
"Get" "Get"
], ],
"DownstreamScheme": "http", "DownstreamScheme": "http",
"ServiceName": "OcelotServiceApplication/OcelotApplicationService", "ServiceName": "OcelotServiceApplication/OcelotApplicationService"
"UseServiceDiscovery" : true
} }
], ],
"GlobalConfiguration": { "GlobalConfiguration": {

View File

@ -224,6 +224,8 @@ namespace Ocelot.Configuration.Creator
var lbOptions = CreateLoadBalancerOptions(fileReRoute.LoadBalancerOptions); var lbOptions = CreateLoadBalancerOptions(fileReRoute.LoadBalancerOptions);
var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName);
var reRoute = new DownstreamReRouteBuilder() var reRoute = new DownstreamReRouteBuilder()
.WithKey(fileReRoute.Key) .WithKey(fileReRoute.Key)
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
@ -249,7 +251,7 @@ namespace Ocelot.Configuration.Creator
.WithRateLimitOptions(rateLimitOption) .WithRateLimitOptions(rateLimitOption)
.WithHttpHandlerOptions(httpHandlerOptions) .WithHttpHandlerOptions(httpHandlerOptions)
.WithServiceName(fileReRoute.ServiceName) .WithServiceName(fileReRoute.ServiceName)
.WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) .WithUseServiceDiscovery(useServiceDiscovery)
.WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream)
.WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream)
.WithUpstreamHost(fileReRoute.UpstreamHost) .WithUpstreamHost(fileReRoute.UpstreamHost)

View File

@ -43,7 +43,6 @@ namespace Ocelot.Configuration.File
public FileRateLimitRule RateLimitOptions { get; set; } public FileRateLimitRule RateLimitOptions { get; set; }
public FileAuthenticationOptions AuthenticationOptions { get; set; } public FileAuthenticationOptions AuthenticationOptions { get; set; }
public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; }
public bool UseServiceDiscovery { get;set; }
public List<FileHostAndPort> DownstreamHostAndPorts {get;set;} public List<FileHostAndPort> DownstreamHostAndPorts {get;set;}
public string UpstreamHost { get; set; } public string UpstreamHost { get; set; }
public string Key { get;set; } public string Key { get;set; }

View File

@ -11,15 +11,19 @@ namespace Ocelot.Configuration.Validator
{ {
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Ocelot.ServiceDiscovery;
using Requester; using Requester;
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{ {
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates;
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider) public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
{ {
_qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>(); _qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();
_serviceDiscoveryFinderDelegates = provider
.GetServices<ServiceDiscoveryFinderDelegate>()
.ToList();
RuleFor(configuration => configuration.ReRoutes) RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate)); .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate));
@ -31,6 +35,14 @@ namespace Ocelot.Configuration.Validator
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate"); .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate");
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => HaveServiceDiscoveryProviderRegitered(reRoute, config.GlobalConfiguration.ServiceDiscoveryProvider))
.WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?");
RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider)
.Must((config) => HaveServiceDiscoveryProviderRegitered(config))
.WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?");
RuleForEach(configuration => configuration.ReRoutes) RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.Aggregates)) .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.Aggregates))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate"); .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate");
@ -48,6 +60,41 @@ namespace Ocelot.Configuration.Validator
.WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates"); .WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates");
} }
private bool HaveServiceDiscoveryProviderRegitered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider)
{
if (string.IsNullOrEmpty(reRoute.ServiceName))
{
return true;
}
if (serviceDiscoveryProvider?.Type?.ToLower() == "servicefabric")
{
return true;
}
return _serviceDiscoveryFinderDelegates.Any();
}
private bool HaveServiceDiscoveryProviderRegitered(FileServiceDiscoveryProvider serviceDiscoveryProvider)
{
if(serviceDiscoveryProvider == null)
{
return true;
}
if (serviceDiscoveryProvider?.Type?.ToLower() == "servicefabric")
{
return true;
}
if(string.IsNullOrEmpty(serviceDiscoveryProvider.Type))
{
return true;
}
return _serviceDiscoveryFinderDelegates.Any();
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration) public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)
{ {
var validateResult = await ValidateAsync(configuration); var validateResult = await ValidateAsync(configuration);

View File

@ -55,17 +55,12 @@
.MustAsync(IsSupportedAuthenticationProviders) .MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider"); .WithMessage("{PropertyValue} is unsupported authentication provider");
When(reRoute => reRoute.UseServiceDiscovery, () => { When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(r => r.ServiceName).NotEmpty()
.WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!");
});
When(reRoute => !reRoute.UseServiceDiscovery, () => {
RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
.WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!");
}); });
When(reRoute => !reRoute.UseServiceDiscovery, () => { When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(reRoute => reRoute.DownstreamHostAndPorts) RuleFor(reRoute => reRoute.DownstreamHostAndPorts)
.SetCollectionValidator(new HostAndPortValidator()); .SetCollectionValidator(new HostAndPortValidator());
}); });

View File

@ -122,7 +122,7 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
private static bool ServiceFabricRequest(DownstreamContext context) private static bool ServiceFabricRequest(DownstreamContext context)
{ {
return context.Configuration.ServiceProviderConfiguration.Type == "ServiceFabric" && context.DownstreamReRoute.UseServiceDiscovery; return context.Configuration.ServiceProviderConfiguration.Type?.ToLower() == "servicefabric" && context.DownstreamReRoute.UseServiceDiscovery;
} }
} }
} }

View File

@ -1,10 +1,11 @@
using System.Threading.Tasks; namespace Ocelot.LoadBalancer.LoadBalancers
using Ocelot.Configuration;
namespace Ocelot.LoadBalancer.LoadBalancers
{ {
using System.Threading.Tasks;
using Ocelot.Configuration;
using Ocelot.Responses;
public interface ILoadBalancerFactory public interface ILoadBalancerFactory
{ {
Task<ILoadBalancer> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); Task<Response<ILoadBalancer>> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config);
} }
} }

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Infrastructure; using Ocelot.Infrastructure;
using Ocelot.Responses;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
namespace Ocelot.LoadBalancer.LoadBalancers namespace Ocelot.LoadBalancer.LoadBalancers
@ -14,22 +15,29 @@ namespace Ocelot.LoadBalancer.LoadBalancers
_serviceProviderFactory = serviceProviderFactory; _serviceProviderFactory = serviceProviderFactory;
} }
public async Task<ILoadBalancer> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) public async Task<Response<ILoadBalancer>> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config)
{ {
var serviceProvider = _serviceProviderFactory.Get(config, reRoute); var response = _serviceProviderFactory.Get(config, reRoute);
if(response.IsError)
{
return new ErrorResponse<ILoadBalancer>(response.Errors);
}
var serviceProvider = response.Data;
switch (reRoute.LoadBalancerOptions?.Type) switch (reRoute.LoadBalancerOptions?.Type)
{ {
case nameof(RoundRobin): case nameof(RoundRobin):
return new RoundRobin(async () => await serviceProvider.Get()); return new OkResponse<ILoadBalancer>(new RoundRobin(async () => await serviceProvider.Get()));
case nameof(LeastConnection): case nameof(LeastConnection):
return new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName); return new OkResponse<ILoadBalancer>(new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName));
case nameof(CookieStickySessions): case nameof(CookieStickySessions):
var loadBalancer = new RoundRobin(async () => await serviceProvider.Get()); var loadBalancer = new RoundRobin(async () => await serviceProvider.Get());
var bus = new InMemoryBus<StickySession>(); var bus = new InMemoryBus<StickySession>();
return new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs, bus); return new OkResponse<ILoadBalancer>(new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs, bus));
default: default:
return new NoLoadBalancer(async () => await serviceProvider.Get()); return new OkResponse<ILoadBalancer>(new NoLoadBalancer(async () => await serviceProvider.Get()));
} }
} }
} }

View File

@ -22,24 +22,36 @@ namespace Ocelot.LoadBalancer.LoadBalancers
{ {
try try
{ {
if(_loadBalancers.TryGetValue(reRoute.LoadBalancerKey, out var loadBalancer)) Response<ILoadBalancer> result;
if (_loadBalancers.TryGetValue(reRoute.LoadBalancerKey, out var loadBalancer))
{ {
loadBalancer = _loadBalancers[reRoute.LoadBalancerKey]; loadBalancer = _loadBalancers[reRoute.LoadBalancerKey];
if(reRoute.LoadBalancerOptions.Type != loadBalancer.GetType().Name) if (reRoute.LoadBalancerOptions.Type != loadBalancer.GetType().Name)
{ {
loadBalancer = await _factory.Get(reRoute, config); result = await _factory.Get(reRoute, config);
if (result.IsError)
{
return new ErrorResponse<ILoadBalancer>(result.Errors);
}
loadBalancer = result.Data;
AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer);
} }
return new OkResponse<ILoadBalancer>(loadBalancer); return new OkResponse<ILoadBalancer>(loadBalancer);
} }
loadBalancer = await _factory.Get(reRoute, config); result = await _factory.Get(reRoute, config);
if (result.IsError)
{
return new ErrorResponse<ILoadBalancer>(result.Errors);
}
loadBalancer = result.Data;
AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer);
return new OkResponse<ILoadBalancer>(loadBalancer); return new OkResponse<ILoadBalancer>(loadBalancer);
} }
catch(Exception ex) catch (Exception ex)
{ {
return new ErrorResponse<ILoadBalancer>(new List<Ocelot.Errors.Error>() return new ErrorResponse<ILoadBalancer>(new List<Ocelot.Errors.Error>()
{ {

View File

@ -1,10 +1,11 @@
using Ocelot.Configuration;
using Ocelot.ServiceDiscovery.Providers;
namespace Ocelot.ServiceDiscovery namespace Ocelot.ServiceDiscovery
{ {
using Ocelot.Configuration;
using Ocelot.Responses;
using Ocelot.ServiceDiscovery.Providers;
public interface IServiceDiscoveryProviderFactory public interface IServiceDiscoveryProviderFactory
{ {
IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute); Response<IServiceDiscoveryProvider> Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute);
} }
} }

View File

@ -9,23 +9,22 @@ namespace Ocelot.ServiceDiscovery
using System; using System;
using System.Linq; using System.Linq;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Ocelot.Responses;
public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory
{ {
private readonly IOcelotLoggerFactory _factory; private readonly IOcelotLoggerFactory _factory;
private readonly List<ServiceDiscoveryFinderDelegate> _delegates; private readonly ServiceDiscoveryFinderDelegate _delegates;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider)
{ {
_factory = factory; _factory = factory;
_provider = provider; _provider = provider;
_delegates = provider _delegates = provider.GetService<ServiceDiscoveryFinderDelegate>();
.GetServices<ServiceDiscoveryFinderDelegate>()
.ToList();
} }
public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) public Response<IServiceDiscoveryProvider> Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute)
{ {
if (reRoute.UseServiceDiscovery) if (reRoute.UseServiceDiscovery)
{ {
@ -41,27 +40,28 @@ namespace Ocelot.ServiceDiscovery
services.Add(service); services.Add(service);
} }
return new ConfigurationServiceProvider(services); return new OkResponse<IServiceDiscoveryProvider>(new ConfigurationServiceProvider(services));
} }
private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration config, string key) private Response<IServiceDiscoveryProvider> GetServiceDiscoveryProvider(ServiceProviderConfiguration config, string key)
{ {
if (config.Type?.ToLower() == "servicefabric") if (config.Type?.ToLower() == "servicefabric")
{ {
var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, key); var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, key);
return new ServiceFabricServiceDiscoveryProvider(sfConfig); return new OkResponse<IServiceDiscoveryProvider>(new ServiceFabricServiceDiscoveryProvider(sfConfig));
} }
foreach (var serviceDiscoveryFinderDelegate in _delegates) if (_delegates != null)
{ {
var provider = serviceDiscoveryFinderDelegate?.Invoke(_provider, config, key); var provider = _delegates?.Invoke(_provider, config, key);
if (provider != null)
if(provider.GetType().Name.ToLower() == config.Type.ToLower())
{ {
return provider; return new OkResponse<IServiceDiscoveryProvider>(provider);
} }
} }
return null; return new ErrorResponse<IServiceDiscoveryProvider>(new UnableToFindServiceDiscoveryProviderError($"Unable to find service discovery provider for type: {config.Type}"));
} }
} }
} }

View File

@ -0,0 +1,11 @@
namespace Ocelot.ServiceDiscovery
{
using Ocelot.Errors;
public class UnableToFindServiceDiscoveryProviderError : Error
{
public UnableToFindServiceDiscoveryProviderError(string message) : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError)
{
}
}
}

View File

@ -15,6 +15,79 @@ namespace Ocelot.AcceptanceTests
_steps = new Steps(); _steps = new Steps();
} }
[Fact]
public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered_with_dynamic_re_routes()
{
var invalidConfig = new FileConfiguration
{
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "consul",
Port = 8500
}
}
};
Exception exception = null;
_steps.GivenThereIsAConfiguration(invalidConfig);
try
{
_steps.GivenOcelotIsRunning();
}
catch (Exception ex)
{
exception = ex;
}
exception.ShouldNotBeNull();
exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)");
}
[Fact]
public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered()
{
var invalidConfig = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
ServiceName = "test"
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "consul",
Port = 8500
}
}
};
Exception exception = null;
_steps.GivenThereIsAConfiguration(invalidConfig);
try
{
_steps.GivenOcelotIsRunning();
}
catch (Exception ex)
{
exception = ex;
}
exception.ShouldNotBeNull();
exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?,Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)");
}
[Fact] [Fact]
public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally() public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally()
{ {

View File

@ -336,7 +336,6 @@ namespace Ocelot.AcceptanceTests
}, },
UpstreamPathTemplate = "/vacancy/", UpstreamPathTemplate = "/vacancy/",
UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" }, UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" },
ServiceName = "botCore",
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }
}, },
new FileReRoute new FileReRoute
@ -353,7 +352,6 @@ namespace Ocelot.AcceptanceTests
}, },
UpstreamPathTemplate = "/vacancy/{vacancyId}", UpstreamPathTemplate = "/vacancy/{vacancyId}",
UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" }, UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" },
ServiceName = "botCore",
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }
} }
} }
@ -828,7 +826,6 @@ namespace Ocelot.AcceptanceTests
}, },
UpstreamPathTemplate = "/vacancy/", UpstreamPathTemplate = "/vacancy/",
UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" }, UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" },
ServiceName = "botCore",
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }
}, },
new FileReRoute new FileReRoute
@ -845,7 +842,6 @@ namespace Ocelot.AcceptanceTests
}, },
UpstreamPathTemplate = "/vacancy/{vacancyId}", UpstreamPathTemplate = "/vacancy/{vacancyId}",
UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" }, UpstreamHttpMethod = new List<string> { "Options", "Put", "Get", "Post", "Delete" },
ServiceName = "botCore",
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }
} }
} }

View File

@ -33,7 +33,6 @@ namespace Ocelot.AcceptanceTests
DownstreamScheme = "http", DownstreamScheme = "http",
UpstreamPathTemplate = "/{everything}", UpstreamPathTemplate = "/{everything}",
UpstreamHttpMethod = new List<string> { "Get" }, UpstreamHttpMethod = new List<string> { "Get" },
UseServiceDiscovery = true,
ServiceName = "OcelotServiceApplication/OcelotApplicationService" ServiceName = "OcelotServiceApplication/OcelotApplicationService"
} }
}, },
@ -70,7 +69,6 @@ namespace Ocelot.AcceptanceTests
DownstreamScheme = "http", DownstreamScheme = "http",
UpstreamPathTemplate = "/EquipmentInterfaces", UpstreamPathTemplate = "/EquipmentInterfaces",
UpstreamHttpMethod = new List<string> { "Get" }, UpstreamHttpMethod = new List<string> { "Get" },
UseServiceDiscovery = true,
ServiceName = "OcelotServiceApplication/OcelotApplicationService" ServiceName = "OcelotServiceApplication/OcelotApplicationService"
} }
}, },
@ -107,7 +105,6 @@ namespace Ocelot.AcceptanceTests
DownstreamScheme = "http", DownstreamScheme = "http",
UpstreamPathTemplate = "/EquipmentInterfaces", UpstreamPathTemplate = "/EquipmentInterfaces",
UpstreamHttpMethod = new List<string> { "Get" }, UpstreamHttpMethod = new List<string> { "Get" },
UseServiceDiscovery = true,
ServiceName = "OcelotServiceApplication/OcelotApplicationService" ServiceName = "OcelotServiceApplication/OcelotApplicationService"
} }
}, },

View File

@ -17,6 +17,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Ocelot.Requester; using Ocelot.Requester;
using Requester; using Requester;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using Ocelot.ServiceDiscovery;
public class FileConfigurationFluentValidatorTests public class FileConfigurationFluentValidatorTests
{ {
@ -33,6 +36,191 @@
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider);
} }
[Fact]
public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
ServiceName = "test"
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "ServiceFabric",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
ServiceName = "test"
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "FakeServiceDiscoveryProvider",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.And(x => x.GivenAServiceDiscoveryHandler())
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler()
{
var configuration = new FileConfiguration
{
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "FakeServiceDiscoveryProvider",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.And(x => x.GivenAServiceDiscoveryHandler())
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
ServiceName = "test"
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "FakeServiceDiscoveryProvider",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"))
.BDDfy();
}
[Fact]
public void configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler()
{
var configuration = new FileConfiguration
{
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "FakeServiceDiscoveryProvider",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"))
.BDDfy();
}
[Fact]
public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
ServiceName = "test"
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Host = "localhost",
Type = "consul",
Port = 8500
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.And(x => x.GivenAServiceDiscoveryHandler())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"))
.BDDfy();
}
[Fact] [Fact]
public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler() public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler()
{ {
@ -151,7 +339,7 @@
.BDDfy(); .BDDfy();
} }
[Fact] [Fact]
public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler()
{ {
var configuration = new FileConfiguration var configuration = new FileConfiguration
@ -988,31 +1176,6 @@
.BDDfy(); .BDDfy();
} }
[Theory]
[InlineData(null)]
[InlineData("")]
public void configuration_is_invalid_with_using_service_discovery_and_no_service_name(string serviceName)
{
this.Given(x => x.GivenAConfiguration(new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = true,
ServiceName = serviceName
}
}
}))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!"))
.BDDfy();
}
[Fact] [Fact]
public void configuration_is_valid_with_using_service_discovery_and_service_name() public void configuration_is_valid_with_using_service_discovery_and_service_name()
{ {
@ -1025,9 +1188,17 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = true,
ServiceName = "Test" ServiceName = "Test"
} }
},
GlobalConfiguration = new FileGlobalConfiguration
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{
Type = "servicefabric",
Host = "localhost",
Port = 1234
}
} }
})) }))
.When(x => x.WhenIValidateTheConfiguration()) .When(x => x.WhenIValidateTheConfiguration())
@ -1049,7 +1220,6 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = false,
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort new FileHostAndPort
@ -1078,7 +1248,6 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = false,
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort new FileHostAndPort
@ -1106,7 +1275,6 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = false,
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort new FileHostAndPort
@ -1134,7 +1302,6 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = false,
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
} }
@ -1159,7 +1326,6 @@
DownstreamPathTemplate = "/api/products/", DownstreamPathTemplate = "/api/products/",
UpstreamPathTemplate = "/asdf/", UpstreamPathTemplate = "/asdf/",
UpstreamHttpMethod = new List<string> {"Get"}, UpstreamHttpMethod = new List<string> {"Get"},
UseServiceDiscovery = false,
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort() new FileHostAndPort()
@ -1220,6 +1386,23 @@
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider);
} }
private void GivenAServiceDiscoveryHandler()
{
var collection = new ServiceCollection();
ServiceDiscoveryFinderDelegate del = (a,b,c) => new FakeServiceDiscoveryProvider();
collection.AddSingleton<ServiceDiscoveryFinderDelegate>(del);
var provider = collection.BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider);
}
private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider
{
public Task<List<Service>> Get()
{
throw new System.NotImplementedException();
}
}
private class TestOptions : AuthenticationSchemeOptions private class TestOptions : AuthenticationSchemeOptions
{ {
} }

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using Ocelot.ServiceDiscovery.Providers; using Ocelot.ServiceDiscovery.Providers;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
using Ocelot.Responses;
namespace Ocelot.UnitTests.LoadBalancer namespace Ocelot.UnitTests.LoadBalancer
{ {
@ -15,7 +16,7 @@ namespace Ocelot.UnitTests.LoadBalancer
{ {
private DownstreamReRoute _reRoute; private DownstreamReRoute _reRoute;
private readonly LoadBalancerFactory _factory; private readonly LoadBalancerFactory _factory;
private ILoadBalancer _result; private Response<ILoadBalancer> _result;
private readonly Mock<IServiceDiscoveryProviderFactory> _serviceProviderFactory; private readonly Mock<IServiceDiscoveryProviderFactory> _serviceProviderFactory;
private readonly Mock<IServiceDiscoveryProvider> _serviceProvider; private readonly Mock<IServiceDiscoveryProvider> _serviceProvider;
private ServiceProviderConfiguration _serviceProviderConfig; private ServiceProviderConfiguration _serviceProviderConfig;
@ -115,7 +116,7 @@ namespace Ocelot.UnitTests.LoadBalancer
{ {
_serviceProviderFactory _serviceProviderFactory
.Setup(x => x.Get(It.IsAny<ServiceProviderConfiguration>(), It.IsAny<DownstreamReRoute>())) .Setup(x => x.Get(It.IsAny<ServiceProviderConfiguration>(), It.IsAny<DownstreamReRoute>()))
.Returns(_serviceProvider.Object); .Returns(new OkResponse<IServiceDiscoveryProvider>(_serviceProvider.Object));
} }
private void ThenTheServiceProviderIsCalledCorrectly() private void ThenTheServiceProviderIsCalledCorrectly()
@ -136,7 +137,7 @@ namespace Ocelot.UnitTests.LoadBalancer
private void ThenTheLoadBalancerIsReturned<T>() private void ThenTheLoadBalancerIsReturned<T>()
{ {
_result.ShouldBeOfType<T>(); _result.Data.ShouldBeOfType<T>();
} }
} }
} }

View File

@ -111,7 +111,7 @@ namespace Ocelot.UnitTests.LoadBalancer
private void WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamReRoute reRoute) private void WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamReRoute reRoute)
{ {
_reRoute = reRoute; _reRoute = reRoute;
_factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new LeastConnection(null, null)); _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new OkResponse<ILoadBalancer>(new LeastConnection(null, null)));
_getResult = _loadBalancerHouse.Get(_reRoute, _serviceProviderConfig).Result; _getResult = _loadBalancerHouse.Get(_reRoute, _serviceProviderConfig).Result;
} }
@ -138,7 +138,7 @@ namespace Ocelot.UnitTests.LoadBalancer
{ {
_reRoute = reRoute; _reRoute = reRoute;
_loadBalancer = loadBalancer; _loadBalancer = loadBalancer;
_factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(loadBalancer); _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new OkResponse<ILoadBalancer>(loadBalancer));
_getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result;
} }

View File

@ -14,11 +14,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
using Ocelot.Responses;
public class ServiceProviderFactoryTests public class ServiceDiscoveryProviderFactoryTests
{ {
private ServiceProviderConfiguration _serviceConfig; private ServiceProviderConfiguration _serviceConfig;
private IServiceDiscoveryProvider _result; private Response<IServiceDiscoveryProvider> _result;
private ServiceDiscoveryProviderFactory _factory; private ServiceDiscoveryProviderFactory _factory;
private DownstreamReRoute _reRoute; private DownstreamReRoute _reRoute;
private readonly Mock<IOcelotLoggerFactory> _loggerFactory; private readonly Mock<IOcelotLoggerFactory> _loggerFactory;
@ -26,7 +27,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery
private IServiceProvider _provider; private IServiceProvider _provider;
private readonly IServiceCollection _collection; private readonly IServiceCollection _collection;
public ServiceProviderFactoryTests() public ServiceDiscoveryProviderFactoryTests()
{ {
_loggerFactory = new Mock<IOcelotLoggerFactory>(); _loggerFactory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>(); _logger = new Mock<IOcelotLogger>();
@ -71,7 +72,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery
} }
[Fact] [Fact]
public void should_call_delegate() public void should_return_provider_because_type_matches_reflected_type_from_delegate()
{ {
var reRoute = new DownstreamReRouteBuilder() var reRoute = new DownstreamReRouteBuilder()
.WithServiceName("product") .WithServiceName("product")
@ -79,6 +80,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery
.Build(); .Build();
var serviceConfig = new ServiceProviderConfigurationBuilder() var serviceConfig = new ServiceProviderConfigurationBuilder()
.WithType(nameof(Fake))
.Build(); .Build();
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
@ -88,6 +90,25 @@ namespace Ocelot.UnitTests.ServiceDiscovery
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_not_return_provider_because_type_doesnt_match_reflected_type_from_delegate()
{
var reRoute = new DownstreamReRouteBuilder()
.WithServiceName("product")
.WithUseServiceDiscovery(true)
.Build();
var serviceConfig = new ServiceProviderConfigurationBuilder()
.WithType("Wookie")
.Build();
this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute))
.And(x => GivenAFakeDelegate())
.When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheResultIsError())
.BDDfy();
}
[Fact] [Fact]
public void should_return_service_fabric_provider() public void should_return_service_fabric_provider()
{ {
@ -124,12 +145,17 @@ namespace Ocelot.UnitTests.ServiceDiscovery
private void ThenTheDelegateIsCalled() private void ThenTheDelegateIsCalled()
{ {
_result.GetType().Name.ShouldBe("Fake"); _result.Data.GetType().Name.ShouldBe("Fake");
}
private void ThenTheResultIsError()
{
_result.IsError.ShouldBeTrue();
} }
private void ThenTheFollowingServicesAreReturned(List<DownstreamHostAndPort> downstreamAddresses) private void ThenTheFollowingServicesAreReturned(List<DownstreamHostAndPort> downstreamAddresses)
{ {
var result = (ConfigurationServiceProvider)_result; var result = (ConfigurationServiceProvider)_result.Data;
var services = result.Get().Result; var services = result.Get().Result;
for (int i = 0; i < services.Count; i++) for (int i = 0; i < services.Count; i++)
@ -155,7 +181,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery
private void ThenTheServiceProviderIs<T>() private void ThenTheServiceProviderIs<T>()
{ {
_result.ShouldBeOfType<T>(); _result.Data.ShouldBeOfType<T>();
} }
} }
} }