diff --git a/Ocelot.sln b/Ocelot.sln index 82a17db9..fb7008d3 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.15 +VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject diff --git a/build.ps1 b/build.ps1 index 40b4d740..1287451b 100644 --- a/build.ps1 +++ b/build.ps1 @@ -158,14 +158,15 @@ if(-Not $SkipToolPackageRestore.IsPresent) { if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { Write-Verbose -Message "Missing or changed package.config hash..." - Remove-Item * -Recurse -Exclude packages.config,nuget.exe + Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | + Remove-Item -Recurse } Write-Verbose -Message "Restoring tools from NuGet..." $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet tools." + Throw "An error occurred while restoring NuGet tools." } else { @@ -185,7 +186,7 @@ if (Test-Path $ADDINS_PACKAGES_CONFIG) { $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet addins." + Throw "An error occurred while restoring NuGet addins." } Write-Verbose -Message ($NuGetOutput | out-string) @@ -202,7 +203,7 @@ if (Test-Path $MODULES_PACKAGES_CONFIG) { $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" if ($LASTEXITCODE -ne 0) { - Throw "An error occured while restoring NuGet modules." + Throw "An error occurred while restoring NuGet modules." } Write-Verbose -Message ($NuGetOutput | out-string) @@ -231,4 +232,4 @@ $cakeArguments += $ScriptArgs # Start Cake Write-Host "Running build script..." &$CAKE_EXE $cakeArguments -exit $LASTEXITCODE +exit $LASTEXITCODE \ No newline at end of file diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 29a19af0..3bd4da2f 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -61,7 +61,8 @@ Here is an example ReRoute configuration, You don't need to set all of these thi }, "HttpHandlerOptions": { "AllowAutoRedirect": true, - "UseCookieContainer": true + "UseCookieContainer": true, + "UseTracing": true }, "UseServiceDiscovery": false } diff --git a/docs/features/delegatinghandlers.rst b/docs/features/delegatinghandlers.rst new file mode 100644 index 00000000..a6f2e4e9 --- /dev/null +++ b/docs/features/delegatinghandlers.rst @@ -0,0 +1,43 @@ +Delegating Handers +================== + +Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ and I decided that it was going to be useful in various ways. + +Usage +^^^^^^ + +In order to add delegating handlers to the HttpClient transport you need to do the following. + +.. code-block:: csharp + + services.AddOcelot() + .AddDelegatingHandler(() => new FakeHandler()) + .AddDelegatingHandler(() => new FakeHandler()); + +Or for singleton like behaviour.. + +.. code-block:: csharp + + var handlerOne = new FakeHandler(); + var handlerTwo = new FakeHandler(); + + services.AddOcelot() + .AddDelegatingHandler(() => handlerOne) + .AddDelegatingHandler(() => handlerTwo); + +You can have as many DelegatingHandlers as you want and they are run in a first in first out order. If you are using Ocelot's QoS functionality then that will always be run after your last delegating handler. + +In order to create a class that can be used a delegating handler it must look as follows + +.. code-block:: csharp + + public class FakeHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //do stuff and optionally call the base handler.. + return await base.SendAsync(request, cancellationToken); + } + } + +Hopefully other people will find this feature useful! \ No newline at end of file diff --git a/docs/features/tracing.rst b/docs/features/tracing.rst new file mode 100644 index 00000000..a30ea741 --- /dev/null +++ b/docs/features/tracing.rst @@ -0,0 +1,31 @@ +Tracing +======= + +Ocelot providers tracing functionality from the excellent `Butterfly `_ project. + +In order to use the tracing please read the Butterfly documentation. + +In ocelot you need to do the following if you wish to trace a ReRoute. + +In your ConfigureServices method + +.. code-block:: csharp + + services + .AddOcelot(Configuration) + .AddOpenTracing(option => + { + //this is the url that the butterfly collector server is running on... + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot"; + }); + +Then in your configuration.json add the following to the ReRoute you want to trace.. + +.. code-block:: json + + "HttpHandlerOptions": { + "UseTracing": true + }, + +Ocelot will now send tracing information to Butterfly when this ReRoute is called. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index c2bc7041..c98c2174 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,9 +30,12 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/headerstransformation features/claimstransformation features/logging + features/tracing features/requestid features/middlewareinjection features/loadbalancer + features/delegatinghandlers + .. toctree:: :maxdepth: 2 diff --git a/global.json b/global.json index 38106d45..7281f931 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "test" ], "sdk": { - "version": "2.0.2" + "version": "2.1.4" } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs index 52aa7694..6c66f3c0 100644 --- a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs @@ -7,7 +7,7 @@ namespace Ocelot.Configuration.Creator public HttpHandlerOptions Create(FileReRoute fileReRoute) { return new HttpHandlerOptions(fileReRoute.HttpHandlerOptions.AllowAutoRedirect, - fileReRoute.HttpHandlerOptions.UseCookieContainer); + fileReRoute.HttpHandlerOptions.UseCookieContainer, fileReRoute.HttpHandlerOptions.UseTracing); } } } diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index ea352767..7f24b572 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -11,5 +11,7 @@ public bool AllowAutoRedirect { get; set; } public bool UseCookieContainer { get; set; } + + public bool UseTracing { get; set; } } } diff --git a/src/Ocelot/Configuration/HttpHandlerOptions.cs b/src/Ocelot/Configuration/HttpHandlerOptions.cs index 0ec72d1d..ef0edd9c 100644 --- a/src/Ocelot/Configuration/HttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/HttpHandlerOptions.cs @@ -6,10 +6,11 @@ /// public class HttpHandlerOptions { - public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer) + public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool useTracing) { AllowAutoRedirect = allowAutoRedirect; UseCookieContainer = useCookieContainer; + UseTracing = useTracing; } /// @@ -21,5 +22,10 @@ /// Specify is handler has to use a cookie container /// public bool UseCookieContainer { get; private set; } + + // + /// Specify is handler has to use a opentracing + /// + public bool UseTracing { get; private set; } } } diff --git a/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs new file mode 100644 index 00000000..859f4d98 --- /dev/null +++ b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs @@ -0,0 +1,7 @@ +namespace Ocelot.DependencyInjection +{ + public interface IOcelotAdministrationBuilder + { + IOcelotAdministrationBuilder AddRafty(); + } +} diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index d7cb0f0d..d2f92101 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -1,12 +1,16 @@ -using CacheManager.Core; -using System; - -namespace Ocelot.DependencyInjection -{ - public interface IOcelotBuilder - { - IOcelotBuilder AddStoreOcelotConfigurationInConsul(); - IOcelotBuilder AddCacheManager(Action settings); - IOcelotAdministrationBuilder AddAdministration(string path, string secret); - } -} +using Butterfly.Client.AspNetCore; +using CacheManager.Core; +using System; +using System.Net.Http; + +namespace Ocelot.DependencyInjection +{ + public interface IOcelotBuilder + { + IOcelotBuilder AddStoreOcelotConfigurationInConsul(); + IOcelotBuilder AddCacheManager(Action settings); + IOcelotBuilder AddOpenTracing(Action settings); + IOcelotAdministrationBuilder AddAdministration(string path, string secret); + IOcelotBuilder AddDelegatingHandler(Func delegatingHandler); + } +} diff --git a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs new file mode 100644 index 00000000..580839c3 --- /dev/null +++ b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; + +namespace Ocelot.DependencyInjection +{ + public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder + { + private readonly IServiceCollection _services; + private readonly IConfiguration _configurationRoot; + + public OcelotAdministrationBuilder(IServiceCollection services, IConfiguration configurationRoot) + { + _configurationRoot = configurationRoot; + _services = services; + } + + public IOcelotAdministrationBuilder AddRafty() + { + var settings = new InMemorySettings(4000, 5000, 100, 5000); + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(settings); + _services.AddSingleton(); + _services.AddSingleton(); + _services.Configure(_configurationRoot); + return this; + } + } +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index a923071b..10f9d082 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -1,307 +1,295 @@ -using CacheManager.Core; -using IdentityServer4.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Authorisation; -using Ocelot.Cache; -using Ocelot.Claims; -using Ocelot.Configuration.Authentication; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Parser; -using Ocelot.Configuration.Provider; -using Ocelot.Configuration.Repository; -using Ocelot.Configuration.Setter; -using Ocelot.Configuration.Validator; -using Ocelot.DownstreamRouteFinder.Finder; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; -using Ocelot.Headers; -using Ocelot.Infrastructure.Claims.Parser; -using Ocelot.Infrastructure.RequestData; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.QueryStrings; -using Ocelot.RateLimit; -using Ocelot.Request.Builder; -using Ocelot.Request.Mapper; -using Ocelot.Requester; -using Ocelot.Requester.QoS; -using Ocelot.Responder; -using Ocelot.ServiceDiscovery; -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using IdentityServer4.AccessTokenValidation; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Linq; -using Ocelot.Raft; -using Rafty.Concensus; -using Rafty.FiniteStateMachine; -using Rafty.Infrastructure; -using Rafty.Log; -using Newtonsoft.Json; - -namespace Ocelot.DependencyInjection -{ - public class OcelotBuilder : IOcelotBuilder - { - private IServiceCollection _services; - private IConfiguration _configurationRoot; - - public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot) - { - _configurationRoot = configurationRoot; - _services = services; - - //add default cache settings... - Action defaultCachingSettings = x => - { - x.WithDictionaryHandle(); - }; - - AddCacheManager(defaultCachingSettings); - - //add ocelot services... - _services.Configure(configurationRoot); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc - // could maybe use a scoped data repository - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.AddMemoryCache(); - _services.TryAddSingleton(); - - //add asp.net services.. - var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; - - _services.AddMvcCore() - .AddApplicationPart(assembly) - .AddControllersAsServices() - .AddAuthorization() - .AddJsonFormatters(); - - _services.AddLogging(); - _services.AddMiddlewareAnalysis(); - _services.AddWebEncoders(); - _services.AddSingleton(new NullAdministrationPath()); - } - - public IOcelotAdministrationBuilder AddAdministration(string path, string secret) - { - var administrationPath = new AdministrationPath(path); - - //add identity server for admin area - var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret); - - if (identityServerConfiguration != null) - { - AddIdentityServer(identityServerConfiguration, administrationPath); - } - - var descriptor = new ServiceDescriptor(typeof(IAdministrationPath), administrationPath); - _services.Replace(descriptor); - return new OcelotAdministrationBuilder(_services, _configurationRoot); - } - - public IOcelotBuilder AddStoreOcelotConfigurationInConsul() - { - var serviceDiscoveryPort = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Port", 0); - var serviceDiscoveryHost = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Host", string.Empty); - - var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderPort(serviceDiscoveryPort) - .WithServiceDiscoveryProviderHost(serviceDiscoveryHost) - .Build(); - - _services.AddSingleton(config); - _services.AddSingleton(); - _services.AddSingleton(); - return this; - } - - public IOcelotBuilder AddCacheManager(Action settings) - { - var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); - var ocelotOutputCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); - - _services.RemoveAll(typeof(ICacheManager)); - _services.RemoveAll(typeof(IOcelotCache)); - _services.AddSingleton>(cacheManagerOutputCache); - _services.AddSingleton>(ocelotOutputCacheManager); - - var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); - var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); - _services.RemoveAll(typeof(ICacheManager)); - _services.RemoveAll(typeof(IOcelotCache)); - _services.AddSingleton>(ocelotConfigCacheManagerOutputCache); - _services.AddSingleton>(ocelotConfigCacheManager); - - var fileConfigCacheManagerOutputCache = CacheFactory.Build("FileConfigurationCache", settings); - var fileConfigCacheManager = new OcelotCacheManagerCache(fileConfigCacheManagerOutputCache); - _services.RemoveAll(typeof(ICacheManager)); - _services.RemoveAll(typeof(IOcelotCache)); - _services.AddSingleton>(fileConfigCacheManagerOutputCache); - _services.AddSingleton>(fileConfigCacheManager); - return this; - } - - private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath) - { - _services.TryAddSingleton(identityServerConfiguration); - _services.TryAddSingleton(); - var identityServerBuilder = _services - .AddIdentityServer(o => { - o.IssuerUri = "Ocelot"; - }) - .AddInMemoryApiResources(Resources(identityServerConfiguration)) - .AddInMemoryClients(Client(identityServerConfiguration)); - - //todo - refactor a method so we know why this is happening - var whb = _services.First(x => x.ServiceType == typeof(IWebHostBuilder)); - var urlFinder = new BaseUrlFinder((IWebHostBuilder)whb.ImplementationInstance); - var baseSchemeUrlAndPort = urlFinder.Find(); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - _services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) - .AddIdentityServerAuthentication(o => - { - o.Authority = baseSchemeUrlAndPort + adminPath.Path; - o.ApiName = identityServerConfiguration.ApiName; - o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = identityServerConfiguration.ApiSecret; - }); - - //todo - refactor naming.. - if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword)) - { - identityServerBuilder.AddDeveloperSigningCredential(); - } - else - { - //todo - refactor so calls method? - var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword); - identityServerBuilder.AddSigningCredential(cert); - } - } - - private List Resources(IIdentityServerConfiguration identityServerConfiguration) - { - return new List - { - new ApiResource(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName) - { - ApiSecrets = new List - { - new Secret - { - Value = identityServerConfiguration.ApiSecret.Sha256() - } - } - }, - }; - } - - private List Client(IIdentityServerConfiguration identityServerConfiguration) - { - return new List - { - new Client - { - ClientId = identityServerConfiguration.ApiName, - AllowedGrantTypes = GrantTypes.ClientCredentials, - ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, - AllowedScopes = { identityServerConfiguration.ApiName } - } - }; - } - } - - public interface IOcelotAdministrationBuilder - { - IOcelotAdministrationBuilder AddRafty(); - } - - public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder - { - private IServiceCollection _services; - private IConfiguration _configurationRoot; - - public OcelotAdministrationBuilder(IServiceCollection services, IConfiguration configurationRoot) - { - _configurationRoot = configurationRoot; - _services = services; - } - - public IOcelotAdministrationBuilder AddRafty() - { - var settings = new InMemorySettings(4000, 5000, 100, 5000); - _services.AddSingleton(); - _services.AddSingleton(); - _services.AddSingleton(settings); - _services.AddSingleton(); - _services.AddSingleton(); - _services.Configure(_configurationRoot); - return this; - } - } -} +using CacheManager.Core; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Authorisation; +using Ocelot.Cache; +using Ocelot.Claims; +using Ocelot.Configuration.Authentication; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; +using Ocelot.Configuration.Validator; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Headers; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.QueryStrings; +using Ocelot.RateLimit; +using Ocelot.Request.Builder; +using Ocelot.Request.Mapper; +using Ocelot.Requester; +using Ocelot.Requester.QoS; +using Ocelot.Responder; +using Ocelot.ServiceDiscovery; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Linq; +using System.Net.Http; +using Butterfly.Client.AspNetCore; + +namespace Ocelot.DependencyInjection +{ + public class OcelotBuilder : IOcelotBuilder + { + private readonly IServiceCollection _services; + private readonly IConfiguration _configurationRoot; + private IDelegatingHandlerHandlerProvider _provider; + + public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot) + { + _configurationRoot = configurationRoot; + _services = services; + + //add default cache settings... + Action defaultCachingSettings = x => + { + x.WithDictionaryHandle(); + }; + + AddCacheManager(defaultCachingSettings); + + //add ocelot services... + _services.Configure(configurationRoot); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + + // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc + // could maybe use a scoped data repository + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.AddMemoryCache(); + _services.TryAddSingleton(); + + //add asp.net services.. + var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; + + _services.AddMvcCore() + .AddApplicationPart(assembly) + .AddControllersAsServices() + .AddAuthorization() + .AddJsonFormatters(); + + _services.AddLogging(); + _services.AddMiddlewareAnalysis(); + _services.AddWebEncoders(); + _services.AddSingleton(new NullAdministrationPath()); + + //these get picked out later and added to http request + _provider = new DelegatingHandlerHandlerProvider(); + _services.TryAddSingleton(_provider); + _services.AddTransient(); + } + + public IOcelotAdministrationBuilder AddAdministration(string path, string secret) + { + var administrationPath = new AdministrationPath(path); + + //add identity server for admin area + var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret); + + if (identityServerConfiguration != null) + { + AddIdentityServer(identityServerConfiguration, administrationPath); + } + + var descriptor = new ServiceDescriptor(typeof(IAdministrationPath), administrationPath); + _services.Replace(descriptor); + return new OcelotAdministrationBuilder(_services, _configurationRoot); + } + + public IOcelotBuilder AddDelegatingHandler(Func delegatingHandler) + { + _provider.Add(delegatingHandler); + return this; + } + + public IOcelotBuilder AddOpenTracing(Action settings) + { + _services.AddTransient(); + _services.AddButterfly(settings); + return this; + } + + public IOcelotBuilder AddStoreOcelotConfigurationInConsul() + { + var serviceDiscoveryPort = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Port", 0); + var serviceDiscoveryHost = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Host", string.Empty); + + var config = new ServiceProviderConfigurationBuilder() + .WithServiceDiscoveryProviderPort(serviceDiscoveryPort) + .WithServiceDiscoveryProviderHost(serviceDiscoveryHost) + .Build(); + + _services.AddSingleton(config); + _services.AddSingleton(); + _services.AddSingleton(); + return this; + } + + public IOcelotBuilder AddCacheManager(Action settings) + { + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); + var ocelotOutputCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + + _services.RemoveAll(typeof(ICacheManager)); + _services.RemoveAll(typeof(IOcelotCache)); + _services.AddSingleton>(cacheManagerOutputCache); + _services.AddSingleton>(ocelotOutputCacheManager); + + var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); + var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); + _services.RemoveAll(typeof(ICacheManager)); + _services.RemoveAll(typeof(IOcelotCache)); + _services.AddSingleton>(ocelotConfigCacheManagerOutputCache); + _services.AddSingleton>(ocelotConfigCacheManager); + + var fileConfigCacheManagerOutputCache = CacheFactory.Build("FileConfigurationCache", settings); + var fileConfigCacheManager = new OcelotCacheManagerCache(fileConfigCacheManagerOutputCache); + _services.RemoveAll(typeof(ICacheManager)); + _services.RemoveAll(typeof(IOcelotCache)); + _services.AddSingleton>(fileConfigCacheManagerOutputCache); + _services.AddSingleton>(fileConfigCacheManager); + return this; + } + + private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath) + { + _services.TryAddSingleton(identityServerConfiguration); + _services.TryAddSingleton(); + var identityServerBuilder = _services + .AddIdentityServer(o => { + o.IssuerUri = "Ocelot"; + }) + .AddInMemoryApiResources(Resources(identityServerConfiguration)) + .AddInMemoryClients(Client(identityServerConfiguration)); + + //todo - refactor a method so we know why this is happening + var whb = _services.First(x => x.ServiceType == typeof(IWebHostBuilder)); + var urlFinder = new BaseUrlFinder((IWebHostBuilder)whb.ImplementationInstance); + var baseSchemeUrlAndPort = urlFinder.Find(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + _services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) + .AddIdentityServerAuthentication(o => + { + o.Authority = baseSchemeUrlAndPort + adminPath.Path; + o.ApiName = identityServerConfiguration.ApiName; + o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = identityServerConfiguration.ApiSecret; + }); + + //todo - refactor naming.. + if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword)) + { + identityServerBuilder.AddDeveloperSigningCredential(); + } + else + { + //todo - refactor so calls method? + var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword); + identityServerBuilder.AddSigningCredential(cert); + } + } + + private List Resources(IIdentityServerConfiguration identityServerConfiguration) + { + return new List + { + new ApiResource(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName) + { + ApiSecrets = new List + { + new Secret + { + Value = identityServerConfiguration.ApiSecret.Sha256() + } + } + }, + }; + } + + private List Client(IIdentityServerConfiguration identityServerConfiguration) + { + return new List + { + new Client + { + ClientId = identityServerConfiguration.ApiName, + AllowedGrantTypes = GrantTypes.ClientCredentials, + ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, + AllowedScopes = { identityServerConfiguration.ApiName } + } + }; + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 4befa3f9..bcfde4b0 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Ocelot.Configuration; -using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; using Ocelot.Responses; diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index c2574ff9..54461bbd 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -62,7 +62,7 @@ namespace Ocelot.Errors.Middleware private async Task TrySetGlobalRequestId(HttpContext context) { //try and get the global request id and set it for logs... - //shoudl this basically be immutable per request...i guess it should! + //should this basically be immutable per request...i guess it should! //first thing is get config var configuration = await _configProvider.Get(); diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index e4fba055..b976e29c 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -33,6 +33,7 @@ UnmappableRequestError, RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, - FileValidationFailedError + FileValidationFailedError, + UnableToFindDelegatingHandlerProviderError } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs index 2daf74ff..8ce7bcd4 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -9,4 +9,4 @@ namespace Ocelot.LoadBalancer.LoadBalancers { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index 29347c88..aa37196f 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.Provider; using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; @@ -46,11 +45,14 @@ namespace Ocelot.LoadBalancer.Middleware } var uriBuilder = new UriBuilder(DownstreamRequest.RequestUri); + uriBuilder.Host = hostAndPort.Data.DownstreamHost; + if (hostAndPort.Data.DownstreamPort > 0) { uriBuilder.Port = hostAndPort.Data.DownstreamPort; } + DownstreamRequest.RequestUri = uriBuilder.Uri; try diff --git a/src/Ocelot/Logging/OcelotDiagnosticListener.cs b/src/Ocelot/Logging/OcelotDiagnosticListener.cs index 53d6e14c..ee0b5b8a 100644 --- a/src/Ocelot/Logging/OcelotDiagnosticListener.cs +++ b/src/Ocelot/Logging/OcelotDiagnosticListener.cs @@ -1,6 +1,8 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DiagnosticAdapter; +using Butterfly.Client.AspNetCore; +using Butterfly.OpenTracing; namespace Ocelot.Logging { @@ -17,6 +19,7 @@ namespace Ocelot.Logging public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { _logger.LogTrace($"MiddlewareStarting: {name}; {httpContext.Request.Path}"); + Event(httpContext, $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] @@ -29,6 +32,13 @@ namespace Ocelot.Logging public virtual void OnMiddlewareFinished(HttpContext httpContext, string name) { _logger.LogTrace($"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); + Event(httpContext, $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); + } + + private void Event(HttpContext httpContext, string @event) + { + var span = httpContext.GetSpan(); + span?.Log(LogField.CreateNew().Event(@event)); } } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index d2a00af1..eb4ce773 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -1,46 +1,47 @@ - - - netcoreapp2.0 - 2.0.0 - 2.0.0 - This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. - Ocelot - 0.0.0-dev - Ocelot - Ocelot - API Gateway;.NET core - https://github.com/TomPallister/Ocelot - https://github.com/TomPallister/Ocelot - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - - - full - True - - - - - - - - - - - - - - - - - - - - - - + + + netcoreapp2.0 + 2.0.0 + 2.0.0 + This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. + Ocelot + 0.0.0-dev + Ocelot + Ocelot + API Gateway;.NET core + https://github.com/TomPallister/Ocelot + https://github.com/TomPallister/Ocelot + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + + + full + True + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index 9a7d0b76..5b8ef968 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -1,20 +1,22 @@ -using System.Threading.Tasks; -using Ocelot.Responses; -using Ocelot.Requester.QoS; -using System.Net.Http; - -namespace Ocelot.Request.Builder -{ - public sealed class HttpRequestCreator : IRequestCreator - { - public async Task> Build( - HttpRequestMessage httpRequestMessage, - bool isQos, - IQoSProvider qosProvider, - bool useCookieContainer, - bool allowAutoRedirect) - { - return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer)); - } - } -} \ No newline at end of file +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Requester.QoS; +using System.Net.Http; + +namespace Ocelot.Request.Builder +{ + public sealed class HttpRequestCreator : IRequestCreator + { + public async Task> Build( + HttpRequestMessage httpRequestMessage, + bool isQos, + IQoSProvider qosProvider, + bool useCookieContainer, + bool allowAutoRedirect, + string reRouteKey, + bool isTracing) + { + return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer, reRouteKey, isTracing)); + } + } +} diff --git a/src/Ocelot/Request/Builder/IRequestCreator.cs b/src/Ocelot/Request/Builder/IRequestCreator.cs index ab678582..abde4a6b 100644 --- a/src/Ocelot/Request/Builder/IRequestCreator.cs +++ b/src/Ocelot/Request/Builder/IRequestCreator.cs @@ -1,18 +1,20 @@ -namespace Ocelot.Request.Builder -{ - using System.Net.Http; - using System.Threading.Tasks; - - using Ocelot.Requester.QoS; - using Ocelot.Responses; - - public interface IRequestCreator - { - Task> Build( - HttpRequestMessage httpRequestMessage, - bool isQos, - IQoSProvider qosProvider, - bool useCookieContainer, - bool allowAutoRedirect); - } -} +namespace Ocelot.Request.Builder +{ + using System.Net.Http; + using System.Threading.Tasks; + + using Ocelot.Requester.QoS; + using Ocelot.Responses; + + public interface IRequestCreator + { + Task> Build( + HttpRequestMessage httpRequestMessage, + bool isQos, + IQoSProvider qosProvider, + bool useCookieContainer, + bool allowAutoRedirect, + string reRouteKe, + bool isTracing); + } +} diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs index dd1ad429..4160db31 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -1,66 +1,67 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Ocelot.Infrastructure.RequestData; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Request.Builder; -using Ocelot.Requester.QoS; - -namespace Ocelot.Request.Middleware -{ - public class HttpRequestBuilderMiddleware : OcelotMiddleware - { - private readonly RequestDelegate _next; - private readonly IRequestCreator _requestCreator; - private readonly IOcelotLogger _logger; - private readonly IQosProviderHouse _qosProviderHouse; - - public HttpRequestBuilderMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRequestScopedDataRepository requestScopedDataRepository, - IRequestCreator requestCreator, - IQosProviderHouse qosProviderHouse) - :base(requestScopedDataRepository) - { - _next = next; - _requestCreator = requestCreator; - _qosProviderHouse = qosProviderHouse; - _logger = loggerFactory.CreateLogger(); - } - - public async Task Invoke(HttpContext context) - { - var qosProvider = _qosProviderHouse.Get(DownstreamRoute.ReRoute); - - if (qosProvider.IsError) - { - _logger.LogDebug("IQosProviderHouse returned an error, setting pipeline error"); - - SetPipelineError(qosProvider.Errors); - - return; - } - - var buildResult = await _requestCreator.Build( - DownstreamRequest, - DownstreamRoute.ReRoute.IsQos, - qosProvider.Data, - DownstreamRoute.ReRoute.HttpHandlerOptions.UseCookieContainer, - DownstreamRoute.ReRoute.HttpHandlerOptions.AllowAutoRedirect); - - if (buildResult.IsError) - { - _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); - - SetPipelineError(buildResult.Errors); - - return; - } - _logger.LogDebug("setting upstream request"); - - SetUpstreamRequestForThisRequest(buildResult.Data); - - await _next.Invoke(context); - } - } -} \ No newline at end of file +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Builder; +using Ocelot.Requester.QoS; + +namespace Ocelot.Request.Middleware +{ + public class HttpRequestBuilderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IRequestCreator _requestCreator; + private readonly IOcelotLogger _logger; + private readonly IQosProviderHouse _qosProviderHouse; + + public HttpRequestBuilderMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IRequestCreator requestCreator, + IQosProviderHouse qosProviderHouse) + :base(requestScopedDataRepository) + { + _next = next; + _requestCreator = requestCreator; + _qosProviderHouse = qosProviderHouse; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + var qosProvider = _qosProviderHouse.Get(DownstreamRoute.ReRoute); + + if (qosProvider.IsError) + { + _logger.LogDebug("IQosProviderHouse returned an error, setting pipeline error"); + + SetPipelineError(qosProvider.Errors); + + return; + } + + var buildResult = await _requestCreator.Build( + DownstreamRequest, + DownstreamRoute.ReRoute.IsQos, + qosProvider.Data, + DownstreamRoute.ReRoute.HttpHandlerOptions.UseCookieContainer, + DownstreamRoute.ReRoute.HttpHandlerOptions.AllowAutoRedirect, + DownstreamRoute.ReRoute.ReRouteKey, + DownstreamRoute.ReRoute.HttpHandlerOptions.UseTracing); + + if (buildResult.IsError) + { + _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); + SetPipelineError(buildResult.Errors); + return; + } + + _logger.LogDebug("setting upstream request"); + + SetUpstreamRequestForThisRequest(buildResult.Data); + + await _next.Invoke(context); + } + } +} diff --git a/src/Ocelot/Request/Request.cs b/src/Ocelot/Request/Request.cs index 79c170ea..3bb670f6 100644 --- a/src/Ocelot/Request/Request.cs +++ b/src/Ocelot/Request/Request.cs @@ -1,28 +1,35 @@ -using System.Net.Http; -using Ocelot.Requester.QoS; - -namespace Ocelot.Request -{ - public class Request - { - public Request( - HttpRequestMessage httpRequestMessage, - bool isQos, - IQoSProvider qosProvider, - bool allowAutoRedirect, - bool useCookieContainer) - { - HttpRequestMessage = httpRequestMessage; - IsQos = isQos; - QosProvider = qosProvider; - AllowAutoRedirect = allowAutoRedirect; - UseCookieContainer = useCookieContainer; - } - - public HttpRequestMessage HttpRequestMessage { get; private set; } - public bool IsQos { get; private set; } - public IQoSProvider QosProvider { get; private set; } - public bool AllowAutoRedirect { get; private set; } - public bool UseCookieContainer { get; private set; } - } -} +using System.Net.Http; +using Ocelot.Requester.QoS; + +namespace Ocelot.Request +{ + public class Request + { + public Request( + HttpRequestMessage httpRequestMessage, + bool isQos, + IQoSProvider qosProvider, + bool allowAutoRedirect, + bool useCookieContainer, + string reRouteKey, + bool isTracing + ) + { + HttpRequestMessage = httpRequestMessage; + IsQos = isQos; + QosProvider = qosProvider; + AllowAutoRedirect = allowAutoRedirect; + UseCookieContainer = useCookieContainer; + ReRouteKey = reRouteKey; + IsTracing = isTracing; + } + + public HttpRequestMessage HttpRequestMessage { get; private set; } + public bool IsQos { get; private set; } + public bool IsTracing { get; private set; } + public IQoSProvider QosProvider { get; private set; } + public bool AllowAutoRedirect { get; private set; } + public bool UseCookieContainer { get; private set; } + public string ReRouteKey { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs new file mode 100644 index 00000000..0a7d6947 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerHouse : IDelegatingHandlerHandlerHouse + { + private readonly IDelegatingHandlerHandlerProviderFactory _factory; + private readonly ConcurrentDictionary _housed; + + public DelegatingHandlerHandlerHouse(IDelegatingHandlerHandlerProviderFactory factory) + { + _factory = factory; + _housed = new ConcurrentDictionary(); + } + + public Response Get(Request.Request request) + { + try + { + if (_housed.TryGetValue(request.ReRouteKey, out var provider)) + { + //todo once day we might need a check here to see if we need to create a new provider + provider = _housed[request.ReRouteKey]; + return new OkResponse(provider); + } + + provider = _factory.Get(request); + AddHoused(request.ReRouteKey, provider); + return new OkResponse(provider); + } + catch (Exception ex) + { + return new ErrorResponse(new List() + { + new UnableToFindDelegatingHandlerProviderError($"unabe to find delegating handler provider for {request.ReRouteKey} exception is {ex}") + }); + } + } + + private void AddHoused(string key, IDelegatingHandlerHandlerProvider provider) + { + _housed.AddOrUpdate(key, provider, (k, v) => provider); + } + } +} diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs new file mode 100644 index 00000000..71684c52 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerProvider : IDelegatingHandlerHandlerProvider + { + private readonly Dictionary> _handlers; + + public DelegatingHandlerHandlerProvider() + { + _handlers = new Dictionary>(); + } + + public void Add(Func handler) + { + var key = _handlers.Count == 0 ? 0 : _handlers.Count + 1; + _handlers[key] = handler; + } + + public List> Get() + { + return _handlers.Count > 0 ? _handlers.OrderBy(x => x.Key).Select(x => x.Value).ToList() : new List>(); + } + } +} diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs new file mode 100644 index 00000000..1e6cb4c4 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using Ocelot.Logging; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerProviderFactory : IDelegatingHandlerHandlerProviderFactory + { + private readonly ITracingHandler _tracingHandler; + private readonly IOcelotLoggerFactory _loggerFactory; + private readonly IDelegatingHandlerHandlerProvider _allRoutesProvider; + + public DelegatingHandlerHandlerProviderFactory(IOcelotLoggerFactory loggerFactory, IDelegatingHandlerHandlerProvider allRoutesProvider, ITracingHandler tracingHandler) + { + _tracingHandler = tracingHandler; + _loggerFactory = loggerFactory; + _allRoutesProvider = allRoutesProvider; + } + + public IDelegatingHandlerHandlerProvider Get(Request.Request request) + { + var handlersAppliedToAll = _allRoutesProvider.Get(); + + var provider = new DelegatingHandlerHandlerProvider(); + + foreach (var handler in handlersAppliedToAll) + { + provider.Add(handler); + } + + if (request.IsTracing) + { + provider.Add(() => (DelegatingHandler)_tracingHandler); + } + + if (request.IsQos) + { + provider.Add(() => new PollyCircuitBreakingDelegatingHandler(request.QosProvider, _loggerFactory)); + } + + return provider; + } + } +} diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 257c4222..561b5205 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,66 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Requester.QoS; - -namespace Ocelot.Requester -{ - internal class HttpClientBuilder : IHttpClientBuilder - { - private readonly Dictionary> _handlers = new Dictionary>(); - - public IHttpClientBuilder WithQos(IQoSProvider qosProvider, IOcelotLogger logger) - { - _handlers.Add(5000, () => new PollyCircuitBreakingDelegatingHandler(qosProvider, logger)); - - return this; - } - - public IHttpClient Create(bool useCookies, bool allowAutoRedirect) - { - var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = allowAutoRedirect, UseCookies = useCookies}; - - var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler)); - - return new HttpClientWrapper(client); - } - - private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler) - { - _handlers - .OrderByDescending(handler => handler.Key) - .Select(handler => handler.Value) - .Reverse() - .ToList() - .ForEach(handler => - { - var delegatingHandler = handler(); - delegatingHandler.InnerHandler = httpMessageHandler; - httpMessageHandler = delegatingHandler; - }); - return httpMessageHandler; - } - } - - /// - /// This class was made to make unit testing easier when HttpClient is used. - /// - internal class HttpClientWrapper : IHttpClient - { - public HttpClient Client { get; } - - public HttpClientWrapper(HttpClient client) - { - Client = client; - } - - public Task SendAsync(HttpRequestMessage request) - { - return Client.SendAsync(request); - } - } -} +using System.Linq; +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class HttpClientBuilder : IHttpClientBuilder + { + private readonly IDelegatingHandlerHandlerHouse _house; + + public HttpClientBuilder(IDelegatingHandlerHandlerHouse house) + { + _house = house; + } + + public IHttpClient Create(Request.Request request) + { + var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = request.AllowAutoRedirect, UseCookies = request.UseCookieContainer}; + + var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler, request)); + + return new HttpClientWrapper(client); + } + + private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, Request.Request request) + { + var provider = _house.Get(request); + + //todo handle error + provider.Data.Get() + .Select(handler => handler) + .Reverse() + .ToList() + .ForEach(handler => + { + var delegatingHandler = handler(); + delegatingHandler.InnerHandler = httpMessageHandler; + httpMessageHandler = delegatingHandler; + }); + return httpMessageHandler; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 29bb5c3e..9fa3b271 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Net.Http; using System.Threading.Tasks; -using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Responses; using Polly.CircuitBreaker; @@ -14,21 +12,24 @@ namespace Ocelot.Requester { private readonly IHttpClientCache _cacheHandlers; private readonly IOcelotLogger _logger; + private readonly IDelegatingHandlerHandlerHouse _house; public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, - IHttpClientCache cacheHandlers) + IHttpClientCache cacheHandlers, + IDelegatingHandlerHandlerHouse house) { _logger = loggerFactory.CreateLogger(); _cacheHandlers = cacheHandlers; + _house = house; } public async Task> GetResponse(Request.Request request) { - var builder = new HttpClientBuilder(); + var builder = new HttpClientBuilder(_house); - var cacheKey = GetCacheKey(request, builder); + var cacheKey = GetCacheKey(request); - var httpClient = GetHttpClient(cacheKey, builder, request.UseCookieContainer, request.AllowAutoRedirect); + var httpClient = GetHttpClient(cacheKey, builder, request); try { @@ -56,28 +57,28 @@ namespace Ocelot.Requester } - private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, bool useCookieContainer, bool allowAutoRedirect) + private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, Request.Request request) { var httpClient = _cacheHandlers.Get(cacheKey); if (httpClient == null) { - httpClient = builder.Create(useCookieContainer, allowAutoRedirect); + httpClient = builder.Create(request); } + return httpClient; } - private string GetCacheKey(Request.Request request, IHttpClientBuilder builder) + private string GetCacheKey(Request.Request request) { - string baseUrl = $"{request.HttpRequestMessage.RequestUri.Scheme}://{request.HttpRequestMessage.RequestUri.Authority}"; + var baseUrl = $"{request.HttpRequestMessage.RequestUri.Scheme}://{request.HttpRequestMessage.RequestUri.Authority}"; if (request.IsQos) { - builder.WithQos(request.QosProvider, _logger); baseUrl = $"{baseUrl}{request.QosProvider.CircuitBreaker.CircuitBreakerPolicy.PolicyKey}"; } return baseUrl; } } -} +} \ No newline at end of file diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs new file mode 100644 index 00000000..21e74e48 --- /dev/null +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -0,0 +1,23 @@ +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + /// + /// This class was made to make unit testing easier when HttpClient is used. + /// + internal class HttpClientWrapper : IHttpClient + { + public HttpClient Client { get; } + + public HttpClientWrapper(HttpClient client) + { + Client = client; + } + + public Task SendAsync(HttpRequestMessage request) + { + return Client.SendAsync(request); + } + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs new file mode 100644 index 00000000..78dd1fc1 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerHouse + { + Response Get(Request.Request request); + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs new file mode 100644 index 00000000..addaaeb7 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerProvider + { + void Add(Func handler); + List> Get(); + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs new file mode 100644 index 00000000..dcf007d4 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerProviderFactory + { + IDelegatingHandlerHandlerProvider Get(Request.Request request); + } +} diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs index 6de5f87a..d6b43501 100644 --- a/src/Ocelot/Requester/IHttpClientBuilder.cs +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -1,27 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Requester.QoS; -using System.Net; +using System.Net.Http; +using Ocelot.Configuration; namespace Ocelot.Requester { public interface IHttpClientBuilder { - /// - /// Sets a PollyCircuitBreakingDelegatingHandler . - /// - IHttpClientBuilder WithQos(IQoSProvider qosProvider, IOcelotLogger logger); - - /// /// Creates the - /// - /// Defines if http client should use cookie container - /// Defines if http client should allow auto redirect - IHttpClient Create(bool useCookies, bool allowAutoRedirect); + /// + /// + IHttpClient Create(Request.Request request); } } diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs index b2116818..81d9fa22 100644 --- a/src/Ocelot/Requester/IHttpRequester.cs +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -7,7 +7,5 @@ namespace Ocelot.Requester public interface IHttpRequester { Task> GetResponse(Request.Request request); - - } } diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index 412983de..ae3e7c5f 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -1,9 +1,8 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; +using System.Threading.Tasks; namespace Ocelot.Requester.Middleware { @@ -16,7 +15,8 @@ namespace Ocelot.Requester.Middleware public HttpRequesterMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester, - IRequestScopedDataRepository requestScopedDataRepository) + IRequestScopedDataRepository requestScopedDataRepository +) :base(requestScopedDataRepository) { _next = next; @@ -25,7 +25,7 @@ namespace Ocelot.Requester.Middleware } public async Task Invoke(HttpContext context) - { + { var response = await _requester.GetResponse(Request); if (response.IsError) @@ -41,4 +41,4 @@ namespace Ocelot.Requester.Middleware SetHttpResponseMessageThisRequest(response.Data); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs new file mode 100644 index 00000000..ff588fe8 --- /dev/null +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Butterfly.Client.Tracing; +using Butterfly.OpenTracing; + +namespace Ocelot.Requester +{ + public interface ITracingHandler + { + } + + public class NoTracingHandler : DelegatingHandler, ITracingHandler + { + + } + + public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler + { + private readonly IServiceTracer _tracer; + private const string prefix_spanId = "ot-spanId"; + + public OcelotHttpTracingHandler(IServiceTracer tracer, HttpMessageHandler httpMessageHandler = null) + { + _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); + InnerHandler = httpMessageHandler ?? new HttpClientHandler(); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken)); + } + + protected virtual async Task TracingSendAsync(ISpan span, HttpRequestMessage request, CancellationToken cancellationToken) + { + IEnumerable traceIdVals = null; + if (request.Headers.TryGetValues(prefix_spanId, out traceIdVals)) + { + request.Headers.Remove(prefix_spanId); + request.Headers.TryAddWithoutValidation(prefix_spanId, span.SpanContext.SpanId); + }; + + span.Tags.Client().Component("HttpClient") + .HttpMethod(request.Method.Method) + .HttpUrl(request.RequestUri.OriginalString) + .HttpHost(request.RequestUri.Host) + .HttpPath(request.RequestUri.PathAndQuery) + .PeerAddress(request.RequestUri.OriginalString) + .PeerHostName(request.RequestUri.Host) + .PeerPort(request.RequestUri.Port); + + _tracer.Tracer.Inject(span.SpanContext, request.Headers, (c, k, v) => + { + if (!c.Contains(k)) + { + c.Add(k, v); + }; + }); + + span.Log(LogField.CreateNew().ClientSend()); + + var responseMessage = await base.SendAsync(request, cancellationToken); + + span.Log(LogField.CreateNew().ClientReceive()); + + return responseMessage; + } + } +} diff --git a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs index ce8eec1a..45b0ac12 100644 --- a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -16,10 +16,10 @@ namespace Ocelot.Requester public PollyCircuitBreakingDelegatingHandler( IQoSProvider qoSProvider, - IOcelotLogger logger) + IOcelotLoggerFactory loggerFactory) { _qoSProvider = qoSProvider; - _logger = logger; + _logger = loggerFactory.CreateLogger(); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs b/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs new file mode 100644 index 00000000..fd8aeb9e --- /dev/null +++ b/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class UnableToFindDelegatingHandlerProviderError : Error + { + public UnableToFindDelegatingHandlerProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindDelegatingHandlerProviderError) + { + } + } +} diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index b6ed2717..d0c12642 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Ocelot.Errors; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Ocelot.Responder.Middleware { @@ -22,7 +22,8 @@ namespace Ocelot.Responder.Middleware IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository, - IErrorsToHttpStatusCodeMapper codeMapper) + IErrorsToHttpStatusCodeMapper codeMapper + ) :base(requestScopedDataRepository) { _next = next; @@ -33,14 +34,13 @@ namespace Ocelot.Responder.Middleware } public async Task Invoke(HttpContext context) - { + { await _next.Invoke(context); if (PipelineError) { var errors = PipelineErrors; _logger.LogError($"{PipelineErrors.Count} pipeline errors found in {MiddlewareName}. Setting error response status code"); - SetErrorResponse(context, errors); } else @@ -50,10 +50,10 @@ namespace Ocelot.Responder.Middleware } } - private void SetErrorResponse(HttpContext context, List errors) - { + private void SetErrorResponse(HttpContext context, List errors) + { var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs index c21650f1..ef882a50 100644 --- a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Consul; using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; using Ocelot.Values; namespace Ocelot.ServiceDiscovery @@ -11,13 +12,18 @@ namespace Ocelot.ServiceDiscovery public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider { private readonly ConsulRegistryConfiguration _consulConfig; + private readonly IOcelotLogger _logger; private readonly ConsulClient _consul; private const string VersionPrefix = "version-"; - public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration) - { + public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration, IOcelotLoggerFactory factory) + {; + _logger = factory.CreateLogger(); + var consulHost = string.IsNullOrEmpty(consulRegistryConfiguration?.HostName) ? "localhost" : consulRegistryConfiguration.HostName; + var consulPort = consulRegistryConfiguration?.Port ?? 8500; + _consulConfig = new ConsulRegistryConfiguration(consulHost, consulPort, consulRegistryConfiguration?.KeyOfServiceInConsul); _consul = new ConsulClient(config => @@ -30,7 +36,19 @@ namespace Ocelot.ServiceDiscovery { var queryResult = await _consul.Health.Service(_consulConfig.KeyOfServiceInConsul, string.Empty, true); - var services = queryResult.Response.Select(BuildService); + var services = new List(); + + foreach (var serviceEntry in queryResult.Response) + { + if (IsValid(serviceEntry)) + { + services.Add(BuildService(serviceEntry)); + } + else + { + _logger.LogError($"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"); + } + } return services.ToList(); } @@ -45,6 +63,16 @@ namespace Ocelot.ServiceDiscovery serviceEntry.Service.Tags ?? Enumerable.Empty()); } + private bool IsValid(ServiceEntry serviceEntry) + { + if (serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) + { + return false; + } + + return true; + } + private string GetVersionFromStrings(IEnumerable strings) { return strings diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index eb73dae0..e8c97bd4 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,11 +1,19 @@ using System.Collections.Generic; using Ocelot.Configuration; +using Ocelot.Logging; using Ocelot.Values; namespace Ocelot.ServiceDiscovery { public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { + private readonly IOcelotLoggerFactory _factory; + + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory) + { + _factory = factory; + } + public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, ReRoute reRoute) { if (reRoute.UseServiceDiscovery) @@ -28,7 +36,7 @@ namespace Ocelot.ServiceDiscovery private IServiceDiscoveryProvider GetServiceDiscoveryProvider(string keyOfServiceInConsul, string providerHostName, int providerPort) { var consulRegistryConfiguration = new ConsulRegistryConfiguration(providerHostName, providerPort, keyOfServiceInConsul); - return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory); } } } diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs new file mode 100644 index 00000000..bb2a43df --- /dev/null +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HttpDelegatingHandlersTests + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public HttpDelegatingHandlersTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_call_handlers() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 61879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + var handlerOne = new FakeHandler(); + var handlerTwo = new FakeHandler(); + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:61879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithHandlers(handlerOne, handlerTwo)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheHandlersAreCalledCorrectly(handlerOne, handlerTwo)) + .BDDfy(); + } + + private void ThenTheHandlersAreCalledCorrectly(FakeHandler one, FakeHandler two) + { + one.TimeCalled.ShouldBeLessThan(two.TimeCalled); + } + + class FakeHandler : DelegatingHandler + { + + public DateTime TimeCalled { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return await base.SendAsync(request, cancellationToken); + } + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builder.Start(); + } + + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index bd41f007..bcd017b4 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -88,6 +88,38 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunningWithHandlers(DelegatingHandler handlerOne, DelegatingHandler handlerTwo) + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(() => handlerOne) + .AddDelegatingHandler(() => handlerTwo); + }); + _webHostBuilder.ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("configuration.json"); + config.AddEnvironmentVariables(); + }).Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + /// /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// diff --git a/test/Ocelot.IntegrationTests/RaftStartup.cs b/test/Ocelot.IntegrationTests/RaftStartup.cs index 70a91e2c..bb9f26d9 100644 --- a/test/Ocelot.IntegrationTests/RaftStartup.cs +++ b/test/Ocelot.IntegrationTests/RaftStartup.cs @@ -1,52 +1,53 @@ -using System; -using System.IO; -using System.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Ocelot.Raft; -using Rafty.Concensus; -using Rafty.FiniteStateMachine; -using Rafty.Infrastructure; -using Rafty.Log; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; - -namespace Ocelot.IntegrationTests -{ - public class RaftStartup - { - public RaftStartup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("peers.json", optional: true, reloadOnChange: true) - .AddJsonFile("configuration.json") - .AddEnvironmentVariables(); - - Configuration = builder.Build(); - } - - public IConfiguration Configuration { get; } - - public virtual void ConfigureServices(IServiceCollection services) - { - services - .AddOcelot(Configuration) - .AddAdministration("/administration", "secret") - .AddRafty(); - } - - public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - app.UseOcelot().Wait(); - } - } -} +using System; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; + +namespace Ocelot.IntegrationTests +{ + public class RaftStartup + { + public RaftStartup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("peers.json", optional: true, reloadOnChange: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; } + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot(Configuration) + .AddAdministration("/administration", "secret") + .AddRafty() + ; + } + + public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseOcelot().Wait(); + } + } +} diff --git a/test/Ocelot.ManualTest/ManualTestStartup.cs b/test/Ocelot.ManualTest/ManualTestStartup.cs index 329984a1..dd5cf979 100644 --- a/test/Ocelot.ManualTest/ManualTestStartup.cs +++ b/test/Ocelot.ManualTest/ManualTestStartup.cs @@ -1,40 +1,37 @@ -using System; -using CacheManager.Core; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; - -namespace Ocelot.ManualTest -{ - public class ManualTestStartup - { - public void ConfigureServices(IServiceCollection services) - { - Action settings = (x) => - { - x.WithDictionaryHandle(); - }; - - services.AddAuthentication() - .AddJwtBearer("TestKey", x => - { - x.Authority = "test"; - x.Audience = "test"; - }); - - services.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - } - - public void Configure(IApplicationBuilder app) - { - app.UseOcelot().Wait(); - } - } -} +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.ManualTest +{ + public class ManualTestStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication() + .AddJwtBearer("TestKey", x => + { + x.Authority = "test"; + x.Audience = "test"; + }); + + services.AddOcelot() + .AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) + .AddOpenTracing(option => + { + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot.ManualTest"; + }) + .AddAdministration("/administration", "secret"); + } + + public void Configure(IApplicationBuilder app) + { + app.UseOcelot().Wait(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json index a8d01b24..45f184bc 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/configuration.json @@ -1,287 +1,319 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 52876 - } - ], - "UpstreamPathTemplate": "/identityserverexample", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "AuthenticationOptions": { - "AuthenticationProviderKey": "TestKey", - "AllowedScopes": [ - "openid", - "offline_access" - ] - }, - "AddHeadersToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddClaimsToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddQueriesToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "RouteClaimsRequirement": { - "UserType": "registered" - }, - "RequestIdKey": "OcRequestId" - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 443 - } - ], - "UpstreamPathTemplate": "/posts", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Get" ], - "RequestIdKey": "ReRouteRequestId", - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}/comments", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}/comments", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/comments", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/comments", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts", - "UpstreamHttpMethod": [ "Post" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Patch" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Delete" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/api/products", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/api/products/{productId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products/{productId}", - "UpstreamHttpMethod": [ "Get" ], - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/api/products", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products", - "UpstreamHttpMethod": [ "Post" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/api/products/{productId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products/{productId}", - "UpstreamHttpMethod": [ "Put" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "www.bbc.co.uk", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/bbc/", - "UpstreamHttpMethod": [ "Get" ] - } - ], - - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId" - } -} +{ + "ReRoutes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/api/values", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5001 + } + ], + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + } + }, + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 52876 + } + ], + "UpstreamPathTemplate": "/identityserverexample", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "AuthenticationOptions": { + "AuthenticationProviderKey": "TestKey", + "AllowedScopes": [ + "openid", + "offline_access" + ] + }, + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "RouteClaimsRequirement": { + "UserType": "registered" + }, + "RequestIdKey": "OcRequestId" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": [ "Get" ], + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Get" ], + "RequestIdKey": "ReRouteRequestId", + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}/comments", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}/comments", + "UpstreamHttpMethod": [ "Get" ], + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/comments", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/comments", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": [ "Post" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Patch" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Delete" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": [ "Get" ], + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": [ "Post" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": [ "Put" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "www.bbc.co.uk", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/bbc/", + "UpstreamHttpMethod": [ "Get" ] + } + ], + + "GlobalConfiguration": { + "RequestIdKey": "ot-traceid" + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 22a73e2a..b2ff9a00 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -471,7 +471,7 @@ namespace Ocelot.UnitTests.Configuration { var reRouteOptions = new ReRouteOptionsBuilder() .Build(); - var httpHandlerOptions = new HttpHandlerOptions(true, true); + var httpHandlerOptions = new HttpHandlerOptions(true, true,false); this.Given(x => x.GivenTheConfigIs(new FileConfiguration { diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index 69f414e5..3821ef6b 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -23,7 +23,7 @@ namespace Ocelot.UnitTests.Configuration public void should_create_options_with_useCookie_and_allowAutoRedirect_true_as_default() { var fileReRoute = new FileReRoute(); - var expectedOptions = new HttpHandlerOptions(true, true); + var expectedOptions = new HttpHandlerOptions(true, true, false); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -39,11 +39,12 @@ namespace Ocelot.UnitTests.Configuration HttpHandlerOptions = new FileHttpHandlerOptions { AllowAutoRedirect = false, - UseCookieContainer = false + UseCookieContainer = false, + UseTracing = false } }; - var expectedOptions = new HttpHandlerOptions(false, false); + var expectedOptions = new HttpHandlerOptions(false, false, false); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -66,6 +67,7 @@ namespace Ocelot.UnitTests.Configuration _httpHandlerOptions.ShouldNotBeNull(); _httpHandlerOptions.AllowAutoRedirect.ShouldBe(options.AllowAutoRedirect); _httpHandlerOptions.UseCookieContainer.ShouldBe(options.UseCookieContainer); + _httpHandlerOptions.UseTracing.ShouldBe(options.UseTracing); } } } diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index ef783591..e5839856 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; using CacheManager.Core; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Internal; @@ -13,8 +12,10 @@ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; -using Ocelot.Logging; +using Ocelot.Requester; +using Ocelot.UnitTests.Requester; using Shouldly; +using System; using TestStack.BDDfy; using Xunit; @@ -22,11 +23,11 @@ namespace Ocelot.UnitTests.DependencyInjection { public class OcelotBuilderTests { - private IServiceCollection _services; + private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; - private IConfiguration _configRoot; + private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; - private int _maxRetries; + private readonly int _maxRetries; public OcelotBuilderTests() { @@ -40,6 +41,19 @@ namespace Ocelot.UnitTests.DependencyInjection } private Exception _ex; + [Fact] + public void should_add_delegating_handlers() + { + var fakeOne = new FakeDelegatingHandler(0); + var fakeTwo = new FakeDelegatingHandler(1); + + this.Given(x => WhenISetUpOcelotServices()) + .When(x => AddDelegate(fakeOne)) + .And(x => AddDelegate(fakeTwo)) + .Then(x => ThenTheProviderIsRegisteredAndReturnsHandlers()) + .BDDfy(); + } + [Fact] public void should_set_up_services() { @@ -56,7 +70,7 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } - + [Fact] public void should_set_up_cache_manager() { @@ -76,7 +90,7 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } - [Fact] + [Fact] public void should_set_up_rafty() { this.Given(x => WhenISetUpOcelotServices()) @@ -96,6 +110,16 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_set_up_tracing() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpOpentracing()) + .When(x => WhenIAccessOcelotHttpTracingHandler()) + .BDDfy(); + } + + [Fact] public void should_set_up_without_passing_in_config() { @@ -111,6 +135,17 @@ namespace Ocelot.UnitTests.DependencyInjection path.Path.ShouldBe("/administration"); } + private void ThenTheProviderIsRegisteredAndReturnsHandlers() + { + _serviceProvider = _services.BuildServiceProvider(); + var provider = _serviceProvider.GetService(); + var handlers = provider.Get(); + var handler = (FakeDelegatingHandler)handlers[0].Invoke(); + handler.Order.ShouldBe(0); + handler = (FakeDelegatingHandler)handlers[1].Invoke(); + handler.Order.ShouldBe(1); + } + private void OnlyOneVersionOfEachCacheIsRegistered() { var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); @@ -149,6 +184,11 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void AddDelegate(DelegatingHandler handler) + { + _ocelotBuilder.AddDelegatingHandler(() => handler); + } + private void ThenAnOcelotBuilderIsReturned() { _ocelotBuilder.ShouldBeOfType(); @@ -193,6 +233,24 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void WhenISetUpOpentracing() + { + try + { + _ocelotBuilder.AddOpenTracing( + option => + { + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot.ManualTest"; + } + ); + } + catch (Exception e) + { + _ex = e; + } + } + private void WhenIAccessLoggerFactory() { try @@ -205,6 +263,18 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void WhenIAccessOcelotHttpTracingHandler() + { + try + { + var tracingHandler = _serviceProvider.GetService(); + } + catch (Exception e) + { + _ex = e; + } + } + private void WhenIValidateScopes() { try diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs index 10cbc120..b4d6acdc 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -17,6 +17,7 @@ using Ocelot.Requester.QoS; using Ocelot.Configuration; using Microsoft.AspNetCore.Builder; + using Ocelot.Errors; public class HttpRequestBuilderMiddlewareTests : ServerHostedMiddlewareTest { @@ -51,18 +52,39 @@ new ReRouteBuilder() .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true,false)) .Build()); this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) .And(x => x.GivenTheQosProviderHouseReturns(new OkResponse(new NoQoSProvider()))) .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false))) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false, "", false))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); } + [Fact] + public void should_call_scoped_data_repository_QosProviderError() + { + + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheQosProviderHouseReturns(new ErrorResponse(It.IsAny()))) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false, "", false))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryQosProviderError()) + .BDDfy(); + } + + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) { services.AddSingleton(); @@ -109,6 +131,8 @@ It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(_request); } @@ -118,5 +142,11 @@ _scopedRepository .Verify(x => x.Add("Request", _request.Data), Times.Once()); } + + private void ThenTheScopedDataRepositoryQosProviderError() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once()); + } } } diff --git a/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs index f4f67ca0..dbfc8643 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs @@ -1,79 +1,86 @@ -namespace Ocelot.UnitTests.Request -{ - using System.Net.Http; - - using Ocelot.Request.Builder; - using Ocelot.Requester.QoS; - using Ocelot.Responses; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - - public class HttpRequestCreatorTests - { - private readonly IRequestCreator _requestCreator; - private readonly bool _isQos; - private readonly IQoSProvider _qoSProvider; - private readonly HttpRequestMessage _requestMessage; - private readonly bool _useCookieContainer; - private readonly bool _allowAutoRedirect; - - private Response _response; - - public HttpRequestCreatorTests() - { - _requestCreator = new HttpRequestCreator(); - _isQos = true; - _qoSProvider = new NoQoSProvider(); - _useCookieContainer = false; - _allowAutoRedirect = false; - - _requestMessage = new HttpRequestMessage(); - } - - [Fact] - public void ShouldBuildRequest() - { - this.When(x => x.WhenIBuildARequest()) - .Then(x => x.ThenTheRequestContainsTheRequestMessage()) - .Then(x => x.ThenTheRequestContainsTheIsQos()) - .Then(x => x.ThenTheRequestContainsTheQosProvider()) - .Then(x => x.ThenTheRequestContainsUseCookieContainer()) - .Then(x => x.ThenTheRequestContainsAllowAutoRedirect()) - .BDDfy(); - } - - private void WhenIBuildARequest() - { - _response = _requestCreator.Build(_requestMessage, - _isQos, _qoSProvider, _useCookieContainer, _allowAutoRedirect) - .GetAwaiter() - .GetResult(); - } - - private void ThenTheRequestContainsTheRequestMessage() - { - _response.Data.HttpRequestMessage.ShouldBe(_requestMessage); - } - - private void ThenTheRequestContainsTheIsQos() - { - _response.Data.IsQos.ShouldBe(_isQos); - } - - private void ThenTheRequestContainsTheQosProvider() - { - _response.Data.QosProvider.ShouldBe(_qoSProvider); - } - +namespace Ocelot.UnitTests.Request +{ + using System.Net.Http; + + using Ocelot.Request.Builder; + using Ocelot.Requester.QoS; + using Ocelot.Responses; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class HttpRequestCreatorTests + { + private readonly IRequestCreator _requestCreator; + private readonly bool _isQos; + private readonly IQoSProvider _qoSProvider; + private readonly HttpRequestMessage _requestMessage; + private readonly bool _useCookieContainer; + private readonly bool _allowAutoRedirect; + private Response _response; + private string _reRouteKey; + private readonly bool _useTracing; + + public HttpRequestCreatorTests() + { + _requestCreator = new HttpRequestCreator(); + _isQos = true; + _qoSProvider = new NoQoSProvider(); + _useCookieContainer = false; + _allowAutoRedirect = false; + + _requestMessage = new HttpRequestMessage(); + } + + [Fact] + public void ShouldBuildRequest() + { + this.When(x => x.WhenIBuildARequest()) + .Then(x => x.ThenTheRequestContainsTheRequestMessage()) + .Then(x => x.ThenTheRequestContainsTheIsQos()) + .Then(x => x.ThenTheRequestContainsTheQosProvider()) + .Then(x => x.ThenTheRequestContainsUseCookieContainer()) + .Then(x => x.ThenTheRequestContainsUseTracing()) + .Then(x => x.ThenTheRequestContainsAllowAutoRedirect()) + .BDDfy(); + } + + private void WhenIBuildARequest() + { + _response = _requestCreator.Build(_requestMessage, + _isQos, _qoSProvider, _useCookieContainer, _allowAutoRedirect, _reRouteKey, _useTracing) + .GetAwaiter() + .GetResult(); + } + + private void ThenTheRequestContainsTheRequestMessage() + { + _response.Data.HttpRequestMessage.ShouldBe(_requestMessage); + } + + private void ThenTheRequestContainsTheIsQos() + { + _response.Data.IsQos.ShouldBe(_isQos); + } + + private void ThenTheRequestContainsTheQosProvider() + { + _response.Data.QosProvider.ShouldBe(_qoSProvider); + } + private void ThenTheRequestContainsUseCookieContainer() { _response.Data.UseCookieContainer.ShouldBe(_useCookieContainer); - } - + } + + private void ThenTheRequestContainsUseTracing() + { + _response.Data.IsTracing.ShouldBe(_useTracing); + } + private void ThenTheRequestContainsAllowAutoRedirect() { _response.Data.AllowAutoRedirect.ShouldBe(_allowAutoRedirect); - } - } -} + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs new file mode 100644 index 00000000..f78917d4 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Net.Http; +using Moq; +using Ocelot.Requester; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerHouseTests + { + private readonly DelegatingHandlerHandlerHouse _house; + private Mock _factory; + private readonly Mock _provider; + private Ocelot.Request.Request _request; + private Response _result; + + public DelegatingHandlerHandlerHouseTests() + { + _provider = new Mock(); + _factory = new Mock(); + _house = new DelegatingHandlerHandlerHouse(_factory.Object); + } + + [Fact] + public void should_create_and_store_provider() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenTheFactoryIsCalled(1)) + .And(x => ThenTheProviderIsNotNull()) + .BDDfy(); + } + + [Fact] + public void should_get_provider() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderReturns()) + .And(x => WhenIGet()) + .And(x => GivenTheFactoryIsCleared()) + .When(x => WhenIGet()) + .Then(x => ThenTheFactoryIsCalled(0)) + .And(x => ThenTheProviderIsNotNull()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderThrows()) + .When(x => WhenIGet()) + .And(x => ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenTheProviderThrows() + { + _factory.Setup(x => x.Get(It.IsAny())).Throws(); + } + + private void GivenTheFactoryIsCleared() + { + _factory = new Mock(); + } + + private void ThenTheProviderIsNotNull() + { + _result.Data.ShouldBe(_provider.Object); + } + + private void WhenIGet() + { + _result = _house.Get(_request); + } + + private void GivenTheRequest(Ocelot.Request.Request request) + { + _request = request; + } + + private void GivenTheProviderReturns() + { + _factory.Setup(x => x.Get(It.IsAny())).Returns(_provider.Object); + } + + private void ThenTheFactoryIsCalled(int times) + { + _factory.Verify(x => x.Get(_request), Times.Exactly(times)); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs new file mode 100644 index 00000000..e64df1b9 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Moq; +using Ocelot.Logging; +using Ocelot.Requester; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerProviderFactoryTests + { + private readonly DelegatingHandlerHandlerProviderFactory _factory; + private Mock _loggerFactory; + private Ocelot.Request.Request _request; + private IDelegatingHandlerHandlerProvider _provider; + private readonly Mock _allRoutesProvider; + + public DelegatingHandlerHandlerProviderFactoryTests() + { + _allRoutesProvider = new Mock(); + _loggerFactory = new Mock(); + _factory = new DelegatingHandlerHandlerProviderFactory(_loggerFactory.Object, _allRoutesProvider.Object, null); + } + + [Fact] + public void should_all_from_all_routes_provider_and_qos() + { + var handlers = new List> + { + () => new FakeDelegatingHandler(0), + () => new FakeDelegatingHandler(1) + }; + + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns(handlers)) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(3)) + .And(x => ThenTheDelegatesAreAddedCorrectly()) + .And(x => ThenItIsPolly(2)) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_no_delegates() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), false, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenNoDelegatesAreInTheProvider()) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_qos_delegate() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(1)) + .And(x => ThenItIsPolly(0)) + .BDDfy(); + } + + private void ThenTheDelegatesAreAddedCorrectly() + { + var delegates = _provider.Get(); + var del = delegates[0].Invoke(); + var handler = (FakeDelegatingHandler) del; + handler.Order.ShouldBe(0); + + del = delegates[1].Invoke(); + handler = (FakeDelegatingHandler)del; + handler.Order.ShouldBe(1); + } + + private void GivenTheAllRoutesProviderReturns() + { + _allRoutesProvider.Setup(x => x.Get()).Returns(new List>()); + } + + private void GivenTheAllRoutesProviderReturns(List> handlers) + { + _allRoutesProvider.Setup(x => x.Get()).Returns(handlers); + } + + private void ThenItIsPolly(int i) + { + var delegates = _provider.Get(); + var del = delegates[i].Invoke(); + del.ShouldBeOfType(); + } + + private void ThenThereIsDelegatesInProvider(int count) + { + _provider.ShouldNotBeNull(); + _provider.Get().Count.ShouldBe(count); + } + + private void GivenTheFollowingRequest(Ocelot.Request.Request request) + { + _request = request; + } + + private void WhenIGet() + { + _provider = _factory.Get(_request); + } + + private void ThenNoDelegatesAreInTheProvider() + { + _provider.ShouldNotBeNull(); + _provider.Get().Count.ShouldBe(0); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs new file mode 100644 index 00000000..d93e291a --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Requester; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerProviderTests + { + private readonly DelegatingHandlerHandlerProvider _provider; + private List> _handlers; + + public DelegatingHandlerHandlerProviderTests() + { + _provider = new DelegatingHandlerHandlerProvider(); + } + + [Fact] + public void should_return_empty_list() + { + this.When(x => WhenIGet()) + .Then(x => ThenAnEmptyListIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_get_delegating_handlers_in_order_first_in_first_out() + { + this.Given(x => GivenTheHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenTheHandlersAreReturnedInOrder()) + .BDDfy(); + } + + private void ThenAnEmptyListIsReturned() + { + _handlers.Count.ShouldBe(0); + } + + private void ThenTheHandlersAreReturnedInOrder() + { + var handler = (FakeDelegatingHandler)_handlers[0].Invoke(); + handler.Order.ShouldBe(0); + handler = (FakeDelegatingHandler)_handlers[1].Invoke(); + handler.Order.ShouldBe(1); + } + + private void WhenIGet() + { + _handlers = _provider.Get(); + } + + private void GivenTheHandlers() + { + _provider.Add(() => new FakeDelegatingHandler(0)); + _provider.Add(() => new FakeDelegatingHandler(1)); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs new file mode 100644 index 00000000..545e956e --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.UnitTests.Requester +{ + public class FakeDelegatingHandler : DelegatingHandler + { + public FakeDelegatingHandler() + { + + } + + public FakeDelegatingHandler(int order) + { + Order = order; + } + public int Order {get;private set;} + public DateTime TimeCalled {get;private set;} + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return new HttpResponseMessage(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs new file mode 100644 index 00000000..6081590a --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Moq; +using Ocelot.Requester; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpClientBuilderTests + { + private readonly HttpClientBuilder _builder; + private readonly Mock _house; + private readonly Mock _provider; + private IHttpClientBuilder _builderResult; + private IHttpClient _httpClient; + private HttpResponseMessage _response; + private Ocelot.Request.Request _request; + + public HttpClientBuilderTests() + { + _provider = new Mock(); + _house = new Mock(); + _builder = new HttpClientBuilder(_house.Object); + } + + [Fact] + public void should_build_http_client() + { + this.Given(x => GivenTheProviderReturns()) + .And(x => GivenARequest()) + .And(x => GivenTheHouseReturns()) + .When(x => WhenIBuild()) + .Then(x => ThenTheHttpClientShouldNotBeNull()) + .BDDfy(); + } + + [Fact] + public void should_call_delegating_handlers_in_order() + { + var fakeOne = new FakeDelegatingHandler(); + var fakeTwo = new FakeDelegatingHandler(); + + var handlers = new List>() + { + () => fakeOne, + () => fakeTwo + }; + + this.Given(x => GivenTheProviderReturns(handlers)) + .And(x => GivenARequest()) + .And(x => GivenTheHouseReturns()) + .And(x => WhenIBuild()) + .When(x => WhenICallTheClient()) + .Then(x => ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo)) + .And(x => ThenSomethingIsReturned()) + .BDDfy(); + } + + private void GivenARequest() + { + _request = new Ocelot.Request.Request(null, false, null, false, false, "", false); + } + + private void GivenTheHouseReturns() + { + _house + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(_provider.Object)); + } + + private void ThenSomethingIsReturned() + { + _response.ShouldNotBeNull(); + } + + private void WhenICallTheClient() + { + _response = _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")).GetAwaiter().GetResult(); + } + + private void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) + { + fakeOne.TimeCalled.ShouldBeGreaterThan(fakeTwo.TimeCalled); + } + + private void GivenTheProviderReturns() + { + _provider + .Setup(x => x.Get()) + .Returns(new List>(){ () => new FakeDelegatingHandler()}); + } + + private void GivenTheProviderReturns(List> handlers) + { + _provider + .Setup(x => x.Get()) + .Returns(handlers); + } + + private void WhenIBuild() + { + _httpClient = _builder.Create(_request); + } + + private void ThenTheHttpClientShouldNotBeNull() + { + _httpClient.ShouldNotBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs new file mode 100644 index 00000000..29271671 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Logging; +using Ocelot.Requester; +using Ocelot.Requester.QoS; +using Ocelot.Responses; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using TestStack.BDDfy; +using Xunit; +using Shouldly; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpClientHttpRequesterTest + { + private readonly Mock _cacheHandlers; + private Mock _house; + private Mock _provider; + private Response _response; + private readonly HttpClientHttpRequester _httpClientRequester; + private Ocelot.Request.Request _request; + private Mock _loggerFactory; + private Mock _logger; + + public HttpClientHttpRequesterTest() + { + _provider = new Mock(); + _provider.Setup(x => x.Get()).Returns(new List>()); + _house = new Mock(); + _house.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(_provider.Object)); + _logger = new Mock(); + _loggerFactory = new Mock(); + _loggerFactory + .Setup(x => x.CreateLogger()) + .Returns(_logger.Object); + _cacheHandlers = new Mock(); + _httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _house.Object); + } + + [Fact] + public void should_call_request_correctly() + { + this.Given(x=>x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }, false, new NoQoSProvider(), false, false, "", false))) + .When(x=>x.WhenIGetResponse()) + .Then(x => x.ThenTheResponseIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_request_unable_to_complete_request() + { + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }, false, new NoQoSProvider(), false, false, "", false))) + .When(x => x.WhenIGetResponse()) + .Then(x => x.ThenTheResponseIsCalledError()) + .BDDfy(); + } + + private void GivenTheRequestIs(Ocelot.Request.Request request) + { + _request = request; + } + + private void WhenIGetResponse() + { + _response = _httpClientRequester.GetResponse(_request).Result; + } + + private void ThenTheResponseIsCalledCorrectly() + { + _response.IsError.ShouldBeFalse(); + } + + private void ThenTheResponseIsCalledError() + { + _response.IsError.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 7c6bd487..5658984a 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -1,81 +1,81 @@ -namespace Ocelot.UnitTests.Requester -{ - using System.Net.Http; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Logging; - using Ocelot.Requester; - using Ocelot.Requester.Middleware; - using Ocelot.Requester.QoS; - using Ocelot.Responses; - using TestStack.BDDfy; - using Xunit; - - public class HttpRequesterMiddlewareTests : ServerHostedMiddlewareTest - { - private readonly Mock _requester; - private OkResponse _response; - private OkResponse _request; - - public HttpRequesterMiddlewareTests() - { - _requester = new Mock(); - - GivenTheTestServerIsConfigured(); - } - - [Fact] - public void should_call_scoped_data_repository_correctly() - { - this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),true, new NoQoSProvider(), false, false))) - .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) - .And(x => x.GivenTheScopedRepoReturns()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheScopedRepoIsCalledCorrectly()) - .BDDfy(); - } - - protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) - { - services.AddSingleton(); - services.AddLogging(); - services.AddSingleton(_requester.Object); - services.AddSingleton(ScopedRepository.Object); - } - - protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) - { - app.UseHttpRequesterMiddleware(); - } - - private void GivenTheRequestIs(Ocelot.Request.Request request) - { - _request = new OkResponse(request); - ScopedRepository - .Setup(x => x.Get(It.IsAny())) - .Returns(_request); - } - - private void GivenTheRequesterReturns(HttpResponseMessage response) - { - _response = new OkResponse(response); - _requester - .Setup(x => x.GetResponse(It.IsAny())) - .ReturnsAsync(_response); - } - - private void GivenTheScopedRepoReturns() - { - ScopedRepository - .Setup(x => x.Add(It.IsAny(), _response.Data)) - .Returns(new OkResponse()); - } - - private void ThenTheScopedRepoIsCalledCorrectly() - { - ScopedRepository - .Verify(x => x.Add("HttpResponseMessage", _response.Data), Times.Once()); - } - } -} +namespace Ocelot.UnitTests.Requester +{ + using System.Net.Http; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using Ocelot.Logging; + using Ocelot.Requester; + using Ocelot.Requester.Middleware; + using Ocelot.Requester.QoS; + using Ocelot.Responses; + using TestStack.BDDfy; + using Xunit; + + public class HttpRequesterMiddlewareTests : ServerHostedMiddlewareTest + { + private readonly Mock _requester; + private OkResponse _response; + private OkResponse _request; + + public HttpRequesterMiddlewareTests() + { + _requester = new Mock(); + + GivenTheTestServerIsConfigured(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),true, new NoQoSProvider(), false, false, "", false))) + .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) + .And(x => x.GivenTheScopedRepoReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedRepoIsCalledCorrectly()) + .BDDfy(); + } + + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) + { + services.AddSingleton(); + services.AddLogging(); + services.AddSingleton(_requester.Object); + services.AddSingleton(ScopedRepository.Object); + } + + protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) + { + app.UseHttpRequesterMiddleware(); + } + + private void GivenTheRequestIs(Ocelot.Request.Request request) + { + _request = new OkResponse(request); + ScopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_request); + } + + private void GivenTheRequesterReturns(HttpResponseMessage response) + { + _response = new OkResponse(response); + _requester + .Setup(x => x.GetResponse(It.IsAny())) + .ReturnsAsync(_response); + } + + private void GivenTheScopedRepoReturns() + { + ScopedRepository + .Setup(x => x.Add(It.IsAny(), _response.Data)) + .Returns(new OkResponse()); + } + + private void ThenTheScopedRepoIsCalledCorrectly() + { + ScopedRepository + .Verify(x => x.Add("HttpResponseMessage", _response.Data), Times.Once()); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs index 71929404..b3c91c5b 100644 --- a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs +++ b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs @@ -1,10 +1,8 @@ using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Requester.QoS; using Ocelot.Responses; -using Ocelot.UnitTests.LoadBalancer; using Shouldly; using TestStack.BDDfy; using Xunit; diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 06eebb4b..9922df6a 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -121,7 +121,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(32, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(33, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index a8bf5475..ce98a972 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -1,10 +1,14 @@ namespace Ocelot.UnitTests.Responder { + using System.Collections.Generic; using System.Net.Http; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Moq; + using Ocelot.DownstreamRouteFinder.Finder; + using Ocelot.Errors; using Ocelot.Logging; + using Ocelot.Requester; using Ocelot.Responder; using Ocelot.Responder.Middleware; using Ocelot.Responses; @@ -35,6 +39,17 @@ .BDDfy(); } + + [Fact] + public void should_return_any_errors() + { + this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) + .And(x => x.GivenThereArePipelineErrors(new UnableToFindDownstreamRouteError())) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenThereAreNoErrors()) + .BDDfy(); + } + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) { services.AddSingleton(); @@ -68,5 +83,14 @@ { //todo a better assert? } + + private void GivenThereArePipelineErrors(Error error) + { + ScopedRepository + .Setup(x => x.Get("OcelotMiddlewareError")) + .Returns(new OkResponse(true)); + ScopedRepository.Setup(x => x.Get>("OcelotMiddlewareErrors")) + .Returns(new OkResponse>(new List() { error })); + } } } diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..a272d1b4 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery; +using Ocelot.Values; +using Xunit; +using TestStack.BDDfy; +using Shouldly; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ConsulServiceDiscoveryProviderTests : IDisposable + { + private IWebHost _fakeConsulBuilder; + private readonly List _serviceEntries; + private readonly ConsulServiceDiscoveryProvider _provider; + private readonly string _serviceName; + private readonly int _port; + private readonly string _consulHost; + private readonly string _fakeConsulServiceDiscoveryUrl; + private List _services; + private Mock _factory; + private readonly Mock _logger; + + public ConsulServiceDiscoveryProviderTests() + { + _serviceName = "test"; + _port = 8500; + _consulHost = "localhost"; + _fakeConsulServiceDiscoveryUrl = $"http://{_consulHost}:{_port}"; + _serviceEntries = new List(); + + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + + var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName); + _provider = new ConsulServiceDiscoveryProvider(config, _factory.Object); + } + + [Fact] + public void should_return_service_from_consul() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = 50881, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x =>GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(1)) + .BDDfy(); + } + + [Fact] + public void should_not_return_services_with_invalid_address() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "http://localhost", + Port = 50881, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "http://localhost", + Port = 50888, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(0)) + .And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress()) + .BDDfy(); + } + + [Fact] + public void should_not_return_services_with_invalid_port() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = -1, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = 0, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(0)) + .And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts()) + .BDDfy(); + } + + private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress() + { + _logger.Verify( + x => x.LogError( + "Unable to use service Address: http://localhost and Port: 50881 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + + _logger.Verify( + x => x.LogError( + "Unable to use service Address: http://localhost and Port: 50888 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + } + + private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts() + { + _logger.Verify( + x => x.LogError( + "Unable to use service Address: localhost and Port: -1 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + + _logger.Verify( + x => x.LogError( + "Unable to use service Address: localhost and Port: 0 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + } + + private void ThenTheCountIs(int count) + { + _services.Count.ShouldBe(count); + } + + private void WhenIGetTheServices() + { + _services = _provider.Get().GetAwaiter().GetResult(); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public void Dispose() + { + _fakeConsulBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index 55ecf502..625ffaa3 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Logging; using Ocelot.ServiceDiscovery; using Shouldly; using TestStack.BDDfy; @@ -15,10 +17,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery private IServiceDiscoveryProvider _result; private readonly ServiceDiscoveryProviderFactory _factory; private ReRoute _reRoute; + private Mock _loggerFactory; public ServiceProviderFactoryTests() { - _factory = new ServiceDiscoveryProviderFactory(); + _loggerFactory = new Mock(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object); } [Fact] @@ -104,4 +108,4 @@ namespace Ocelot.UnitTests.ServiceDiscovery _result.ShouldBeOfType(); } } -} \ No newline at end of file +}