diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index e32e7cd0e..504ecbfb9 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -3,29 +3,45 @@ Service Discovery ================= -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 -GlobalConfiguration section which means the same service discovery provider will be used for all Routes you specify a ServiceName for at Route level. +Ocelot allows you to specify a *service discovery* provider and will use this to find the host and port for the downstream service to which Ocelot forwards the request. +At the moment this is only supported in the **GlobalConfiguration** section, which means the same *service discovery* provider will be used for all Routes for which you specify a ``ServiceName`` at Route level. Consul ------ -The first thing you need to do is install the NuGet package that provides Consul support in Ocelot. + | **Namespace**: `Ocelot.Provider.Consul `_ + +The first thing you need to do is install `the NuGet package `_ that provides `Consul `_ support in Ocelot. .. code-block:: powershell Install-Package Ocelot.Provider.Consul -Then add the following to your ConfigureServices method. +Then add the following to your ``ConfigureServices`` method: .. code-block:: csharp - s.AddOcelot() - .AddConsul(); + ConfigureServices(services => + { + services.AddOcelot() + .AddConsul(); + }); + +Currently there are 2 types of Consul *service discovery* providers: ``Consul`` and ``PollConsul``. +The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, then a ``Consul`` provider instance is created by the factory. + +Explore these types of providers and understand the differences in the subsections below. + +Consul Provider Type +^^^^^^^^^^^^^^^^^^^^ + + | **Class**: `Ocelot.Provider.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 -will be used. +The following is required in the `GlobalConfiguration `_. +The **ServiceDiscoveryProvider** property is required, and if you do not specify a host and port, the Consul default ones will be used. -Please note the Scheme option defaults to HTTP. It was added in this `PR `_. It defaults to HTTP to not introduce a breaking change. +Please note the `Scheme `_ option defaults to HTTP. +It was added in `PR 1154 `_. It defaults to HTTP to not introduce a breaking change. .. code-block:: json @@ -38,7 +54,10 @@ Please note the Scheme option defaults to HTTP. It was added in this `PR `_ +and `LeastConnection `_ algorithm you can use. +If no load balancer is specified Ocelot will not load balance requests. .. code-block:: json @@ -53,9 +72,15 @@ In order to tell Ocelot a Route is to use the service discovery provider for its }, } -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 discovery* provider and load balance requests across any available services. -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. +PollConsul Provider Type +^^^^^^^^^^^^^^^^^^^^^^^^ + + | **Class**: `Ocelot.Provider.Consul.PollConsul `_ + +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 @@ -68,11 +93,19 @@ A lot of people have asked me to implement a feature where Ocelot polls Consul f The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. -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 volatile 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. +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 volatile 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. -Your services need to be added to Consul something like below (C# style but hopefully this make sense)...The only important thing to note is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme in address. After reading `this `_ I don't think the scheme should be in there. +Service Definition +^^^^^^^^^^^^^^^^^^ -.. code-block: csharp +Your services need to be added to Consul something like below (C# style but hopefully this make sense)... +The only important thing to note is not to add ``http`` or ``https`` to the Address field. +I have been contacted before about not accepting scheme in Address and accepting scheme in address. +After reading `this `_ I don't think the scheme should be in there. + +In C# + +.. code-block:: csharp new AgentService() { @@ -82,21 +115,22 @@ Your services need to be added to Consul something like below (C# style but hope ID = "some-id", } -Or +Or, in JSON .. code-block:: json - "Service": { - "ID": "some-id", - "Service": "some-service-name", - "Address": "localhost", - "Port": 8080 - } + "Service": { + "ID": "some-id", + "Service": "some-service-name", + "Address": "localhost", + "Port": 8080 + } ACL Token ^^^^^^^^^ -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. +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: .. code-block:: json @@ -248,7 +282,7 @@ This configuration means that if you have a request come into Ocelot on /product Please take a look through all of the docs to understand these options. Custom Providers ----------------------------------- +---------------- Ocelot also allows you to create your own ServiceDiscovery implementation. This is done by implementing the ``IServiceDiscoveryProvider`` interface, as shown in the following example: diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index aee165d41..ecb61b2ab 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,86 +1,78 @@ -using Consul; -using Ocelot.Infrastructure.Extensions; +using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; -namespace Ocelot.Provider.Consul -{ - public class Consul : IServiceDiscoveryProvider - { - private readonly ConsulRegistryConfiguration _config; - private readonly IOcelotLogger _logger; - private readonly IConsulClient _consul; - private const string VersionPrefix = "version-"; +namespace Ocelot.Provider.Consul; - public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) - { - _config = config; - _logger = factory.CreateLogger(); - _consul = clientFactory.Get(_config); - } +public class Consul : IServiceDiscoveryProvider +{ + private const string VersionPrefix = "version-"; + private readonly ConsulRegistryConfiguration _config; + private readonly IConsulClient _consul; + private readonly IOcelotLogger _logger; - public async Task> Get() - { - var consulAddress = (_consul as ConsulClient)?.Config.Address; - _logger.LogDebug($"Querying Consul {consulAddress} about a service: {_config.KeyOfServiceInConsul}"); + public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + { + _logger = factory.CreateLogger(); + _config = config; + _consul = clientFactory.Get(_config); + } - var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + public async Task> Get() + { + var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); - var services = new List(); + var services = new List(); - foreach (var serviceEntry in queryResult.Response) + foreach (var serviceEntry in queryResult.Response) + { + if (IsValid(serviceEntry)) { - var address = serviceEntry.Service.Address; - var port = serviceEntry.Service.Port; - - if (IsValid(serviceEntry)) + var nodes = await _consul.Catalog.Nodes(); + if (nodes.Response == null) { - var nodes = await _consul.Catalog.Nodes(); - if (nodes.Response == null) - { - services.Add(BuildService(serviceEntry, null)); - } - else - { - var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == address); - services.Add(BuildService(serviceEntry, serviceNode)); - } - - _logger.LogDebug($"Consul answer: Address: {address}, Port: {port}"); + services.Add(BuildService(serviceEntry, null)); } else { - _logger.LogWarning($"Unable to use service Address: {address} and Port: {port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == serviceEntry.Service.Address); + services.Add(BuildService(serviceEntry, serviceNode)); } } - - return services.ToList(); + else + { + _logger.LogWarning( + $"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + } } - private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) - { - return new Service( - serviceEntry.Service.Service, - new ServiceHostAndPort(serviceNode == null ? serviceEntry.Service.Address : serviceNode.Name, serviceEntry.Service.Port), - serviceEntry.Service.ID, - GetVersionFromStrings(serviceEntry.Service.Tags), - serviceEntry.Service.Tags ?? Enumerable.Empty()); - } + return services.ToList(); + } - private static bool IsValid(ServiceEntry serviceEntry) - { - if (string.IsNullOrEmpty(serviceEntry.Service.Address) || serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) - { - return false; - } + private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) + { + return new Service( + serviceEntry.Service.Service, + new ServiceHostAndPort(serviceNode == null ? serviceEntry.Service.Address : serviceNode.Name, + serviceEntry.Service.Port), + serviceEntry.Service.ID, + GetVersionFromStrings(serviceEntry.Service.Tags), + serviceEntry.Service.Tags ?? Enumerable.Empty()); + } - return true; - } + private static bool IsValid(ServiceEntry serviceEntry) + { + return !string.IsNullOrEmpty(serviceEntry.Service.Address) + && !serviceEntry.Service.Address.Contains("http://") + && !serviceEntry.Service.Address.Contains("https://") + && serviceEntry.Service.Port > 0; + } - private static string GetVersionFromStrings(IEnumerable strings) - => strings? - .FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); + private static string GetVersionFromStrings(IEnumerable strings) + { + return strings + ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); } } diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs index 583d81d78..5f5009d9b 100644 --- a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -1,20 +1,14 @@ -using Consul; +namespace Ocelot.Provider.Consul; -namespace Ocelot.Provider.Consul +public class ConsulClientFactory : IConsulClientFactory { - public class ConsulClientFactory : IConsulClientFactory + public IConsulClient Get(ConsulRegistryConfiguration config) { - public IConsulClient Get(ConsulRegistryConfiguration config) + return new ConsulClient(c => { - return new ConsulClient(c => - { - c.Address = new Uri($"{config.Scheme}://{config.Host}:{config.Port}"); + c.Address = new Uri($"{config.Scheme}://{config.Host}:{config.Port}"); - if (!string.IsNullOrEmpty(config?.Token)) - { - c.Token = config.Token; - } - }); - } + if (!string.IsNullOrEmpty(config?.Token)) c.Token = config.Token; + }); } } diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index 353b217ae..7ac7612aa 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -1,86 +1,87 @@ -using Consul; +using System.Text; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Ocelot.Cache; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; using Ocelot.Responses; -using System.Text; -namespace Ocelot.Provider.Consul +namespace Ocelot.Provider.Consul; + +public class ConsulFileConfigurationRepository : IFileConfigurationRepository { - public class ConsulFileConfigurationRepository : IFileConfigurationRepository + private readonly IOcelotCache _cache; + private readonly string _configurationKey; + private readonly IConsulClient _consul; + private readonly IOcelotLogger _logger; + + public ConsulFileConfigurationRepository( + IOptions fileConfiguration, + IOcelotCache cache, + IConsulClientFactory factory, + IOcelotLoggerFactory loggerFactory) { - private readonly IConsulClient _consul; - private readonly string _configurationKey; - private readonly Cache.IOcelotCache _cache; - private readonly IOcelotLogger _logger; - - public ConsulFileConfigurationRepository( - IOptions fileConfiguration, - Cache.IOcelotCache cache, - IConsulClientFactory factory, - IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _cache = cache; + _logger = loggerFactory.CreateLogger(); + _cache = cache; - var serviceDiscoveryProvider = fileConfiguration.Value.GlobalConfiguration.ServiceDiscoveryProvider; - _configurationKey = string.IsNullOrWhiteSpace(serviceDiscoveryProvider.ConfigurationKey) ? "InternalConfiguration" : - serviceDiscoveryProvider.ConfigurationKey; + var serviceDiscoveryProvider = fileConfiguration.Value.GlobalConfiguration.ServiceDiscoveryProvider; + _configurationKey = string.IsNullOrWhiteSpace(serviceDiscoveryProvider.ConfigurationKey) + ? "InternalConfiguration" + : serviceDiscoveryProvider.ConfigurationKey; - var config = new ConsulRegistryConfiguration(serviceDiscoveryProvider.Scheme, serviceDiscoveryProvider.Host, - serviceDiscoveryProvider.Port, _configurationKey, serviceDiscoveryProvider.Token); + var config = new ConsulRegistryConfiguration(serviceDiscoveryProvider.Scheme, serviceDiscoveryProvider.Host, + serviceDiscoveryProvider.Port, _configurationKey, serviceDiscoveryProvider.Token); - _consul = factory.Get(config); - } - - public async Task> Get() - { - var config = _cache.Get(_configurationKey, _configurationKey); + _consul = factory.Get(config); + } - if (config != null) - { - return new OkResponse(config); - } + public async Task> Get() + { + var config = _cache.Get(_configurationKey, _configurationKey); - var queryResult = await _consul.KV.Get(_configurationKey); + if (config != null) + { + return new OkResponse(config); + } - if (queryResult.Response == null) - { - return new OkResponse(null); - } + var queryResult = await _consul.KV.Get(_configurationKey); - var bytes = queryResult.Response.Value; + if (queryResult.Response == null) + { + return new OkResponse(null); + } - var json = Encoding.UTF8.GetString(bytes); + var bytes = queryResult.Response.Value; - var consulConfig = JsonConvert.DeserializeObject(json); + var json = Encoding.UTF8.GetString(bytes); - return new OkResponse(consulConfig); - } + var consulConfig = JsonConvert.DeserializeObject(json); - public async Task Set(FileConfiguration ocelotConfiguration) - { - var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); + return new OkResponse(consulConfig); + } - var bytes = Encoding.UTF8.GetBytes(json); + public async Task Set(FileConfiguration ocelotConfiguration) + { + var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); - var kvPair = new KVPair(_configurationKey) - { - Value = bytes, - }; + var bytes = Encoding.UTF8.GetBytes(json); - var result = await _consul.KV.Put(kvPair); + var kvPair = new KVPair(_configurationKey) + { + Value = bytes, + }; - if (result.Response) - { - _cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(3), _configurationKey); + var result = await _consul.KV.Put(kvPair); - return new OkResponse(); - } + if (result.Response) + { + _cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(3), _configurationKey); - return new ErrorResponse(new UnableToSetConfigInConsulError($"Unable to set FileConfiguration in consul, response status code from consul was {result.StatusCode}")); + return new OkResponse(); } + + return new ErrorResponse(new UnableToSetConfigInConsulError( + $"Unable to set FileConfiguration in consul, response status code from consul was {result.StatusCode}")); } } diff --git a/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs index b7cb22b88..f6b644199 100644 --- a/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs +++ b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs @@ -7,84 +7,84 @@ using Ocelot.Middleware; using Ocelot.Responses; -namespace Ocelot.Provider.Consul +namespace Ocelot.Provider.Consul; + +public static class ConsulMiddlewareConfigurationProvider { - public static class ConsulMiddlewareConfigurationProvider + public static OcelotMiddlewareConfigurationDelegate Get = async builder => { - public static OcelotMiddlewareConfigurationDelegate Get = async builder => + var fileConfigRepo = builder.ApplicationServices.GetService(); + var fileConfig = builder.ApplicationServices.GetService>(); + var internalConfigCreator = builder.ApplicationServices.GetService(); + var internalConfigRepo = builder.ApplicationServices.GetService(); + + if (UsingConsul(fileConfigRepo)) { - var fileConfigRepo = builder.ApplicationServices.GetService(); - var fileConfig = builder.ApplicationServices.GetService>(); - var internalConfigCreator = builder.ApplicationServices.GetService(); - var internalConfigRepo = builder.ApplicationServices.GetService(); + await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo); + } + }; - if (UsingConsul(fileConfigRepo)) - { - await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo); - } - }; + private static bool UsingConsul(IFileConfigurationRepository fileConfigRepo) + { + return fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository); + } + + private static async Task SetFileConfigInConsul(IApplicationBuilder builder, + IFileConfigurationRepository fileConfigRepo, IOptionsMonitor fileConfig, + IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo) + { + // get the config from consul. + var fileConfigFromConsul = await fileConfigRepo.Get(); - private static bool UsingConsul(IFileConfigurationRepository fileConfigRepo) + if (IsError(fileConfigFromConsul)) { - return fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository); + ThrowToStopOcelotStarting(fileConfigFromConsul); } - - private static async Task SetFileConfigInConsul(IApplicationBuilder builder, - IFileConfigurationRepository fileConfigRepo, IOptionsMonitor fileConfig, - IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo) + else if (ConfigNotStoredInConsul(fileConfigFromConsul)) + { + //there was no config in consul set the file in config in consul + await fileConfigRepo.Set(fileConfig.CurrentValue); + } + else { - // get the config from consul. - var fileConfigFromConsul = await fileConfigRepo.Get(); + // create the internal config from consul data + var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data); - if (IsError(fileConfigFromConsul)) - { - ThrowToStopOcelotStarting(fileConfigFromConsul); - } - else if (ConfigNotStoredInConsul(fileConfigFromConsul)) + if (IsError(internalConfig)) { - //there was no config in consul set the file in config in consul - await fileConfigRepo.Set(fileConfig.CurrentValue); + ThrowToStopOcelotStarting(internalConfig); } else { - // create the internal config from consul data - var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data); + // add the internal config to the internal repo + var response = internalConfigRepo.AddOrReplace(internalConfig.Data); - if (IsError(internalConfig)) - { - ThrowToStopOcelotStarting(internalConfig); - } - else + if (IsError(response)) { - // add the internal config to the internal repo - var response = internalConfigRepo.AddOrReplace(internalConfig.Data); - - if (IsError(response)) - { - ThrowToStopOcelotStarting(response); - } + ThrowToStopOcelotStarting(response); } + } - if (IsError(internalConfig)) - { - ThrowToStopOcelotStarting(internalConfig); - } + if (IsError(internalConfig)) + { + ThrowToStopOcelotStarting(internalConfig); } } + } - private static void ThrowToStopOcelotStarting(Response config) - { - throw new Exception($"Unable to start Ocelot, errors are: {string.Join(',', config.Errors.Select(x => x.ToString()))}"); - } + private static void ThrowToStopOcelotStarting(Response config) + { + throw new Exception( + $"Unable to start Ocelot, errors are: {string.Join(',', config.Errors.Select(x => x.ToString()))}"); + } - private static bool IsError(Response response) - { - return response == null || response.IsError; - } + private static bool IsError(Response response) + { + return response == null || response.IsError; + } - private static bool ConfigNotStoredInConsul(Response fileConfigFromConsul) - { - return fileConfigFromConsul.Data == null; - } + private static bool ConfigNotStoredInConsul(Response fileConfigFromConsul) + { + return fileConfigFromConsul.Data == null; } } diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index dfe8b1938..724c6fb9d 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -1,27 +1,52 @@ using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; using Ocelot.Logging; -using Ocelot.ServiceDiscovery; - -namespace Ocelot.Provider.Consul -{ - public static class ConsulProviderFactory - { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => - { - var factory = provider.GetService(); - - var consulFactory = provider.GetService(); - - var consulRegistryConfiguration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); - - var consulServiceDiscoveryProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); - - if (config.Type?.ToLower() == "pollconsul") - { - return new PollConsul(config.PollingInterval, factory, consulServiceDiscoveryProvider); - } - - return consulServiceDiscoveryProvider; - }; - } -} +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.Provider.Consul; + +public static class ConsulProviderFactory +{ + /// + /// String constant used for provider type definition. + /// + public const string PollConsul = nameof(Provider.Consul.PollConsul); + + private static readonly List ServiceDiscoveryProviders = new(); + private static readonly object LockObject = new(); + + public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; + + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, + ServiceProviderConfiguration config, DownstreamRoute route) + { + var factory = provider.GetService(); + var consulFactory = provider.GetService(); + + var consulRegistryConfiguration = new ConsulRegistryConfiguration( + config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + + var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); + + if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) + { + lock (LockObject) + { + var discoveryProvider = + ServiceDiscoveryProviders.FirstOrDefault(x => x.ServiceName == route.ServiceName); + if (discoveryProvider != null) + { + return discoveryProvider; + } + + discoveryProvider = new PollConsul( + config.PollingInterval, route.ServiceName, factory, consulProvider); + ServiceDiscoveryProviders.Add(discoveryProvider); + + return discoveryProvider; + } + } + + return consulProvider; + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs index 38bf515fa..52fba7893 100644 --- a/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs +++ b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs @@ -1,20 +1,19 @@ -namespace Ocelot.Provider.Consul +namespace Ocelot.Provider.Consul; + +public class ConsulRegistryConfiguration { - public class ConsulRegistryConfiguration + public ConsulRegistryConfiguration(string scheme, string host, int port, string keyOfServiceInConsul, string token) { - public ConsulRegistryConfiguration(string scheme, string host, int port, string keyOfServiceInConsul, string token) - { - Host = string.IsNullOrEmpty(host) ? "localhost" : host; - Port = port > 0 ? port : 8500; - Scheme = string.IsNullOrEmpty(scheme) ? "http" : scheme; - KeyOfServiceInConsul = keyOfServiceInConsul; - Token = token; - } - - public string KeyOfServiceInConsul { get; } - public string Scheme { get; } - public string Host { get; } - public int Port { get; } - public string Token { get; } + Host = string.IsNullOrEmpty(host) ? "localhost" : host; + Port = port > 0 ? port : 8500; + Scheme = string.IsNullOrEmpty(scheme) ? "http" : scheme; + KeyOfServiceInConsul = keyOfServiceInConsul; + Token = token; } + + public string KeyOfServiceInConsul { get; } + public string Scheme { get; } + public string Host { get; } + public int Port { get; } + public string Token { get; } } diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs index 2f9f3058e..3ee3a2b25 100644 --- a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs @@ -1,9 +1,6 @@ -using Consul; +namespace Ocelot.Provider.Consul; -namespace Ocelot.Provider.Consul +public interface IConsulClientFactory { - public interface IConsulClientFactory - { - IConsulClient Get(ConsulRegistryConfiguration config); - } + IConsulClient Get(ConsulRegistryConfiguration config); } diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index cc3ec264b..48bce6f6e 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -3,25 +3,24 @@ using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; -namespace Ocelot.Provider.Consul +namespace Ocelot.Provider.Consul; + +public static class OcelotBuilderExtensions { - public static class OcelotBuilderExtensions + public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { - public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) - { - builder.Services.AddSingleton(ConsulProviderFactory.Get); - builder.Services.AddSingleton(); - builder.Services.RemoveAll(typeof(IFileConfigurationPollerOptions)); - builder.Services.AddSingleton(); - return builder; - } + builder.Services.AddSingleton(ConsulProviderFactory.Get); + builder.Services.AddSingleton(); + builder.Services.RemoveAll(typeof(IFileConfigurationPollerOptions)); + builder.Services.AddSingleton(); + return builder; + } - public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) - { - builder.Services.AddSingleton(ConsulMiddlewareConfigurationProvider.Get); - builder.Services.AddHostedService(); - builder.Services.AddSingleton(); - return builder; - } + public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) + { + builder.Services.AddSingleton(ConsulMiddlewareConfigurationProvider.Get); + builder.Services.AddHostedService(); + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Ocelot.Provider.Consul/PollConsul.cs b/src/Ocelot.Provider.Consul/PollConsul.cs index 6f42e15b3..295a17e15 100644 --- a/src/Ocelot.Provider.Consul/PollConsul.cs +++ b/src/Ocelot.Provider.Consul/PollConsul.cs @@ -4,46 +4,56 @@ namespace Ocelot.Provider.Consul; -public sealed class PollConsul : IServiceDiscoveryProvider, IDisposable +public sealed class PollConsul : IServiceDiscoveryProvider { - private readonly IOcelotLogger _logger; private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider; - private Timer _timer; - private bool _polling; + private readonly object _lockObject = new(); + private readonly IOcelotLogger _logger; + + private readonly int _pollingInterval; + + private DateTime _lastUpdateTime; private List _services; - public PollConsul(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) + public PollConsul(int pollingInterval, string serviceName, IOcelotLoggerFactory factory, + IServiceDiscoveryProvider consulServiceDiscoveryProvider) { _logger = factory.CreateLogger(); _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; - _services = new List(); + _pollingInterval = pollingInterval; - _timer = new Timer(async x => - { - if (_polling) - { - return; - } + // Initialize by DateTime.MinValue as lowest value. + // Polling will occur immediately during the first call + _lastUpdateTime = DateTime.MinValue; - _polling = true; - await Poll(); - _polling = false; - }, null, pollingInterval, pollingInterval); + _services = new List(); + ServiceName = serviceName; } - public void Dispose() - { - _timer?.Dispose(); - _timer = null; - } + public string ServiceName { get; } + /// + /// Get the services. + /// If the first call, retrieve the services and then start the timer. + /// + /// A with a result of . public Task> Get() { - return Task.FromResult(_services); - } + lock (_lockObject) + { + var refreshTime = _lastUpdateTime.AddMilliseconds(_pollingInterval); - private async Task Poll() - { - _services = await _consulServiceDiscoveryProvider.Get(); + // Check if any services available + if (refreshTime >= DateTime.UtcNow && _services.Any()) + { + return Task.FromResult(_services); + } + + _logger.LogInformation($"Retrieving new client information for service: {ServiceName}."); + _services = _consulServiceDiscoveryProvider.Get().Result; + _lastUpdateTime = DateTime.UtcNow; + + return Task.FromResult(_services); + } } } diff --git a/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs index abf763735..0e6481aad 100644 --- a/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs +++ b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs @@ -1,12 +1,11 @@ using Ocelot.Errors; -namespace Ocelot.Provider.Consul +namespace Ocelot.Provider.Consul; + +public class UnableToSetConfigInConsulError : Error { - public class UnableToSetConfigInConsulError : Error + public UnableToSetConfigInConsulError(string s) + : base(s, OcelotErrorCode.UnknownError, 404) { - public UnableToSetConfigInConsulError(string s) - : base(s, OcelotErrorCode.UnknownError, 404) - { - } } } diff --git a/src/Ocelot.Provider.Consul/Usings.cs b/src/Ocelot.Provider.Consul/Usings.cs index 8dee5b38a..777ae303e 100644 --- a/src/Ocelot.Provider.Consul/Usings.cs +++ b/src/Ocelot.Provider.Consul/Usings.cs @@ -1,13 +1,10 @@ // Default Microsoft.NET.Sdk namespaces + global using System; global using System.Collections.Generic; -global using System.IO; global using System.Linq; -global using System.Net.Http; -global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using Consul; -global using Ocelot; global using Ocelot.ServiceDiscovery; diff --git a/src/Ocelot.Provider.Eureka/Eureka.cs b/src/Ocelot.Provider.Eureka/Eureka.cs index 49756e9d2..cbb73785f 100644 --- a/src/Ocelot.Provider.Eureka/Eureka.cs +++ b/src/Ocelot.Provider.Eureka/Eureka.cs @@ -1,6 +1,5 @@ using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; -using Steeltoe.Discovery; +using Ocelot.Values; namespace Ocelot.Provider.Eureka { diff --git a/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs index 72e1906b4..c45d0b540 100644 --- a/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs +++ b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs @@ -1,21 +1,30 @@ using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; using Steeltoe.Discovery; +using System; -namespace Ocelot.Provider.Eureka +namespace Ocelot.Provider.Eureka; + +public static class EurekaProviderFactory { - public static class EurekaProviderFactory + /// + /// String constant used for provider type definition. + /// + public const string Eureka = nameof(Provider.Eureka.Eureka); + + public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; + + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => - { - var client = provider.GetService(); + var client = provider.GetService(); - if (config.Type?.ToLower() == "eureka" && client != null) - { - return new Eureka(route.ServiceName, client); - } + if (Eureka.Equals(config.Type, StringComparison.OrdinalIgnoreCase) && client != null) + { + return new Eureka(route.ServiceName, client); + } - return null; - }; + return null; } } diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs index 781a2352f..5d9fae016 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs @@ -3,35 +3,39 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; +using System; namespace Ocelot.Provider.Kubernetes { public static class KubernetesProviderFactory - { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => - { - var factory = provider.GetService(); - return GetKubeProvider(provider, config, route, factory); - }; + { + /// + /// String constant used for provider type definition. + /// + public const string PollKube = nameof(Kubernetes.PollKube); + + public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; - private static ServiceDiscovery.Providers.IServiceDiscoveryProvider GetKubeProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route, IOcelotLoggerFactory factory) + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { + var factory = provider.GetService(); var kubeClient = provider.GetService(); - var k8sRegistryConfiguration = new KubeRegistryConfiguration + var k8SRegistryConfiguration = new KubeRegistryConfiguration { KeyOfServiceInK8s = route.ServiceName, KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, }; - var k8sServiceDiscoveryProvider = new KubernetesServiceDiscoveryProvider(k8sRegistryConfiguration, factory, kubeClient); + var k8SServiceDiscoveryProvider = new KubernetesServiceDiscoveryProvider(k8SRegistryConfiguration, factory, kubeClient); - if (config.Type?.ToLower() == "pollkube") + if (PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { - return new PollKubernetes(config.PollingInterval, factory, k8sServiceDiscoveryProvider); + return new PollKube(config.PollingInterval, factory, k8SServiceDiscoveryProvider); } - return k8sServiceDiscoveryProvider; + return k8SServiceDiscoveryProvider; } } } diff --git a/src/Ocelot.Provider.Kubernetes/PollKubernetes.cs b/src/Ocelot.Provider.Kubernetes/PollKube.cs similarity index 76% rename from src/Ocelot.Provider.Kubernetes/PollKubernetes.cs rename to src/Ocelot.Provider.Kubernetes/PollKube.cs index 45ac3824f..31d42a56b 100644 --- a/src/Ocelot.Provider.Kubernetes/PollKubernetes.cs +++ b/src/Ocelot.Provider.Kubernetes/PollKube.cs @@ -1,10 +1,13 @@ using Ocelot.Logging; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Ocelot.Provider.Kubernetes { - public class PollKubernetes : IServiceDiscoveryProvider + public class PollKube : IServiceDiscoveryProvider { private readonly IOcelotLogger _logger; private readonly IServiceDiscoveryProvider _kubeServiceDiscoveryProvider; @@ -12,9 +15,9 @@ public class PollKubernetes : IServiceDiscoveryProvider private bool _polling; private List _services; - public PollKubernetes(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeServiceDiscoveryProvider) + public PollKube(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeServiceDiscoveryProvider) { - _logger = factory.CreateLogger(); + _logger = factory.CreateLogger(); _kubeServiceDiscoveryProvider = kubeServiceDiscoveryProvider; _services = new List(); diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 46c1592b5..c58bd1b9f 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -5,26 +5,30 @@ using Ocelot.ServiceDiscovery.Configuration; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; +using System; +using System.Collections.Generic; namespace Ocelot.ServiceDiscovery { public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { - private readonly IOcelotLoggerFactory _factory; private readonly ServiceDiscoveryFinderDelegate _delegates; - private readonly IServiceProvider _provider; + private readonly IServiceProvider _provider; + private readonly IOcelotLogger _logger; public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) { - _factory = factory; _provider = provider; - _delegates = provider.GetService(); + _delegates = provider.GetService(); + _logger = factory.CreateLogger(); } public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { if (route.UseServiceDiscovery) - { + { + var routeName = route.UpstreamPathTemplate?.Template ?? route.ServiceName ?? string.Empty; + _logger.LogInformation($"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{routeName}' is enabled."); return GetServiceDiscoveryProvider(serviceConfig, route); } @@ -41,7 +45,9 @@ public Response Get(ServiceProviderConfiguration serv } private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route) - { + { + _logger.LogInformation($"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); + if (config.Type?.ToLower() == "servicefabric") { var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); @@ -57,8 +63,11 @@ private Response GetServiceDiscoveryProvider(ServiceP return new OkResponse(provider); } } - - return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError($"Unable to find service discovery provider for type: {config.Type}")); + + var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; + _logger.LogWarning(message); + + return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError(message)); } } } diff --git a/test/Ocelot.AcceptanceTests/SequentialTestsCollection.cs b/test/Ocelot.AcceptanceTests/SequentialTestsCollection.cs new file mode 100644 index 000000000..2b9c8d474 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/SequentialTestsCollection.cs @@ -0,0 +1,8 @@ +using Xunit; + +namespace Ocelot.AcceptanceTests; + +[CollectionDefinition("Sequential", DisableParallelization = true)] +public class SequentialTestsCollection +{ +} diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs index f75fa20f2..1313923c0 100644 --- a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -36,6 +36,17 @@ public void should_return_service_from_consul() .BDDfy(); } + [Fact] + public void should_return_service_from_consul_without_delay() + { + var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); + + this.Given(x => GivenConsulReturns(service)) + .When(x => WhenIGetTheServicesWithoutDelay(1)) + .Then(x => ThenTheCountIs(1)) + .BDDfy(); + } + private void GivenConsulReturns(Service service) { _services.Add(service); @@ -49,28 +60,38 @@ private void ThenTheCountIs(int count) private void WhenIGetTheServices(int expected) { - using (var provider = new PollConsul(_delay, _factory.Object, _consulServiceDiscoveryProvider.Object)) + var provider = new PollConsul(_delay, "test", _factory.Object, _consulServiceDiscoveryProvider.Object); + var result = Wait.WaitFor(3000).Until(() => { - var result = Wait.WaitFor(3000).Until(() => + try + { + _result = provider.Get().GetAwaiter().GetResult(); + return _result.Count == expected; + } + catch (Exception) { - try - { - _result = provider.Get().GetAwaiter().GetResult(); - if (_result.Count == expected) - { - return true; - } + return false; + } + }); - return false; - } - catch (Exception) - { - return false; - } - }); + result.ShouldBeTrue(); + } - result.ShouldBeTrue(); + private void WhenIGetTheServicesWithoutDelay(int expected) + { + var provider = new PollConsul(_delay, "test2", _factory.Object, _consulServiceDiscoveryProvider.Object); + bool result; + try + { + _result = provider.Get().GetAwaiter().GetResult(); + result = _result.Count == expected; + } + catch (Exception) + { + result = false; } + + result.ShouldBeTrue(); } } } diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs index 9fe0bdb4c..f4265b5b7 100644 --- a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -1,52 +1,117 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Consul; - -namespace Ocelot.UnitTests.Consul -{ - public class ProviderFactoryTests - { - private readonly IServiceProvider _provider; - - public ProviderFactoryTests() - { - var services = new ServiceCollection(); - var loggerFactory = new Mock(); - var logger = new Mock(); - loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); - loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); - var consulFactory = new Mock(); - services.AddSingleton(consulFactory.Object); - services.AddSingleton(loggerFactory.Object); - _provider = services.BuildServiceProvider(); - } - - [Fact] - public void should_return_ConsulServiceDiscoveryProvider() - { - var route = new DownstreamRouteBuilder() - .WithServiceName(string.Empty) - .Build(); - - var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration(string.Empty, string.Empty, string.Empty, 1, string.Empty, string.Empty, 1), route); - provider.ShouldBeOfType(); - } - - [Fact] - public void should_return_PollingConsulServiceDiscoveryProvider() - { - var stopsPollerFromPolling = 10000; - - var route = new DownstreamRouteBuilder() - .WithServiceName(string.Empty) - .Build(); - - var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("pollconsul", "http", string.Empty, 1, string.Empty, string.Empty, stopsPollerFromPolling), route); - var pollProvider = provider as PollConsul; - pollProvider.ShouldNotBeNull(); - pollProvider.Dispose(); - } - } +using Ocelot.ServiceDiscovery.Providers; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Consul; + +public class ProviderFactoryTests +{ + private readonly IServiceProvider _provider; + + public ProviderFactoryTests() + { + var services = new ServiceCollection(); + var loggerFactory = new Mock(); + var logger = new Mock(); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + var consulFactory = new Mock(); + services.AddSingleton(consulFactory.Object); + services.AddSingleton(loggerFactory.Object); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void should_return_ConsulServiceDiscoveryProvider() + { + var route = new DownstreamRouteBuilder() + .WithServiceName(string.Empty) + .Build(); + + var provider = ConsulProviderFactory.Get(_provider, + new ServiceProviderConfiguration(string.Empty, string.Empty, string.Empty, 1, string.Empty, string.Empty, + 1), route); + provider.ShouldBeOfType(); + } + + [Fact] + public void should_return_PollingConsulServiceDiscoveryProvider() + { + var provider = DummyPollingConsulServiceFactory(string.Empty); + var pollProvider = provider as PollConsul; + pollProvider.ShouldNotBeNull(); + } + + [Fact] + public void should_return_SameProviderForGivenServiceName() + { + var provider = DummyPollingConsulServiceFactory("test"); + var provider2 = DummyPollingConsulServiceFactory("test"); + + provider.ShouldBeEquivalentTo(provider2); + + var pollProvider = provider as PollConsul; + pollProvider.ShouldNotBeNull(); + + var pollProvider2 = provider2 as PollConsul; + pollProvider2.ShouldNotBeNull(); + + pollProvider.ServiceName.ShouldBeEquivalentTo(pollProvider2.ServiceName); + } + + [Theory] + [InlineData(new object[] { new[] { "service1", "service2", "service3", "service4" } })] + public void should_return_ProviderAccordingToServiceName(string[] serviceNames) + { + var providersList = serviceNames.Select(DummyPollingConsulServiceFactory).ToList(); + + foreach (var serviceName in serviceNames) + { + var currentProvider = DummyPollingConsulServiceFactory(serviceName); + providersList.ShouldContain(currentProvider); + } + + var convertedProvidersList = providersList.Select(x => x as PollConsul).ToList(); + + foreach (var convertedProvider in convertedProvidersList) + { + convertedProvider.ShouldNotBeNull(); + } + + foreach (var serviceName in serviceNames) + { + var cProvider = DummyPollingConsulServiceFactory(serviceName); + var convertedCProvider = cProvider as PollConsul; + + convertedCProvider.ShouldNotBeNull(); + + var matchingProviders = convertedProvidersList.Where(x => x.ServiceName == convertedCProvider.ServiceName) + .ToList(); + matchingProviders.ShouldHaveSingleItem(); + + matchingProviders.First().ShouldNotBeNull(); + matchingProviders.First().ServiceName.ShouldBeEquivalentTo(convertedCProvider.ServiceName); + } + } + + private IServiceDiscoveryProvider DummyPollingConsulServiceFactory(string serviceName) + { + var stopsFromPolling = 10000; + + var route = new DownstreamRouteBuilder() + .WithServiceName(serviceName) + .Build(); + + return ConsulProviderFactory.Get(_provider, + new ServiceProviderConfiguration("pollconsul", "http", string.Empty, 1, string.Empty, string.Empty, + stopsFromPolling), route); + } } diff --git a/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs index f7143bb1c..3d21728df 100644 --- a/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Kubernetes public class PollingKubeServiceDiscoveryProviderTests { private readonly int _delay; - private PollKubernetes _provider; + private PollKube _provider; private readonly List _services; private readonly Mock _factory; private readonly Mock _logger; @@ -22,7 +22,7 @@ public PollingKubeServiceDiscoveryProviderTests() _delay = 1; _factory = new Mock(); _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _kubeServiceDiscoveryProvider = new Mock(); } @@ -50,7 +50,7 @@ private void ThenTheCountIs(int count) private void WhenIGetTheServices(int expected) { - _provider = new PollKubernetes(_delay, _factory.Object, _kubeServiceDiscoveryProvider.Object); + _provider = new PollKube(_delay, _factory.Object, _kubeServiceDiscoveryProvider.Object); var result = Wait.WaitFor(3000).Until(() => { diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index 5b1cbf423..d83f146d1 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -16,7 +16,7 @@ public class ServiceDiscoveryProviderFactoryTests private ServiceDiscoveryProviderFactory _factory; private DownstreamRoute _route; private readonly Mock _loggerFactory; - private Mock _logger; + private readonly Mock _logger; private IServiceProvider _provider; private readonly IServiceCollection _collection; @@ -26,7 +26,10 @@ public ServiceDiscoveryProviderFactoryTests() _logger = new Mock(); _collection = new ServiceCollection(); _provider = _collection.BuildServiceProvider(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); + + _loggerFactory.Setup(x => x.CreateLogger()) + .Returns(_logger.Object); } [Fact] @@ -114,7 +117,8 @@ public void should_return_service_fabric_provider() .WithType("ServiceFabric") .Build(); - this.Given(x => x.GivenTheRoute(serviceConfig, route)) + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .And(x => GivenAFakeDelegate()) .When(x => x.WhenIGetTheServiceProvider()) .Then(x => x.ThenTheServiceProviderIs()) .BDDfy(); @@ -143,7 +147,18 @@ private void ThenTheDelegateIsCalled() private void ThenTheResultIsError() { - _result.IsError.ShouldBeTrue(); + _result.IsError.ShouldBeTrue(); + _result.Errors.Count.ShouldBe(1); + + _logInformationMessages.ShouldNotBeNull() + .Count.ShouldBe(2); + _logger.Verify(x => x.LogInformation(It.IsAny()), + Times.Exactly(2)); + + _logWarningMessages.ShouldNotBeNull() + .Count.ShouldBe(1); + _logger.Verify(x => x.LogWarning(It.IsAny()), + Times.Once()); } private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) @@ -166,9 +181,17 @@ private void GivenTheRoute(ServiceProviderConfiguration serviceConfig, Downstrea _serviceConfig = serviceConfig; _route = route; } + + private List _logInformationMessages = new(); + private List _logWarningMessages = new(); private void WhenIGetTheServiceProvider() - { + { + _logger.Setup(x => x.LogInformation(It.IsAny())) + .Callback(message => _logInformationMessages.Add(message)); + _logger.Setup(x => x.LogWarning(It.IsAny())) + .Callback(message => _logWarningMessages.Add(message)); + _result = _factory.Get(_serviceConfig, _route); }