diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..a038c8775 Binary files /dev/null and b/.DS_Store differ diff --git a/Ocelot.sln b/Ocelot.sln index 2f8fd206e..6c0006b0a 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -9,19 +9,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .gitignore = .gitignore appveyor.yml = appveyor.yml + build-and-release-unstable.ps1 = build-and-release-unstable.ps1 build-and-run-tests.ps1 = build-and-run-tests.ps1 build.cake = build.cake build.ps1 = build.ps1 + build.readme.md = build.readme.md configuration-explanation.txt = configuration-explanation.txt + configuration.yaml = configuration.yaml + GitVersion.yml = GitVersion.yml global.json = global.json LICENSE.md = LICENSE.md Ocelot.nuspec = Ocelot.nuspec README.md = README.md release.ps1 = release.ps1 + ReleaseNotes.md = ReleaseNotes.md run-acceptance-tests.ps1 = run-acceptance-tests.ps1 - run-benchmarks.bat = run-benchmarks.bat run-benchmarks.ps1 = run-benchmarks.ps1 run-unit-tests.ps1 = run-unit-tests.ps1 + version.ps1 = version.ps1 EndProjectSection EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" diff --git a/README.md b/README.md index 249a99a31..738ffbe13 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ocelot -[![Build status](https://ci.appveyor.com/api/projects/status/roahbe4nl526ysya?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot) +[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) [![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -41,13 +41,13 @@ touch either via gitter or create an issue. ## How to install Ocelot is designed to work with ASP.NET core only and is currently -built to netcoreapp1.4 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. +built to netcoreapp1.1 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. Install Ocelot and it's dependecies using nuget. At the moment all we have is the pre version. Once we have something working in a half decent way we will drop a version. -`Install-Package Ocelot -Pre` +`Install-Package Ocelot` All versions can be found [here](https://www.nuget.org/packages/Ocelot/) @@ -162,6 +162,44 @@ This means that when Ocelot tries to match the incoming upstream url with an ups evaluation will be case sensitive. This setting defaults to false so only set it if you want the ReRoute to be case sensitive is my advice! + +## Service Discovery + +Ocelot allows you to specify a service discovery provider and will use this to find the host and port +for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the +GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes +you specify a ServiceName for at ReRoute level. + +In the future we can add a feature that allows ReRoute specfic configuration. + +At the moment the only supported service discovery provider is Consul. The following is required in the +GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default +will be used. + + "ServiceDiscoveryProvider": + { + "Provider":"Consul", + "Host":"localhost", + "Port":8500 + } + +In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the +ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin +and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put", + "ServiceName": "product" + "LoadBalancer": "LeastConnection" + } + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balancer +requests across any available services. + + ## Authentication Ocelot currently supports the use of bearer tokens with Identity Server (more providers to @@ -389,3 +427,4 @@ that isnt available is annoying. Let alone it be null. You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) + diff --git a/appveyor.yml b/appveyor.yml index b5cd7c0c0..6e8b16f66 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,6 @@ configuration: - Release platform: Any CPU build_script: -- ./build.ps1 +- build.ps1 cache: - '%USERPROFILE%\.nuget\packages' \ No newline at end of file diff --git a/build.cake b/build.cake index 1d798d743..1a1f005d8 100644 --- a/build.cake +++ b/build.cake @@ -42,8 +42,8 @@ var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; // internal build variables - don't change these. var releaseTag = ""; -var buildVersion = committedVersion; var committedVersion = "0.0.0-dev"; +var buildVersion = committedVersion; var target = Argument("target", "Default"); diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..04731adf1 --- /dev/null +++ b/build.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +########################################################################## +# This is the Cake bootstrapper script for Linux and OS X. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +# Define directories. +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TOOLS_DIR=$SCRIPT_DIR/tools +NUGET_EXE=$TOOLS_DIR/nuget.exe +CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +PACKAGES_CONFIG=$TOOLS_DIR/packages.config +PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum + +# Define md5sum or md5 depending on Linux/OSX +MD5_EXE= +if [[ "$(uname -s)" == "Darwin" ]]; then + MD5_EXE="md5 -r" +else + MD5_EXE="md5sum" +fi + +# Define default arguments. +SCRIPT="build.cake" +TARGET="Default" +CONFIGURATION="Release" +VERBOSITY="verbose" +DRYRUN= +SHOW_VERSION=false +SCRIPT_ARGUMENTS=() + +# Parse arguments. +for i in "$@"; do + case $1 in + -s|--script) SCRIPT="$2"; shift ;; + -t|--target) TARGET="$2"; shift ;; + -c|--configuration) CONFIGURATION="$2"; shift ;; + -v|--verbosity) VERBOSITY="$2"; shift ;; + -d|--dryrun) DRYRUN="-dryrun" ;; + --version) SHOW_VERSION=true ;; + --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; + *) SCRIPT_ARGUMENTS+=("$1") ;; + esac + shift +done + +# Make sure the tools folder exist. +if [ ! -d "$TOOLS_DIR" ]; then + mkdir "$TOOLS_DIR" +fi + +# Make sure that packages.config exist. +if [ ! -f "$TOOLS_DIR/packages.config" ]; then + echo "Downloading packages.config..." + curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages + if [ $? -ne 0 ]; then + echo "An error occured while downloading packages.config." + exit 1 + fi +fi + +# Download NuGet if it does not exist. +if [ ! -f "$NUGET_EXE" ]; then + echo "Downloading NuGet..." + curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe + if [ $? -ne 0 ]; then + echo "An error occured while downloading nuget.exe." + exit 1 + fi +fi + +# Restore tools from NuGet. +pushd "$TOOLS_DIR" >/dev/null +if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then + find . -type d ! -name . | xargs rm -rf +fi + +mono "$NUGET_EXE" install -ExcludeVersion +if [ $? -ne 0 ]; then + echo "Could not restore NuGet packages." + exit 1 +fi + +$MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" + +popd >/dev/null + +# Make sure that Cake has been installed. +if [ ! -f "$CAKE_EXE" ]; then + echo "Could not find Cake.exe at '$CAKE_EXE'." + exit 1 +fi + +# Start Cake +if $SHOW_VERSION; then + exec mono "$CAKE_EXE" -version +else + exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" +fi \ No newline at end of file diff --git a/configuration-explanation.txt b/configuration-explanation.txt index ad0204699..b6db84a2f 100644 --- a/configuration-explanation.txt +++ b/configuration-explanation.txt @@ -80,12 +80,24 @@ # the caching a lot. "FileCacheOptions": { "TtlSeconds": 15 }, # The value of this is used when matching the upstream template to an upstream url. - "ReRouteIsCaseSensitive": false + "ReRouteIsCaseSensitive": false, + # Tells Ocelot the name of the service it is looking when making requests to service discovery + # for hosts and ports + "ServiceName": "product" + # Tells Ocelot which load balancer to use when making downstream requests. + "LoadBalancer": "RoundRobin" }, # This section is meant to be for global configuration settings "GlobalConfiguration": { # If this is set it will override any route specific request id keys, behaves the same # otherwise "RequestIdKey": "OcRequestId", + # If set Ocelot will try and use service discovery to locate downstream hosts and ports + "ServiceDiscoveryProvider": + { + "Provider":"Consul", + "Host":"localhost", + "Port":8500 + } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 1e06a4407..caa09d3f9 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -6,6 +6,7 @@ namespace Ocelot.Configuration.Builder { public class ReRouteBuilder { + private string _loadBalancerKey; private string _downstreamPathTemplate; private string _upstreamTemplate; private string _upstreamTemplatePattern; @@ -32,12 +33,21 @@ public class ReRouteBuilder private string _downstreamScheme; private string _downstreamHost; private int _dsPort; + private string _loadBalancer; + private string _serviceProviderHost; + private int _serviceProviderPort; public ReRouteBuilder() { _additionalScopes = new List(); } + public ReRouteBuilder WithLoadBalancer(string loadBalancer) + { + _loadBalancer = loadBalancer; + return this; + } + public ReRouteBuilder WithDownstreamScheme(string downstreamScheme) { _downstreamScheme = downstreamScheme; @@ -192,15 +202,31 @@ public ReRouteBuilder WithDownstreamPort(int port) return this; } - public ReRoute Build() + public ReRouteBuilder WithLoadBalancerKey(string loadBalancerKey) { - Func downstreamHostFunc = () => new HostAndPort(_downstreamHost, _dsPort); + _loadBalancerKey = loadBalancerKey; + return this; + } + public ReRouteBuilder WithServiceProviderHost(string serviceProviderHost) + { + _serviceProviderHost = serviceProviderHost; + return this; + } + + public ReRouteBuilder WithServiceProviderPort(int serviceProviderPort) + { + _serviceProviderPort = serviceProviderPort; + return this; + } + + public ReRoute Build() + { return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, - _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName, - _useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, downstreamHostFunc, _downstreamScheme); + _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer, + _downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery, _serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort)); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 8884f0d9f..703239d0f 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; +using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Responses; using Ocelot.Utilities; using Ocelot.Values; @@ -25,22 +27,28 @@ public class FileOcelotConfigurationCreator : IOcelotConfigurationCreator private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly ILogger _logger; + private readonly ILoadBalancerFactory _loadBalanceFactory; + private readonly ILoadBalancerHouse _loadBalancerHouse; public FileOcelotConfigurationCreator( IOptions options, IConfigurationValidator configurationValidator, IClaimToThingConfigurationParser claimToThingConfigurationParser, - ILogger logger) + ILogger logger, + ILoadBalancerFactory loadBalancerFactory, + ILoadBalancerHouse loadBalancerHouse) { + _loadBalanceFactory = loadBalancerFactory; + _loadBalancerHouse = loadBalancerHouse; _options = options; _configurationValidator = configurationValidator; _claimToThingConfigurationParser = claimToThingConfigurationParser; _logger = logger; } - public Response Create() + public async Task> Create() { - var config = SetUpConfiguration(); + var config = await SetUpConfiguration(); return new OkResponse(config); } @@ -49,7 +57,7 @@ public Response Create() /// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see /// will need a refactor at some point as its crap /// - private IOcelotConfiguration SetUpConfiguration() + private async Task SetUpConfiguration() { var response = _configurationValidator.IsValid(_options.Value); @@ -69,63 +77,81 @@ private IOcelotConfiguration SetUpConfiguration() foreach (var reRoute in _options.Value.ReRoutes) { - var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); + var ocelotReRoute = await SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); reRoutes.Add(ocelotReRoute); } return new OcelotConfiguration(reRoutes); } - private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) + private async Task SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) { var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); - var upstreamTemplate = BuildUpstreamTemplate(reRoute); + var upstreamTemplate = BuildUpstreamTemplate(fileReRoute); - var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider); + var isAuthenticated = !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); - var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0; + var isAuthorised = fileReRoute.RouteClaimsRequirement?.Count > 0; - var isCached = reRoute.FileCacheOptions.TtlSeconds > 0; + var isCached = fileReRoute.FileCacheOptions.TtlSeconds > 0; var requestIdKey = globalRequestIdConfiguration ? globalConfiguration.RequestIdKey - : reRoute.RequestIdKey; + : fileReRoute.RequestIdKey; - var useServiceDiscovery = !string.IsNullOrEmpty(reRoute.ServiceName) - && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address) + var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName) && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain + var loadBalancerKey = $"{fileReRoute.UpstreamTemplate}{fileReRoute.UpstreamHttpMethod}"; - Func downstreamHostAndPortFunc = () => new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort); + ReRoute reRoute; - if (isAuthenticated) - { - var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, - reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName, - reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, - reRoute.AuthenticationOptions.ScopeSecret); + var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; - var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest); - var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest); - var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest); + var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName, + fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, useServiceDiscovery, + globalConfiguration?.ServiceDiscoveryProvider?.Provider, globalConfiguration?.ServiceDiscoveryProvider?.Host, + serviceProviderPort); - return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, - reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + if (isAuthenticated) + { + var authOptionsForRoute = new AuthenticationOptions(fileReRoute.AuthenticationOptions.Provider, + fileReRoute.AuthenticationOptions.ProviderRootUrl, fileReRoute.AuthenticationOptions.ScopeName, + fileReRoute.AuthenticationOptions.RequireHttps, fileReRoute.AuthenticationOptions.AdditionalScopes, + fileReRoute.AuthenticationOptions.ScopeSecret); + + var claimsToHeaders = GetAddThingsToRequest(fileReRoute.AddHeadersToRequest); + var claimsToClaims = GetAddThingsToRequest(fileReRoute.AddClaimsToRequest); + var claimsToQueries = GetAddThingsToRequest(fileReRoute.AddQueriesToRequest); + + reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), + fileReRoute.UpstreamTemplate, + fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, authOptionsForRoute, claimsToHeaders, claimsToClaims, - reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, - requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), - reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, - globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme); + fileReRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, + requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds) + , fileReRoute.DownstreamScheme, + fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, + serviceProviderConfiguration); + } + else + { + reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), + fileReRoute.UpstreamTemplate, + fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + null, new List(), new List(), + fileReRoute.RouteClaimsRequirement, isAuthorised, new List(), + requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds), + fileReRoute.DownstreamScheme, + fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, + serviceProviderConfiguration); } - return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, - reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - null, new List(), new List(), - reRoute.RouteClaimsRequirement, isAuthorised, new List(), - requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), - reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, - globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme); + var loadBalancer = await _loadBalanceFactory.Get(reRoute); + _loadBalancerHouse.Add(reRoute.LoadBalancerKey, loadBalancer); + return reRoute; } private string BuildUpstreamTemplate(FileReRoute reRoute) diff --git a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs index 6cc7c2e8d..7547d91f0 100644 --- a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs @@ -1,9 +1,10 @@ +using System.Threading.Tasks; using Ocelot.Responses; namespace Ocelot.Configuration.Creator { public interface IOcelotConfigurationCreator { - Response Create(); + Task> Create(); } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index a653224a5..0d0fc7bda 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -29,5 +29,6 @@ public FileReRoute() public string DownstreamScheme {get;set;} public string DownstreamHost {get;set;} public int DownstreamPort { get; set; } + public string LoadBalancer {get;set;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs index 47efc6dfd..2f26b6eac 100644 --- a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -3,6 +3,7 @@ namespace Ocelot.Configuration.File public class FileServiceDiscoveryProvider { public string Provider {get;set;} - public string Address {get;set;} + public string Host {get;set;} + public int Port { get; set; } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs index 30ded2e9e..3256e44a4 100644 --- a/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs +++ b/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs @@ -1,9 +1,10 @@ -using Ocelot.Responses; +using System.Threading.Tasks; +using Ocelot.Responses; namespace Ocelot.Configuration.Provider { public interface IOcelotConfigurationProvider { - Response Get(); + Task> Get(); } } diff --git a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs index 4b6c5fd2d..80fd56970 100644 --- a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs +++ b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs @@ -1,4 +1,5 @@ -using Ocelot.Configuration.Creator; +using System.Threading.Tasks; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.Repository; using Ocelot.Responses; @@ -19,7 +20,7 @@ public OcelotConfigurationProvider(IOcelotConfigurationRepository repo, _creator = creator; } - public Response Get() + public async Task> Get() { var repoConfig = _repo.Get(); @@ -30,7 +31,7 @@ public Response Get() if (repoConfig.Data == null) { - var creatorConfig = _creator.Create(); + var creatorConfig = await _creator.Create(); if (creatorConfig.IsError) { diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 960374cc2..278d07460 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -6,12 +6,23 @@ namespace Ocelot.Configuration { public class ReRoute { - public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, - bool isAuthenticated, AuthenticationOptions authenticationOptions, List configurationHeaderExtractorProperties, - List claimsToClaims, Dictionary routeClaimsRequirement, bool isAuthorised, List claimsToQueries, - string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery, - string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func downstreamHostAndPort, string downstreamScheme) + public ReRoute(DownstreamPathTemplate downstreamPathTemplate, + string upstreamTemplate, string upstreamHttpMethod, + string upstreamTemplatePattern, + bool isAuthenticated, AuthenticationOptions authenticationOptions, + List configurationHeaderExtractorProperties, + List claimsToClaims, + Dictionary routeClaimsRequirement, bool isAuthorised, + List claimsToQueries, + string requestIdKey, bool isCached, CacheOptions fileCacheOptions, + string downstreamScheme, string loadBalancer, string downstreamHost, + int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion) { + LoadBalancerKey = loadBalancerKey; + ServiceProviderConfiguraion = serviceProviderConfiguraion; + LoadBalancer = loadBalancer; + DownstreamHost = downstreamHost; + DownstreamPort = downstreamPort; DownstreamPathTemplate = downstreamPathTemplate; UpstreamTemplate = upstreamTemplate; UpstreamHttpMethod = upstreamHttpMethod; @@ -29,14 +40,10 @@ public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTem ?? new List(); ClaimsToHeaders = configurationHeaderExtractorProperties ?? new List(); - ServiceName = serviceName; - UseServiceDiscovery = useServiceDiscovery; - ServiceDiscoveryProvider = serviceDiscoveryProvider; - ServiceDiscoveryAddress = serviceDiscoveryAddress; - DownstreamHostAndPort = downstreamHostAndPort; DownstreamScheme = downstreamScheme; } + public string LoadBalancerKey {get;private set;} public DownstreamPathTemplate DownstreamPathTemplate { get; private set; } public string UpstreamTemplate { get; private set; } public string UpstreamTemplatePattern { get; private set; } @@ -51,11 +58,10 @@ public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTem public string RequestIdKey { get; private set; } public bool IsCached { get; private set; } public CacheOptions FileCacheOptions { get; private set; } - public string ServiceName { get; private set;} - public bool UseServiceDiscovery { get; private set;} - public string ServiceDiscoveryProvider { get; private set;} - public string ServiceDiscoveryAddress { get; private set;} - public Func DownstreamHostAndPort {get;private set;} public string DownstreamScheme {get;private set;} + public string LoadBalancer {get;private set;} + public string DownstreamHost { get; private set; } + public int DownstreamPort { get; private set; } + public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs b/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs new file mode 100644 index 000000000..d471a9e51 --- /dev/null +++ b/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs @@ -0,0 +1,25 @@ +namespace Ocelot.Configuration +{ + public class ServiceProviderConfiguraion + { + public ServiceProviderConfiguraion(string serviceName, string downstreamHost, + int downstreamPort, bool useServiceDiscovery, string serviceDiscoveryProvider, string serviceProviderHost, int serviceProviderPort) + { + ServiceName = serviceName; + DownstreamHost = downstreamHost; + DownstreamPort = downstreamPort; + UseServiceDiscovery = useServiceDiscovery; + ServiceDiscoveryProvider = serviceDiscoveryProvider; + ServiceProviderHost = serviceProviderHost; + ServiceProviderPort = serviceProviderPort; + } + + public string ServiceName { get; } + public string DownstreamHost { get; } + public int DownstreamPort { get; } + public bool UseServiceDiscovery { get; } + public string ServiceDiscoveryProvider { get; } + public string ServiceProviderHost { get; private set; } + public int ServiceProviderPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 9f40b0098..0a6bd42c3 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -23,11 +23,13 @@ using Ocelot.Headers; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; using Ocelot.QueryStrings; using Ocelot.Request.Builder; using Ocelot.Requester; using Ocelot.Responder; +using Ocelot.ServiceDiscovery; namespace Ocelot.DependencyInjection { @@ -59,6 +61,9 @@ public static IServiceCollection AddOcelot(this IServiceCollection services) { services.AddMvcCore().AddJsonFormatters(); services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 752da281b..eacd69124 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; @@ -21,9 +22,9 @@ public DownstreamRouteFinder(IOcelotConfigurationProvider configProvider, IUrlPa _urlPathPlaceholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; } - public Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod) + public async Task> FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod) { - var configuration = _configProvider.Get(); + var configuration = await _configProvider.Get(); var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase)); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs index e351ab2f0..7ae3ff791 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs @@ -1,9 +1,10 @@ -using Ocelot.Responses; +using System.Threading.Tasks; +using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.Finder { public interface IDownstreamRouteFinder { - Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod); + Task> FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod); } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index f445b46bf..e88bfde8a 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -34,7 +34,7 @@ public async Task Invoke(HttpContext context) _logger.LogDebug("upstream url path is {upstreamUrlPath}", upstreamUrlPath); - var downstreamRoute = _downstreamRouteFinder.FindDownstreamRoute(upstreamUrlPath, context.Request.Method); + var downstreamRoute = await _downstreamRouteFinder.FindDownstreamRoute(upstreamUrlPath, context.Request.Method); if (downstreamRoute.IsError) { diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index 8144b42b9..80365074d 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -6,6 +6,7 @@ using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator.Middleware { @@ -45,8 +46,8 @@ public async Task Invoke(HttpContext context) } var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme; - - var dsHostAndPort = DownstreamRoute.ReRoute.DownstreamHostAndPort(); + + var dsHostAndPort = HostAndPort; var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort); diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index fc3ab2002..6e6b511e0 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -41,13 +41,13 @@ public async Task Invoke(HttpContext context) var message = CreateMessage(context, e); _logger.LogError(message, e); - await SetInternalServerErrorOnResponse(context); + SetInternalServerErrorOnResponse(context); } _logger.LogDebug("ocelot pipeline finished"); } - private async Task SetInternalServerErrorOnResponse(HttpContext context) + private void SetInternalServerErrorOnResponse(HttpContext context) { context.Response.OnStarting(x => { diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index 5de770cdd..d24988b95 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -21,6 +21,10 @@ public enum OcelotErrorCode DownstreamPathTemplateContainsSchemeError, DownstreamPathNullOrEmptyError, DownstreamSchemeNullOrEmptyError, - DownstreamHostNullOrEmptyError + DownstreamHostNullOrEmptyError, + ServicesAreNullError, + ServicesAreEmptyError, + UnableToFindServiceDiscoveryProviderError, + UnableToFindLoadBalancerError } } diff --git a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs new file mode 100644 index 000000000..d74583811 --- /dev/null +++ b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs @@ -0,0 +1,24 @@ +using System; + +namespace Ocelot.Infrastructure.Extensions +{ + public static class StringExtensions + { + public static string TrimStart(this string source, string trim, StringComparison stringComparison = StringComparison.Ordinal) + { + if (source == null) + { + return null; + } + + string s = source; + while (s.StartsWith(trim, stringComparison)) + { + s = s.Substring(trim.Length); + } + + return s; + } + + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs new file mode 100644 index 000000000..73d25d487 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancer + { + Task> Lease(); + void Release(HostAndPort hostAndPort); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs new file mode 100644 index 000000000..19fdf3ebe --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ocelot.Configuration; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerFactory + { + Task Get(ReRoute reRoute); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs new file mode 100644 index 000000000..065ae2ac5 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerHouse + { + Response Get(string key); + Response Add(string key, ILoadBalancer loadBalancer); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs new file mode 100644 index 000000000..bd2e1b885 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs @@ -0,0 +1,15 @@ +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class Lease + { + public Lease(HostAndPort hostAndPort, int connections) + { + HostAndPort = hostAndPort; + Connections = connections; + } + public HostAndPort HostAndPort { get; private set; } + public int Connections { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs new file mode 100644 index 000000000..cd56ef914 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Errors; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LeastConnectionLoadBalancer : ILoadBalancer + { + private readonly Func>> _services; + private readonly List _leases; + private readonly string _serviceName; + private static readonly object _syncLock = new object(); + + public LeastConnectionLoadBalancer(Func>> services, string serviceName) + { + _services = services; + _serviceName = serviceName; + _leases = new List(); + } + + public async Task> Lease() + { + var services = await _services.Invoke(); + + if (services == null) + { + return new ErrorResponse(new List() { new ServicesAreNullError($"services were null for {_serviceName}") }); + } + + if (!services.Any()) + { + return new ErrorResponse(new List() { new ServicesAreEmptyError($"services were empty for {_serviceName}") }); + } + + lock(_syncLock) + { + //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? + UpdateServices(services); + + var leaseWithLeastConnections = GetLeaseWithLeastConnections(); + + _leases.Remove(leaseWithLeastConnections); + + leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); + + _leases.Add(leaseWithLeastConnections); + + return new OkResponse(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); + } + } + + public void Release(HostAndPort hostAndPort) + { + lock(_syncLock) + { + var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost + && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); + + if (matchingLease != null) + { + var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); + + _leases.Remove(matchingLease); + + _leases.Add(replacementLease); + } + } + } + + private Lease AddConnection(Lease lease) + { + return new Lease(lease.HostAndPort, lease.Connections + 1); + } + + private Lease GetLeaseWithLeastConnections() + { + //now get the service with the least connections? + Lease leaseWithLeastConnections = null; + + for (var i = 0; i < _leases.Count; i++) + { + if (i == 0) + { + leaseWithLeastConnections = _leases[i]; + } + else + { + if (_leases[i].Connections < leaseWithLeastConnections.Connections) + { + leaseWithLeastConnections = _leases[i]; + } + } + } + + return leaseWithLeastConnections; + } + + private Response UpdateServices(List services) + { + if (_leases.Count > 0) + { + var leasesToRemove = new List(); + + foreach (var lease in _leases) + { + var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost + && s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort); + + if (match == null) + { + leasesToRemove.Add(lease); + } + } + + foreach (var lease in leasesToRemove) + { + _leases.Remove(lease); + } + + foreach (var service in services) + { + var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString()); + + if (exists == null) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + } + else + { + foreach (var service in services) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs new file mode 100644 index 000000000..08e45d2b3 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerFactory : ILoadBalancerFactory + { + private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; + public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory) + { + _serviceProviderFactory = serviceProviderFactory; + } + + public async Task Get(ReRoute reRoute) + { + var serviceConfig = new ServiceProviderConfiguraion( + reRoute.ServiceProviderConfiguraion.ServiceName, + reRoute.ServiceProviderConfiguraion.DownstreamHost, + reRoute.ServiceProviderConfiguraion.DownstreamPort, + reRoute.ServiceProviderConfiguraion.UseServiceDiscovery, + reRoute.ServiceProviderConfiguraion.ServiceDiscoveryProvider, + reRoute.ServiceProviderConfiguraion.ServiceProviderHost, + reRoute.ServiceProviderConfiguraion.ServiceProviderPort); + + var serviceProvider = _serviceProviderFactory.Get(serviceConfig); + + switch (reRoute.LoadBalancer) + { + case "RoundRobin": + return new RoundRobinLoadBalancer(await serviceProvider.Get()); + case "LeastConnection": + return new LeastConnectionLoadBalancer(async () => await serviceProvider.Get(), reRoute.ServiceProviderConfiguraion.ServiceName); + default: + return new NoLoadBalancer(await serviceProvider.Get()); + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs new file mode 100644 index 000000000..cc6ea73b0 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerHouse : ILoadBalancerHouse + { + private readonly Dictionary _loadBalancers; + + public LoadBalancerHouse() + { + _loadBalancers = new Dictionary(); + } + + public Response Get(string key) + { + ILoadBalancer loadBalancer; + + if(_loadBalancers.TryGetValue(key, out loadBalancer)) + { + return new OkResponse(_loadBalancers[key]); + } + + return new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for {key}") + }); + } + + public Response Add(string key, ILoadBalancer loadBalancer) + { + if (!_loadBalancers.ContainsKey(key)) + { + _loadBalancers.Add(key, loadBalancer); + } + + _loadBalancers.Remove(key); + _loadBalancers.Add(key, loadBalancer); + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs new file mode 100644 index 000000000..bf66950b8 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class NoLoadBalancer : ILoadBalancer + { + private readonly List _services; + + public NoLoadBalancer(List services) + { + _services = services; + } + + public async Task> Lease() + { + var service = await Task.FromResult(_services.FirstOrDefault()); + return new OkResponse(service.HostAndPort); + } + + public void Release(HostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs new file mode 100644 index 000000000..37efe22f7 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class RoundRobinLoadBalancer : ILoadBalancer + { + private readonly List _services; + private int _last; + + public RoundRobinLoadBalancer(List services) + { + _services = services; + } + + public async Task> Lease() + { + if (_last >= _services.Count) + { + _last = 0; + } + + var next = await Task.FromResult(_services[_last]); + _last++; + return new OkResponse(next.HostAndPort); + } + + public void Release(HostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs new file mode 100644 index 000000000..2fc4d953a --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class ServicesAreEmptyError : Error + { + public ServicesAreEmptyError(string message) + : base(message, OcelotErrorCode.ServicesAreEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs new file mode 100644 index 000000000..8e1bb7009 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class ServicesAreNullError : Error + { + public ServicesAreNullError(string message) + : base(message, OcelotErrorCode.ServicesAreNullError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs new file mode 100644 index 000000000..3dd3ede4a --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class UnableToFindLoadBalancerError : Errors.Error + { + public UnableToFindLoadBalancerError(string message) + : base(message, OcelotErrorCode.UnableToFindLoadBalancerError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs new file mode 100644 index 000000000..ce37f8288 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.QueryStrings.Middleware; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer.Middleware +{ + public class LoadBalancingMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly ILoadBalancerHouse _loadBalancerHouse; + + public LoadBalancingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + ILoadBalancerHouse loadBalancerHouse) + : base(requestScopedDataRepository) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + _loadBalancerHouse = loadBalancerHouse; + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling load balancing middleware"); + + var loadBalancer = _loadBalancerHouse.Get(DownstreamRoute.ReRoute.LoadBalancerKey); + if(loadBalancer.IsError) + { + SetPipelineError(loadBalancer.Errors); + return; + } + + var hostAndPort = await loadBalancer.Data.Lease(); + if(hostAndPort.IsError) + { + SetPipelineError(hostAndPort.Errors); + return; + } + + SetHostAndPortForThisRequest(hostAndPort.Data); + + _logger.LogDebug("calling next middleware"); + + try + { + await _next.Invoke(context); + + loadBalancer.Data.Release(hostAndPort.Data); + } + catch (Exception) + { + loadBalancer.Data.Release(hostAndPort.Data); + + _logger.LogDebug("error calling next middleware, exception will be thrown to global handler"); + throw; + } + + _logger.LogDebug("succesfully called next middleware"); + } + } +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs new file mode 100644 index 000000000..0d0224b8d --- /dev/null +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.LoadBalancer.Middleware +{ + public static class LoadBalancingMiddlewareExtensions + { + public static IApplicationBuilder UseLoadBalancingMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Middleware/OcelotMiddleware.cs b/src/Ocelot/Middleware/OcelotMiddleware.cs index 0bb510406..b89371089 100644 --- a/src/Ocelot/Middleware/OcelotMiddleware.cs +++ b/src/Ocelot/Middleware/OcelotMiddleware.cs @@ -3,6 +3,7 @@ using Ocelot.DownstreamRouteFinder; using Ocelot.Errors; using Ocelot.Infrastructure.RequestData; +using Ocelot.Values; namespace Ocelot.Middleware { @@ -69,6 +70,20 @@ public HttpResponseMessage HttpResponseMessage } } + public HostAndPort HostAndPort + { + get + { + var hostAndPort = _requestScopedDataRepository.Get("HostAndPort"); + return hostAndPort.Data; + } + } + + public void SetHostAndPortForThisRequest(HostAndPort hostAndPort) + { + _requestScopedDataRepository.Add("HostAndPort", hostAndPort); + } + public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute) { _requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute); diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index dfa3b3f42..352aa5012 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -18,6 +18,8 @@ namespace Ocelot.Middleware using System.Threading.Tasks; using Authorisation.Middleware; using Microsoft.AspNetCore.Http; + using Ocelot.Configuration.Provider; + using Ocelot.LoadBalancer.Middleware; public static class OcelotMiddlewareExtensions { @@ -28,6 +30,7 @@ public static class OcelotMiddlewareExtensions /// public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) { + CreateConfiguration(builder); builder.UseOcelot(new OcelotMiddlewareConfiguration()); return builder; } @@ -40,6 +43,8 @@ public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) /// public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) { + CreateConfiguration(builder); + // This is registered to catch any global exceptions that are not handled builder.UseExceptionHandlerMiddleware(); @@ -98,6 +103,9 @@ public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, Oc // Now we can run any query string transformation logic builder.UseQueryStringBuilderMiddleware(); + // Get the load balancer for this request + builder.UseLoadBalancingMiddleware(); + // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used builder.UseDownstreamUrlCreatorMiddleware(); @@ -114,6 +122,18 @@ public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, Oc return builder; } + private static void CreateConfiguration(IApplicationBuilder builder) + { + var configProvider = (IOcelotConfigurationProvider)builder.ApplicationServices.GetService(typeof(IOcelotConfigurationProvider)); + + var config = configProvider.Get(); + + if(config == null) + { + throw new Exception("Unable to start Ocelot: configuration was null"); + } + } + private static void UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) { if (middleware != null) diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 8b61e13bc..40b60c309 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -24,7 +24,7 @@ public HttpContextResponder(IRemoveOutputHeaders removeOutputHeaders) _removeOutputHeaders = removeOutputHeaders; } - public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) + public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) { _removeOutputHeaders.Remove(response.Headers); @@ -56,7 +56,6 @@ public async Task SetResponseOnHttpContext(HttpContext context, HttpRe { await stream.CopyToAsync(context.Response.Body); } - return new OkResponse(); } private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair> httpResponseHeader) @@ -67,14 +66,13 @@ private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair SetErrorResponseOnContext(HttpContext context, int statusCode) + public void SetErrorResponseOnContext(HttpContext context, int statusCode) { context.Response.OnStarting(x => { context.Response.StatusCode = statusCode; return Task.CompletedTask; }, context); - return new OkResponse(); } } } \ No newline at end of file diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs index 5292f4df9..f885c6731 100644 --- a/src/Ocelot/Responder/IHttpResponder.cs +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -1,13 +1,12 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Ocelot.Responses; namespace Ocelot.Responder { public interface IHttpResponder { - Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); - Task SetErrorResponseOnContext(HttpContext context, int statusCode); + Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); + void SetErrorResponseOnContext(HttpContext context, int statusCode); } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 06da92dc0..6bce4ac69 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -46,34 +46,27 @@ public async Task Invoke(HttpContext context) _logger.LogDebug("received errors setting error response"); - await SetErrorResponse(context, errors); + SetErrorResponse(context, errors); } else { _logger.LogDebug("no pipeline error, setting response"); - var setResponse = await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); - - if (setResponse.IsError) - { - _logger.LogDebug("error setting response, returning error to client"); - - await SetErrorResponse(context, setResponse.Errors); - } + await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); } } - private async Task SetErrorResponse(HttpContext context, List errors) + private void SetErrorResponse(HttpContext context, List errors) { var statusCode = _codeMapper.Map(errors); if (!statusCode.IsError) { - await _responder.SetErrorResponseOnContext(context, statusCode.Data); + _responder.SetErrorResponseOnContext(context, statusCode.Data); } else { - await _responder.SetErrorResponseOnContext(context, 500); + _responder.SetErrorResponseOnContext(context, 500); } } } diff --git a/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs b/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs new file mode 100644 index 000000000..98e7b0f69 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ConfigurationServiceProvider : IServiceDiscoveryProvider + { + private readonly List _services; + + public ConfigurationServiceProvider(List services) + { + _services = services; + } + + public async Task> Get() + { + return await Task.FromResult(_services); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs b/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs new file mode 100644 index 000000000..8d496a859 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs @@ -0,0 +1,16 @@ +namespace Ocelot.ServiceDiscovery +{ + public class ConsulRegistryConfiguration + { + public ConsulRegistryConfiguration(string hostName, int port, string serviceName) + { + HostName = hostName; + Port = port; + ServiceName = serviceName; + } + + public string ServiceName { get; private set; } + public string HostName { get; private set; } + public int Port { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs new file mode 100644 index 000000000..c74c90f08 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Consul; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly ConsulRegistryConfiguration _configuration; + private readonly ConsulClient _consul; + private const string VersionPrefix = "version-"; + + public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration) + { + var consulHost = string.IsNullOrEmpty(consulRegistryConfiguration?.HostName) ? "localhost" : consulRegistryConfiguration.HostName; + var consulPort = consulRegistryConfiguration?.Port ?? 8500; + _configuration = new ConsulRegistryConfiguration(consulHost, consulPort, consulRegistryConfiguration?.ServiceName); + + _consul = new ConsulClient(config => + { + config.Address = new Uri($"http://{_configuration.HostName}:{_configuration.Port}"); + }); + } + + public async Task> Get() + { + var queryResult = await _consul.Health.Service(_configuration.ServiceName, string.Empty, true); + + var services = queryResult.Response.Select(BuildService); + + return services.ToList(); + } + + private Service BuildService(ServiceEntry serviceEntry) + { + return new Service( + serviceEntry.Service.Service, + new HostAndPort(serviceEntry.Service.Address, serviceEntry.Service.Port), + serviceEntry.Service.ID, + GetVersionFromStrings(serviceEntry.Service.Tags), + serviceEntry.Service.Tags ?? Enumerable.Empty()); + } + + private string GetVersionFromStrings(IEnumerable strings) + { + return strings + ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs new file mode 100644 index 000000000..2c643d4b1 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public interface IServiceDiscoveryProvider + { + Task> Get(); + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs new file mode 100644 index 000000000..6c6c3d4ca --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs @@ -0,0 +1,10 @@ +using System; +using Ocelot.Configuration; + +namespace Ocelot.ServiceDiscovery +{ + public interface IServiceDiscoveryProviderFactory + { + IServiceDiscoveryProvider Get(ServiceProviderConfiguraion serviceConfig); + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs new file mode 100644 index 000000000..006221908 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Ocelot.Configuration; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory + { + public IServiceDiscoveryProvider Get(ServiceProviderConfiguraion serviceConfig) + { + if (serviceConfig.UseServiceDiscovery) + { + return GetServiceDiscoveryProvider(serviceConfig.ServiceName, serviceConfig.ServiceDiscoveryProvider, serviceConfig.ServiceProviderHost, serviceConfig.ServiceProviderPort); + } + + var services = new List() + { + new Service(serviceConfig.ServiceName, + new HostAndPort(serviceConfig.DownstreamHost, serviceConfig.DownstreamPort), + string.Empty, + string.Empty, + new string[0]) + }; + + return new ConfigurationServiceProvider(services); + } + + private IServiceDiscoveryProvider GetServiceDiscoveryProvider(string serviceName, string serviceProviderName, string providerHostName, int providerPort) + { + var consulRegistryConfiguration = new ConsulRegistryConfiguration(providerHostName, providerPort, serviceName); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs new file mode 100644 index 000000000..163e63ef9 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.ServiceDiscovery +{ + public class UnableToFindServiceDiscoveryProviderError : Error + { + public UnableToFindServiceDiscoveryProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Values/HostAndPort.cs b/src/Ocelot/Values/HostAndPort.cs index cd336deca..c12195279 100644 --- a/src/Ocelot/Values/HostAndPort.cs +++ b/src/Ocelot/Values/HostAndPort.cs @@ -4,7 +4,7 @@ public class HostAndPort { public HostAndPort(string downstreamHost, int downstreamPort) { - DownstreamHost = downstreamHost; + DownstreamHost = downstreamHost?.Trim('/'); DownstreamPort = downstreamPort; } diff --git a/src/Ocelot/Values/Service.cs b/src/Ocelot/Values/Service.cs new file mode 100644 index 000000000..0ba12b79b --- /dev/null +++ b/src/Ocelot/Values/Service.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Ocelot.Values +{ + public class Service + { + public Service(string name, + HostAndPort hostAndPort, + string id, + string version, + IEnumerable tags) + { + Name = name; + HostAndPort = hostAndPort; + Id = id; + Version = version; + Tags = tags; + } + public string Id { get; private set; } + + public string Name { get; private set; } + + public string Version { get; private set; } + + public IEnumerable Tags { get; private set; } + + public HostAndPort HostAndPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/project.json b/src/Ocelot/project.json index 8d259469c..6ce4ffbba 100644 --- a/src/Ocelot/project.json +++ b/src/Ocelot/project.json @@ -1,41 +1,42 @@ { "version": "0.0.0-dev", - "dependencies": { - "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", - "Microsoft.Extensions.Configuration.Json": "1.1.0", - "Microsoft.Extensions.Logging": "1.1.0", - "Microsoft.Extensions.Logging.Console": "1.1.0", - "Microsoft.Extensions.Logging.Debug": "1.1.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", - "Microsoft.AspNetCore.Http": "1.1.0", - "System.Text.RegularExpressions": "4.3.0", - "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", - "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", - "Microsoft.AspNetCore.Authentication.Google": "1.1.0", - "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", - "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", - "Microsoft.AspNetCore.Authentication": "1.1.0", - "IdentityServer4.AccessTokenValidation": "1.0.2", - "Microsoft.AspNetCore.Mvc": "1.1.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", - "Microsoft.NETCore.App": "1.1.0", - "CacheManager.Core": "0.9.2", - "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", - "CacheManager.Microsoft.Extensions.Logging": "0.9.2" - }, + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "System.Text.RegularExpressions": "4.3.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", + "Microsoft.AspNetCore.Authentication.Google": "1.1.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", + "Microsoft.AspNetCore.Authentication": "1.1.0", + "IdentityServer4.AccessTokenValidation": "1.0.2", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "CacheManager.Core": "0.9.2", + "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", + "CacheManager.Microsoft.Extensions.Logging": "0.9.2", + "Consul": "0.7.2.1" + }, "runtimes": { "win10-x64": {}, "osx.10.11-x64":{}, "win7-x64": {} }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] } diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 000000000..e73160dc2 Binary files /dev/null and b/test/.DS_Store differ diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs new file mode 100644 index 000000000..ef7e0dd7f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Consul; +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 ServiceDiscoveryTests : IDisposable + { + private IWebHost _builderOne; + private IWebHost _builderTwo; + private IWebHost _fakeConsulBuilder; + private readonly Steps _steps; + private readonly List _serviceEntries; + private int _counterOne; + private int _counterTwo; + private static readonly object _syncLock = new object(); + + public ServiceDiscoveryTests() + { + _steps = new Steps(); + _serviceEntries = new List(); + } + + [Fact] + public void should_use_service_discovery_and_load_balance_request() + { + var serviceName = "product"; + var downstreamServiceOneUrl = "http://localhost:50879"; + var downstreamServiceTwoUrl = "http://localhost:50880"; + var fakeConsulServiceDiscoveryUrl = "http://localhost:8500"; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50879, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50880, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + ServiceName = serviceName, + LoadBalancer = "LeastConnection", + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Provider = "Consul", + Host = "localhost", + Port = 8500 + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes()) + .BDDfy(); + } + + private void ThenBothServicesCalledRealisticAmountOfTimes() + { + _counterOne.ShouldBe(25); + _counterTwo.ShouldBe(25); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach(var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _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/product") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _builderTwo = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + + }); + }) + .Build(); + + _builderTwo.Start(); + } + + public void Dispose() + { + _builderOne?.Dispose(); + _builderTwo?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index c2bd7ee7a..9b5faa043 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -5,6 +5,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; using CacheManager.Core; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -29,6 +31,12 @@ public class Steps : IDisposable private BearerToken _token; public HttpClient OcelotClient => _ocelotClient; public string RequestIdKey = "OcRequestId"; + private Random _random; + + public Steps() + { + _random = new Random(); + } public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { @@ -153,6 +161,28 @@ public void WhenIGetUrlOnTheApiGateway(string url) _response = _ocelotClient.GetAsync(url).Result; } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40,60)); + } + + Task.WaitAll(tasks); + } + + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) { _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs index 0aa730be0..ce802efb6 100644 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs @@ -4,14 +4,12 @@ public static class TestConfiguration { - public static double Version => 1.4; + public static double Version => 1.1; public static string ConfigurationPath => GetConfigurationPath(); public static string GetConfigurationPath() { var osArchitecture = RuntimeInformation.OSArchitecture.ToString(); - - var oSDescription = string.Empty; if(RuntimeInformation.OSDescription.ToLower().Contains("darwin")) { diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index cdcb1624e..78ab541b5 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -1 +1 @@ -{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Address":null}}} \ No newline at end of file +{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0}}} diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index 17f35a3c7..f1aa378b3 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -32,7 +32,8 @@ "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.NETCore.App": "1.1.0", "Shouldly": "2.8.2", - "TestStack.BDDfy": "4.3.2" + "TestStack.BDDfy": "4.3.2", + "Consul": "0.7.2.1" }, "runtimes": { "win10-x64": {}, @@ -40,7 +41,7 @@ "win7-x64": {} }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] } diff --git a/test/Ocelot.Benchmarks/project.json b/test/Ocelot.Benchmarks/project.json index 5f7a49871..061a2223c 100644 --- a/test/Ocelot.Benchmarks/project.json +++ b/test/Ocelot.Benchmarks/project.json @@ -6,7 +6,7 @@ "dependencies": { "Ocelot": "0.0.0-dev", - "BenchmarkDotNet": "0.10.1" + "BenchmarkDotNet": "0.10.2" }, "runtimes": { "win10-x64": {}, @@ -14,7 +14,7 @@ "win7-x64": {} }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] } diff --git a/test/Ocelot.ManualTest/project.json b/test/Ocelot.ManualTest/project.json index 3ae09ccbd..cf67f9bd1 100644 --- a/test/Ocelot.ManualTest/project.json +++ b/test/Ocelot.ManualTest/project.json @@ -24,7 +24,7 @@ "win7-x64": {} }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] } diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index fc80a4787..e1da7de07 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -8,6 +8,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; +using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; @@ -24,6 +25,9 @@ public class FileConfigurationCreatorTests private readonly Mock _configParser; private readonly Mock> _logger; private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator; + private readonly Mock _loadBalancerFactory; + private readonly Mock _loadBalancerHouse; + private readonly Mock _loadBalancer; public FileConfigurationCreatorTests() { @@ -31,8 +35,37 @@ public FileConfigurationCreatorTests() _configParser = new Mock(); _validator = new Mock(); _fileConfig = new Mock>(); + _loadBalancerFactory = new Mock(); + _loadBalancerHouse = new Mock(); + _loadBalancer = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( - _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object); + _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object, + _loadBalancerFactory.Object, _loadBalancerHouse.Object); + } + + [Fact] + public void should_create_load_balancer() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.TheLoadBalancerFactoryIsCalledCorrectly()) + .And(x => x.ThenTheLoadBalancerHouseIsCalledCorrectly()) + + .BDDfy(); } [Fact] @@ -66,6 +99,7 @@ public void should_use_downstream_host() .BDDfy(); } + [Fact] public void should_use_downstream_scheme() { this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -117,7 +151,7 @@ public void should_use_service_discovery_for_downstream_service_host() ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Provider = "consul", - Address = "127.0.0.1" + Host = "127.0.0.1" } } })) @@ -376,6 +410,7 @@ public void should_create_with_headers_to_extract() })) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigHeaderExtractorReturns(new ClaimToThing("CustomerId", "CustomerId", "", 0))) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected)) @@ -430,6 +465,7 @@ public void should_create_with_authentication_properties() } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected)) @@ -543,7 +579,7 @@ private void GivenTheConfigIs(FileConfiguration fileConfiguration) private void WhenICreateTheConfig() { - _config = _ocelotConfigurationCreator.Create(); + _config = _ocelotConfigurationCreator.Create().Result; } private void ThenTheReRoutesAre(List expectedReRoutes) @@ -576,5 +612,24 @@ private void ThenTheAuthenticationOptionsAre(List expectedReRoutes) } } + + private void GivenTheLoadBalancerFactoryReturns() + { + _loadBalancerFactory + .Setup(x => x.Get(It.IsAny())) + .ReturnsAsync(_loadBalancer.Object); + } + + private void TheLoadBalancerFactoryIsCalledCorrectly() + { + _loadBalancerFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void ThenTheLoadBalancerHouseIsCalledCorrectly() + { + _loadBalancerHouse + .Verify(x => x.Add(It.IsAny(), _loadBalancer.Object), Times.Once); + } } } diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs index 56fb64879..98e012934 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs @@ -81,7 +81,7 @@ private void GivenTheCreatorReturns(Response config) { _creator .Setup(x => x.Create()) - .Returns(config); + .ReturnsAsync(config); } private void GivenTheRepoReturns(Response config) @@ -93,7 +93,7 @@ private void GivenTheRepoReturns(Response config) private void WhenIGetTheConfig() { - _result = _ocelotConfigurationProvider.Get(); + _result = _ocelotConfigurationProvider.Get().Result; } private void TheFollowingIsReturned(Response expected) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 0d5a6d48a..a80a31680 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -84,7 +84,7 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRout _downstreamRoute = new OkResponse(downstreamRoute); _downstreamRouteFinder .Setup(x => x.FindDownstreamRoute(It.IsAny(), It.IsAny())) - .Returns(_downstreamRoute); + .ReturnsAsync(_downstreamRoute); } public void Dispose() diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index c0afca426..dc9978b30 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -159,7 +159,7 @@ private void GivenTheConfigurationIs(List reRoutesConfig) _reRoutesConfig = reRoutesConfig; _mockConfig .Setup(x => x.Get()) - .Returns(new OkResponse(new OcelotConfiguration(_reRoutesConfig))); + .ReturnsAsync(new OkResponse(new OcelotConfiguration(_reRoutesConfig))); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) @@ -169,7 +169,7 @@ private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) private void WhenICallTheFinder() { - _result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath, _upstreamHttpMethod); + _result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath, _upstreamHttpMethod).Result; } private void ThenTheFollowingIsReturned(DownstreamRoute expected) diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 5581a32ee..a01677b26 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -36,6 +36,7 @@ public class DownstreamUrlCreatorMiddlewareTests : IDisposable private HttpResponseMessage _result; private OkResponse _downstreamPath; private OkResponse _downstreamUrl; + private HostAndPort _hostAndPort; public DownstreamUrlCreatorMiddlewareTests() { @@ -69,14 +70,25 @@ public DownstreamUrlCreatorMiddlewareTests() [Fact] public void should_call_dependencies_correctly() { + var hostAndPort = new HostAndPort("127.0.0.1", 80); + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("any old string").Build()))) + .And(x => x.GivenTheHostAndPortIs(hostAndPort)) .And(x => x.TheUrlReplacerReturns("/api/products/1")) - .And(x => x.TheUrlBuilderReturns("http://www.bbc.co.uk/api/products/1")) + .And(x => x.TheUrlBuilderReturns("http://127.0.0.1:80/api/products/1")) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); } + private void GivenTheHostAndPortIs(HostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + _scopedRepository + .Setup(x => x.Get("HostAndPort")) + .Returns(new OkResponse(_hostAndPort)); + } + private void TheUrlBuilderReturns(string dsUrl) { _downstreamUrl = new OkResponse(new DownstreamUrl(dsUrl)); diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs new file mode 100644 index 000000000..07002ce36 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LeastConnectionTests + { + private HostAndPort _hostAndPort; + private Response _result; + private LeastConnectionLoadBalancer _leastConnection; + private List _services; + private Random _random; + + public LeastConnectionTests() + { + _random = new Random(); + } + + [Fact] + public void should_be_able_to_lease_and_release_concurrently() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var tasks = new Task[100]; + + for(var i = 0; i < tasks.Length; i++) + { + tasks[i] = LeaseDelayAndRelease(); + } + + Task.WaitAll(tasks); + } + + private async Task LeaseDelayAndRelease() + { + var hostAndPort = await _leastConnection.Lease(); + await Task.Delay(_random.Next(1, 100)); + _leastConnection.Release(hostAndPort.Data); + } + + [Fact] + public void should_get_next_url() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + + var availableServices = new List + { + new Service(serviceName, hostAndPort, string.Empty, string.Empty, new string[0]) + }; + + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(availableServices, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheNextHostAndPortIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_serve_from_service_with_least_connections() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.3", 80), string.Empty, string.Empty, new string[0]) + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_build_connections_per_service() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_release_connection() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + //release this so 2 should have 1 connection and we should get 2 back as our next host and port + _leastConnection.Release(availableServices[1].HostAndPort); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_return_error_if_services_are_null() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(null, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreNullErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_services_are_empty() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(new List(), serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreEmptyErrorIsReturned()) + .BDDfy(); + } + + private void ThenServiceAreNullErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void ThenServiceAreEmptyErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenTheLoadBalancerStarts(List services, string serviceName) + { + _services = services; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + } + + private void WhenTheLoadBalancerStarts(List services, string serviceName) + { + GivenTheLoadBalancerStarts(services, serviceName); + } + + private void GivenAHostAndPort(HostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + } + + private void WhenIGetTheNextHostAndPort() + { + _result = _leastConnection.Lease().Result; + } + + private void ThenTheNextHostAndPortIsReturned() + { + _result.Data.DownstreamHost.ShouldBe(_hostAndPort.DownstreamHost); + _result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs new file mode 100644 index 000000000..d030eb999 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -0,0 +1,110 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerFactoryTests + { + private ReRoute _reRoute; + private LoadBalancerFactory _factory; + private ILoadBalancer _result; + private Mock _serviceProviderFactory; + private Mock _serviceProvider; + + public LoadBalancerFactoryTests() + { + _serviceProviderFactory = new Mock(); + _serviceProvider = new Mock(); + _factory = new LoadBalancerFactory(_serviceProviderFactory.Object); + } + + private void GivenTheServiceProviderFactoryReturns() + { + _serviceProviderFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(_serviceProvider.Object); + } + + [Fact] + public void should_return_no_load_balancer() + { + var reRoute = new ReRouteBuilder() + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_robin_load_balancer() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("RoundRobin") + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_least_connection_balancer() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("LeastConnection") + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_call_service_provider() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("RoundRobin") + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheServiceProviderIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheServiceProviderIsCalledCorrectly() + { + _serviceProviderFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void GivenAReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGetTheLoadBalancer() + { + _result = _factory.Get(_reRoute).Result; + } + + private void ThenTheLoadBalancerIsReturned() + { + _result.ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs new file mode 100644 index 000000000..ac24b4906 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Threading.Tasks; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerHouseTests + { + private ILoadBalancer _loadBalancer; + private readonly LoadBalancerHouse _loadBalancerHouse; + private Response _addResult; + private Response _getResult; + private string _key; + + public LoadBalancerHouseTests() + { + _loadBalancerHouse = new LoadBalancerHouse(); + } + + [Fact] + public void should_store_load_balancer() + { + var key = "test"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .When(x => x.WhenIAddTheLoadBalancer()) + .Then(x => x.ThenItIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_get_load_balancer() + { + var key = "test"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(key)) + .Then(x => x.ThenItIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_store_load_balancers_by_key() + { + var key = "test"; + var keyTwo = "testTwo"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .And(x => x.GivenThereIsALoadBalancer(keyTwo, new FakeRoundRobinLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(key)) + .Then(x => x.ThenTheLoadBalancerIs()) + .When(x => x.WhenWeGetTheLoadBalancer(keyTwo)) + .Then(x => x.ThenTheLoadBalancerIs()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_load_balancer_with_key() + { + this.When(x => x.WhenWeGetTheLoadBalancer("test")) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _getResult.IsError.ShouldBeTrue(); + _getResult.Errors[0].ShouldBeOfType(); + } + + private void ThenTheLoadBalancerIs() + { + _getResult.Data.ShouldBeOfType(); + } + + private void ThenItIsAdded() + { + _addResult.IsError.ShouldBe(false); + _addResult.ShouldBeOfType(); + } + + private void WhenIAddTheLoadBalancer() + { + _addResult = _loadBalancerHouse.Add(_key, _loadBalancer); + } + + + private void GivenThereIsALoadBalancer(string key, ILoadBalancer loadBalancer) + { + _key = key; + _loadBalancer = loadBalancer; + WhenIAddTheLoadBalancer(); + } + + private void WhenWeGetTheLoadBalancer(string key) + { + _getResult = _loadBalancerHouse.Get(key); + } + + private void ThenItIsReturned() + { + _getResult.Data.ShouldBe(_loadBalancer); + } + + class FakeLoadBalancer : ILoadBalancer + { + public Task> Lease() + { + throw new NotImplementedException(); + } + + public void Release(HostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + class FakeRoundRobinLoadBalancer : ILoadBalancer + { + public Task> Lease() + { + throw new NotImplementedException(); + } + + public void Release(HostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs new file mode 100644 index 000000000..5a9eec872 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -0,0 +1,210 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Middleware; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.Values; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerMiddlewareTests + { + private readonly Mock _loadBalancerHouse; + private readonly Mock _scopedRepository; + private readonly Mock _loadBalancer; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private HostAndPort _hostAndPort; + private OkResponse _request; + private OkResponse _downstreamUrl; + private OkResponse _downstreamRoute; + private ErrorResponse _getLoadBalancerHouseError; + private ErrorResponse _getHostAndPortError; + + public LoadBalancerMiddlewareTests() + { + _url = "http://localhost:51879"; + _loadBalancerHouse = new Mock(); + _scopedRepository = new Mock(); + _loadBalancer = new Mock(); + _loadBalancerHouse = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_loadBalancerHouse.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseLoadBalancingMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_load_balancer() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_least() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + private void GivenTheLoadBalancerReturnsAnError() + { + _getHostAndPortError = new ErrorResponse(new List() { new ServicesAreNullError($"services were null for bah") }); + _loadBalancer + .Setup(x => x.Lease()) + .ReturnsAsync(_getHostAndPortError); + } + + private void GivenTheLoadBalancerReturns() + { + _hostAndPort = new HostAndPort("127.0.0.1", 80); + _loadBalancer + .Setup(x => x.Lease()) + .ReturnsAsync(new OkResponse(_hostAndPort)); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void GivenTheLoadBalancerHouseReturns() + { + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(_loadBalancer.Object)); + } + + + private void GivenTheLoadBalancerHouseReturnsAnError() + { + _getLoadBalancerHouseError = new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for bah") + }); + + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(_getLoadBalancerHouseError); + } + + private void ThenTheScopedDataRepositoryIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("HostAndPort", _hostAndPort), Times.Once()); + } + + private void ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", _getLoadBalancerHouseError.Errors), Times.Once); + } + + private void ThenAnErrorSayingReleaseFailedIsSetOnThePipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", It.IsAny>()), Times.Once); + } + + private void ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", _getHostAndPortError.Errors), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamUrlIs(string downstreamUrl) + { + _downstreamUrl = new OkResponse(downstreamUrl); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamUrl); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs new file mode 100644 index 000000000..ac89a6d02 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class NoLoadBalancerTests + { + private List _services; + private NoLoadBalancer _loadBalancer; + private Response _result; + + [Fact] + public void should_return_host_and_port() + { + var hostAndPort = new HostAndPort("127.0.0.1", 80); + + var services = new List + { + new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + }; + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheHostAndPortIs(hostAndPort)) + .BDDfy(); + } + + private void GivenServices(List services) + { + _services = services; + } + + private void WhenIGetTheNextHostAndPort() + { + _loadBalancer = new NoLoadBalancer(_services); + _result = _loadBalancer.Lease().Result; + } + + private void ThenTheHostAndPortIs(HostAndPort expected) + { + _result.Data.ShouldBe(expected); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs new file mode 100644 index 000000000..f2ef53674 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class RoundRobinTests + { + private readonly RoundRobinLoadBalancer _roundRobin; + private readonly List _services; + private Response _hostAndPort; + + public RoundRobinTests() + { + _services = new List + { + new Service("product", new HostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, new string[0]), + new Service("product", new HostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]), + new Service("product", new HostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]) + }; + + _roundRobin = new RoundRobinLoadBalancer(_services); + } + + [Fact] + public void should_get_next_address() + { + this.Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(0)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(1)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(2)) + .BDDfy(); + } + + [Fact] + public void should_go_back_to_first_address_after_finished_last() + { + var stopWatch = Stopwatch.StartNew(); + + while (stopWatch.ElapsedMilliseconds < 1000) + { + var address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[0].HostAndPort); + address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[1].HostAndPort); + address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[2].HostAndPort); + } + } + + private void GivenIGetTheNextAddress() + { + _hostAndPort = _roundRobin.Lease().Result; + } + + private void ThenTheNextAddressIndexIs(int index) + { + _hostAndPort.Data.ShouldBe(_services[index].HostAndPort); + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index b643028ed..09a5c22c7 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -62,19 +62,11 @@ public void should_not_return_any_errors() { this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) .And(x => x.GivenThereAreNoPipelineErrors()) - .And(x => x.GivenTheResponderReturns()) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenThereAreNoErrors()) .BDDfy(); } - private void GivenTheResponderReturns() - { - _responder - .Setup(x => x.SetResponseOnHttpContext(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OkResponse()); - } - private void GivenThereAreNoPipelineErrors() { _scopedRepository diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs new file mode 100644 index 000000000..f1e732e76 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Ocelot.ServiceDiscovery; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ConfigurationServiceProviderTests + { + private ConfigurationServiceProvider _serviceProvider; + private HostAndPort _hostAndPort; + private List _result; + private List _expected; + + [Fact] + public void should_return_services() + { + var hostAndPort = new HostAndPort("127.0.0.1", 80); + + var services = new List + { + new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + }; + + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheService()) + .Then(x => x.ThenTheFollowingIsReturned(services)) + .BDDfy(); + } + + private void GivenServices(List services) + { + _expected = services; + } + + private void WhenIGetTheService() + { + _serviceProvider = new ConfigurationServiceProvider(_expected); + _result = _serviceProvider.Get().Result; + } + + private void ThenTheFollowingIsReturned(List services) + { + _result[0].HostAndPort.DownstreamHost.ShouldBe(services[0].HostAndPort.DownstreamHost); + + _result[0].HostAndPort.DownstreamPort.ShouldBe(services[0].HostAndPort.DownstreamPort); + + _result[0].Name.ShouldBe(services[0].Name); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs new file mode 100644 index 000000000..7dae5e472 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -0,0 +1,57 @@ +using Ocelot.Configuration; +using Ocelot.ServiceDiscovery; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ServiceProviderFactoryTests + { + private ServiceProviderConfiguraion _serviceConfig; + private IServiceDiscoveryProvider _result; + private readonly ServiceDiscoveryProviderFactory _factory; + + public ServiceProviderFactoryTests() + { + _factory = new ServiceDiscoveryProviderFactory(); + } + + [Fact] + public void should_return_no_service_provider() + { + var serviceConfig = new ServiceProviderConfiguraion("product", "127.0.0.1", 80, false, "Does not matter", string.Empty, 0); + + this.Given(x => x.GivenTheReRoute(serviceConfig)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_consul_service_provider() + { + var serviceConfig = new ServiceProviderConfiguraion("product", string.Empty, 0, true, "Consul", string.Empty, 0); + + this.Given(x => x.GivenTheReRoute(serviceConfig)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + private void GivenTheReRoute(ServiceProviderConfiguraion serviceConfig) + { + _serviceConfig = serviceConfig; + } + + private void WhenIGetTheServiceProvider() + { + _result = _factory.Get(_serviceConfig); + } + + private void ThenTheServiceProviderIs() + { + _result.ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs new file mode 100644 index 000000000..874253293 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ServiceRegistryTests + { + private Service _service; + private List _services; + private ServiceRegistry _serviceRegistry; + private ServiceRepository _serviceRepository; + + public ServiceRegistryTests() + { + _serviceRepository = new ServiceRepository(); + _serviceRegistry = new ServiceRegistry(_serviceRepository); + } + + [Fact] + public void should_register_service() + { + this.Given(x => x.GivenAServiceToRegister("product", "localhost:5000", 80)) + .When(x => x.WhenIRegisterTheService()) + .Then(x => x.ThenTheServiceIsRegistered()) + .BDDfy(); + } + + public void should_lookup_service() + { + this.Given(x => x.GivenAServiceIsRegistered("product", "localhost:600", 80)) + .When(x => x.WhenILookupTheService("product")) + .Then(x => x.ThenTheServiceDetailsAreReturned()) + .BDDfy(); + } + + private void ThenTheServiceDetailsAreReturned() + { + _services[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); + _services[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); + _services[0].Name.ShouldBe(_service.Name); + } + + private void WhenILookupTheService(string name) + { + _services = _serviceRegistry.Lookup(name); + } + + private void GivenAServiceIsRegistered(string name, string address, int port) + { + _service = new Service(name, new HostAndPort(address, port), string.Empty, string.Empty, new string[0]); + _serviceRepository.Set(_service); + } + + private void GivenAServiceToRegister(string name, string address, int port) + { + _service = new Service(name, new HostAndPort(address, port), string.Empty, string.Empty, new string[0]); + } + + private void WhenIRegisterTheService() + { + _serviceRegistry.Register(_service); + } + + private void ThenTheServiceIsRegistered() + { + var serviceNameAndAddress = _serviceRepository.Get(_service.Name); + serviceNameAndAddress[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); + serviceNameAndAddress[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); + serviceNameAndAddress[0].Name.ShouldBe(_service.Name); + } + } + + public interface IServiceRegistry + { + void Register(Service serviceNameAndAddress); + List Lookup(string name); + } + + public class ServiceRegistry : IServiceRegistry + { + private readonly IServiceRepository _repository; + public ServiceRegistry(IServiceRepository repository) + { + _repository = repository; + } + + public void Register(Service serviceNameAndAddress) + { + _repository.Set(serviceNameAndAddress); + } + + public List Lookup(string name) + { + return _repository.Get(name); + } + } + + public interface IServiceRepository + { + List Get(string serviceName); + void Set(Service serviceNameAndAddress); + } + + public class ServiceRepository : IServiceRepository + { + private Dictionary> _registeredServices; + + public ServiceRepository() + { + _registeredServices = new Dictionary>(); + } + + public List Get(string serviceName) + { + return _registeredServices[serviceName]; + } + + public void Set(Service serviceNameAndAddress) + { + List services; + if(_registeredServices.TryGetValue(serviceNameAndAddress.Name, out services)) + { + services.Add(serviceNameAndAddress); + _registeredServices[serviceNameAndAddress.Name] = services; + } + else + { + _registeredServices[serviceNameAndAddress.Name] = new List(){ serviceNameAndAddress }; + } + + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index ab3e6cb17..3151ac57c 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -32,7 +32,7 @@ "win7-x64": {} }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] }