diff --git a/Ocelot.sln b/Ocelot.sln index 82a17db98..fb7008d3a 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 40b4d7407..1287451b1 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 29a19af0f..3bd4da2f2 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 000000000..a6f2e4e99 --- /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 000000000..a30ea7413 --- /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 c2bc7041a..c98c21743 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 38106d459..7281f931a 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 52aa76948..6c66f3c06 100644 --- a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs @@ -7,7 +7,7 @@ public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator 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 ea3527672..7f24b5723 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -11,5 +11,7 @@ public FileHttpHandlerOptions() 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 0ec72d1dd..ef0edd9cf 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 @@ public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer) /// 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 000000000..859f4d98a --- /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 d7cb0f0d2..d2f92101a 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 000000000..580839c37 --- /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 a923071b9..10f9d082b 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 4befa3f96..bcfde4b08 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 c2574ff95..54461bbd1 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -62,7 +62,7 @@ public async Task Invoke(HttpContext context) 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 e4fba0559..b976e29c5 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -33,6 +33,7 @@ public enum OcelotErrorCode UnmappableRequestError, RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, - FileValidationFailedError + FileValidationFailedError, + UnableToFindDelegatingHandlerProviderError } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs index 2daf74ffc..8ce7bcd4a 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -9,4 +9,4 @@ public UnableToFindLoadBalancerError(string message) { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index 29347c88f..aa37196f9 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 @@ public async Task Invoke(HttpContext context) } 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 53d6e14c6..ee0b5b8ab 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 @@ public OcelotDiagnosticListener(IOcelotLoggerFactory factory) 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 @@ public virtual void OnMiddlewareException(Exception exception, string name) 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 d2a00af1e..eb4ce7732 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 9a7d0b76d..5b8ef968d 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 ab678582b..abde4a6bf 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 dd1ad4296..4160db313 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 79c170ea5..3bb670f69 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 000000000..0a7d69471 --- /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 000000000..71684c523 --- /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 000000000..1e6cb4c4b --- /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 257c42220..561b5205e 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 29bb5c3e4..9fa3b271d 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 @@ public class HttpClientHttpRequester : IHttpRequester { 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 @@ public async Task> GetResponse(Request.Request req } - 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 000000000..21e74e485 --- /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 000000000..78dd1fc1b --- /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 000000000..addaaeb73 --- /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 000000000..dcf007d4d --- /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 6de5f87a8..d6b435013 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 b21168187..81d9fa22d 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 412983de0..ae3e7c5f9 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 @@ public class HttpRequesterMiddleware : OcelotMiddleware public HttpRequesterMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester, - IRequestScopedDataRepository requestScopedDataRepository) + IRequestScopedDataRepository requestScopedDataRepository +) :base(requestScopedDataRepository) { _next = next; @@ -25,7 +25,7 @@ public HttpRequesterMiddleware(RequestDelegate next, } public async Task Invoke(HttpContext context) - { + { var response = await _requester.GetResponse(Request); if (response.IsError) @@ -41,4 +41,4 @@ public async Task Invoke(HttpContext context) 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 000000000..ff588fe8e --- /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 ce8eec1a0..45b0ac12c 100644 --- a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -16,10 +16,10 @@ public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler 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 000000000..fd8aeb9e4 --- /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 b6ed2717c..d0c126423 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 @@ public ResponderMiddleware(RequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository, - IErrorsToHttpStatusCodeMapper codeMapper) + IErrorsToHttpStatusCodeMapper codeMapper + ) :base(requestScopedDataRepository) { _next = next; @@ -33,14 +34,13 @@ public ResponderMiddleware(RequestDelegate next, } 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 @@ public async Task Invoke(HttpContext context) } } - 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 c21650f15..ef882a50a 100644 --- a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs @@ -4,6 +4,7 @@ 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 @@ public async Task> Get() { 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 @@ private Service BuildService(ServiceEntry serviceEntry) 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 eb73dae00..e8c97bd4a 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 @@ public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig 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 000000000..bb2a43df7 --- /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 bd41f007f..bcd017b4f 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -88,6 +88,38 @@ public void GivenOcelotIsRunning() _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 70a91e2c6..bb9f26d9a 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 329984a10..dd5cf9797 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 a8d01b24f..45f184bcf 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 22a73e2a6..b2ff9a005 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -471,7 +471,7 @@ public void should_call_httpHandler_creator() { 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 69f414e52..3821ef6bd 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -23,7 +23,7 @@ public HttpHandlerOptionsCreatorTests() 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 @@ public void should_create_options_with_specified_useCookie_and_allowAutoRedirect 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 @@ private void ThenTheFollowingOptionsReturned(HttpHandlerOptions options) _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 ef7835919..e58398564 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.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 @@ public OcelotBuilderTests() } 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 @@ public void should_return_ocelot_builder() .BDDfy(); } - + [Fact] public void should_set_up_cache_manager() { @@ -76,7 +90,7 @@ public void should_set_up_consul() .BDDfy(); } - [Fact] + [Fact] public void should_set_up_rafty() { this.Given(x => WhenISetUpOcelotServices()) @@ -96,6 +110,16 @@ public void should_use_logger_factory() .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 @@ private void ThenTheCorrectAdminPathIsRegitered() 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 @@ private void WhenISetUpRafty() } } + private void AddDelegate(DelegatingHandler handler) + { + _ocelotBuilder.AddDelegatingHandler(() => handler); + } + private void ThenAnOcelotBuilderIsReturned() { _ocelotBuilder.ShouldBeOfType(); @@ -193,6 +233,24 @@ private void WhenISetUpCacheManager() } } + 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 @@ private void WhenIAccessLoggerFactory() } } + 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 10cbc1205..b4d6acdc4 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 @@ public void should_call_scoped_data_repository_correctly() 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 @@ private void GivenTheRequestBuilderReturns(Ocelot.Request.Request request) It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(_request); } @@ -118,5 +142,11 @@ private void ThenTheScopedDataRepositoryIsCalledCorrectly() _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 f4f67ca02..dbfc8643b 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 000000000..f78917d47 --- /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 000000000..e64df1b96 --- /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 000000000..d93e291a5 --- /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 000000000..545e956e7 --- /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 000000000..6081590a7 --- /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 000000000..292716718 --- /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 7c6bd4874..5658984a0 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 719294046..b3c91c5bd 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 06eebb4be..9922df6a1 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -121,7 +121,7 @@ public void check_we_have_considered_all_errors_in_these_tests() // 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 a8bf54754..ce98a9724 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 @@ public void should_not_return_any_errors() .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 @@ private void ThenThereAreNoErrors() { //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 000000000..a272d1b49 --- /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 55ecf5022..625ffaa36 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 @@ public class ServiceProviderFactoryTests 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 @@ private void ThenTheServiceProviderIs() _result.ShouldBeOfType(); } } -} \ No newline at end of file +}