Merge branch 'develop' into CircuitBreakerPattern

This commit is contained in:
geffzhang 2017-02-07 18:41:11 +08:00 committed by GitHub
commit a895fdfc12
96 changed files with 3067 additions and 232 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
.gitignore vendored
View File

@ -235,3 +235,4 @@ _Pvt_Extensions
# FAKE - F# Make # FAKE - F# Make
.fake/ .fake/
tools/

4
GitVersion.yml Normal file
View File

@ -0,0 +1,4 @@
mode: ContinuousDelivery
branches: {}
ignore:
sha: []

View File

@ -9,18 +9,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
appveyor.yml = appveyor.yml appveyor.yml = appveyor.yml
build-and-run-tests.bat = build-and-run-tests.bat build-and-release-unstable.ps1 = build-and-release-unstable.ps1
build.bat = build.bat 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-explanation.txt = configuration-explanation.txt
configuration.yaml = configuration.yaml
GitVersion.yml = GitVersion.yml
global.json = global.json global.json = global.json
LICENSE.md = LICENSE.md LICENSE.md = LICENSE.md
Ocelot.nuspec = Ocelot.nuspec Ocelot.nuspec = Ocelot.nuspec
push-to-nuget.bat = push-to-nuget.bat
README.md = README.md README.md = README.md
run-acceptance-tests.bat = run-acceptance-tests.bat release.ps1 = release.ps1
run-benchmarks.bat = run-benchmarks.bat ReleaseNotes.md = ReleaseNotes.md
run-tests.bat = run-tests.bat run-acceptance-tests.ps1 = run-acceptance-tests.ps1
run-unit-tests.bat = run-unit-tests.bat run-benchmarks.ps1 = run-benchmarks.ps1
run-unit-tests.ps1 = run-unit-tests.ps1
version.ps1 = version.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}"

View File

@ -1,6 +1,6 @@
# Ocelot # 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) [![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 ## How to install
Ocelot is designed to work with ASP.NET core only and is currently 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 Install Ocelot and it's dependecies using nuget. At the moment
all we have is the pre version. Once we have something working in all we have is the pre version. Once we have something working in
a half decent way we will drop a version. 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/) 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 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! 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 ## Authentication
Ocelot currently supports the use of bearer tokens with Identity Server (more providers to 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) You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1)

1
ReleaseNotes.md Normal file
View File

@ -0,0 +1 @@
No issues closed since last release

View File

@ -3,10 +3,6 @@ configuration:
- Release - Release
platform: Any CPU platform: Any CPU
build_script: build_script:
- build.bat - build.ps1
test_script:
- run-tests.bat
after_test:
- push-to-nuget.bat %appveyor_build_version% %nugetApiKey%
cache: cache:
- '%USERPROFILE%\.nuget\packages' - '%USERPROFILE%\.nuget\packages'

View File

@ -0,0 +1 @@
./build.ps1 -target BuildAndReleaseUnstable

View File

@ -1,2 +0,0 @@
./run-tests.bat
./build.bat

1
build-and-run-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunTests

View File

@ -1,8 +0,0 @@
echo -------------------------
echo Building Ocelot
dotnet restore src/Ocelot
dotnet build src/Ocelot -c Release

347
build.cake Normal file
View File

@ -0,0 +1,347 @@
#tool "nuget:?package=GitVersion.CommandLine"
#tool "nuget:?package=OpenCover"
#tool "nuget:?package=ReportGenerator"
#tool "nuget:?package=GitReleaseNotes"
#addin "nuget:?package=Cake.DoInDirectory"
#addin "nuget:?package=Cake.Json"
// compile
var compileConfig = Argument("configuration", "Release");
var projectJson = "./src/Ocelot/project.json";
// build artifacts
var artifactsDir = Directory("artifacts");
// unit testing
var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests");
var unitTestAssemblies = @"./test/Ocelot.UnitTests";
// acceptance testing
var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests");
var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests";
// benchmark testing
var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests");
var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks";
// packaging
var packagesDir = artifactsDir + Directory("Packages");
var releaseNotesFile = packagesDir + File("releasenotes.md");
var artifactsFile = packagesDir + File("artifacts.txt");
// unstable releases
var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable");
var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// stable releases
var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/";
var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable");
var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// internal build variables - don't change these.
var releaseTag = "";
var committedVersion = "0.0.0-dev";
var buildVersion = committedVersion;
var target = Argument("target", "Default");
Information("target is " +target);
Information("Build configuration is " + compileConfig);
Task("Default")
.IsDependentOn("Build");
Task("Build")
.IsDependentOn("RunTests")
.IsDependentOn("CreatePackages");
Task("BuildAndReleaseUnstable")
.IsDependentOn("Build")
.IsDependentOn("ReleasePackagesToUnstableFeed");
Task("Clean")
.Does(() =>
{
if (DirectoryExists(artifactsDir))
{
DeleteDirectory(artifactsDir, recursive:true);
}
CreateDirectory(artifactsDir);
});
Task("Version")
.Does(() =>
{
var nugetVersion = GetNuGetVersionForCommit();
Information("SemVer version number: " + nugetVersion);
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Persisting version number...");
PersistVersion(nugetVersion);
buildVersion = nugetVersion;
}
else
{
Information("We are not running on build server, so we won't persist the version number.");
}
});
Task("Restore")
.IsDependentOn("Clean")
.IsDependentOn("Version")
.Does(() =>
{
DotNetCoreRestore("./src");
DotNetCoreRestore("./test");
});
Task("RunUnitTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForUnitTestsDir);
DotNetCoreTest(unitTestAssemblies, buildSettings);
});
Task("RunAcceptanceTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = "Debug", //acceptance test config is hard-coded for debug
};
EnsureDirectoryExists(artifactsForAcceptanceTestsDir);
DoInDirectory("test/Ocelot.AcceptanceTests", () =>
{
DotNetCoreTest(".", buildSettings);
});
});
Task("RunBenchmarkTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreRunSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForBenchmarkTestsDir);
DoInDirectory(benchmarkTestAssemblies, () =>
{
DotNetCoreRun(".", "", buildSettings);
});
});
Task("RunTests")
.IsDependentOn("RunUnitTests")
.IsDependentOn("RunAcceptanceTests")
.Does(() =>
{
});
Task("CreatePackages")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
GenerateReleaseNotes();
var settings = new DotNetCorePackSettings
{
OutputDirectory = packagesDir,
NoBuild = true
};
DotNetCorePack(projectJson, settings);
System.IO.File.WriteAllLines(artifactsFile, new[]{
"nuget:Ocelot." + buildVersion + ".nupkg",
"nugetSymbols:Ocelot." + buildVersion + ".symbols.nupkg",
"releaseNotes:releasenotes.md"
});
if (AppVeyor.IsRunningOnAppVeyor)
{
var path = packagesDir.ToString() + @"/**/*";
foreach (var file in GetFiles(path))
{
AppVeyor.UploadArtifact(file.FullPath);
}
}
});
Task("ReleasePackagesToUnstableFeed")
.IsDependentOn("CreatePackages")
.Does(() =>
{
PublishPackages(nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl);
});
Task("EnsureStableReleaseRequirements")
.Does(() =>
{
if (!AppVeyor.IsRunningOnAppVeyor)
{
throw new Exception("Stable release should happen via appveyor");
}
var isTag =
AppVeyor.Environment.Repository.Tag.IsTag &&
!string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name);
if (!isTag)
{
throw new Exception("Stable release should happen from a published GitHub release");
}
});
Task("UpdateVersionInfo")
.IsDependentOn("EnsureStableReleaseRequirements")
.Does(() =>
{
releaseTag = AppVeyor.Environment.Repository.Tag.Name;
AppVeyor.UpdateBuildVersion(releaseTag);
});
Task("DownloadGitHubReleaseArtifacts")
.IsDependentOn("UpdateVersionInfo")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
var releaseUrl = tagsUrl + releaseTag;
var assets_url = ParseJson(GetResource(releaseUrl))
.GetValue("assets_url")
.Value<string>();
foreach(var asset in DeserializeJson<JArray>(GetResource(assets_url)))
{
var file = packagesDir + File(asset.Value<string>("name"));
Information("Downloading " + file);
DownloadFile(asset.Value<string>("browser_download_url"), file);
}
});
Task("ReleasePackagesToStableFeed")
.IsDependentOn("DownloadGitHubReleaseArtifacts")
.Does(() =>
{
PublishPackages(nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);
});
Task("Release")
.IsDependentOn("ReleasePackagesToStableFeed");
RunTarget(target);
/// Gets nuique nuget version for this commit
private string GetNuGetVersionForCommit()
{
GitVersion(new GitVersionSettings{
UpdateAssemblyInfo = false,
OutputType = GitVersionOutput.BuildServer
});
var versionInfo = GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });
return versionInfo.NuGetVersion;
}
/// Updates project version in all of our projects
private void PersistVersion(string version)
{
Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, version));
var projectJsonFiles = GetFiles("./**/project.json");
foreach(var projectJsonFile in projectJsonFiles)
{
var file = projectJsonFile.ToString();
Information(string.Format("Updating {0}...", file));
var updatedProjectJson = System.IO.File.ReadAllText(file)
.Replace(committedVersion, version);
System.IO.File.WriteAllText(file, updatedProjectJson);
}
}
/// generates release notes based on issues closed in GitHub since the last release
private void GenerateReleaseNotes()
{
Information("Generating release notes at " + releaseNotesFile);
var releaseNotesExitCode = StartProcess(
@"tools/GitReleaseNotes/tools/gitreleasenotes.exe",
new ProcessSettings { Arguments = ". /o " + releaseNotesFile });
if (string.IsNullOrEmpty(System.IO.File.ReadAllText(releaseNotesFile)))
{
System.IO.File.WriteAllText(releaseNotesFile, "No issues closed since last release");
}
if (releaseNotesExitCode != 0)
{
throw new Exception("Failed to generate release notes");
}
}
/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file
private void PublishPackages(string feedApiKey, string codeFeedUrl, string symbolFeedUrl)
{
var artifacts = System.IO.File
.ReadAllLines(artifactsFile)
.Select(l => l.Split(':'))
.ToDictionary(v => v[0], v => v[1]);
var codePackage = packagesDir + File(artifacts["nuget"]);
var symbolsPackage = packagesDir + File(artifacts["nugetSymbols"]);
NuGetPush(
codePackage,
new NuGetPushSettings {
ApiKey = feedApiKey,
Source = codeFeedUrl
});
NuGetPush(
symbolsPackage,
new NuGetPushSettings {
ApiKey = feedApiKey,
Source = symbolFeedUrl
});
}
/// gets the resource from the specified url
private string GetResource(string url)
{
Information("Getting resource from " + url);
var assetsRequest = System.Net.WebRequest.CreateHttp(url);
assetsRequest.Method = "GET";
assetsRequest.Accept = "application/vnd.github.v3+json";
assetsRequest.UserAgent = "BuildScript";
using (var assetsResponse = assetsRequest.GetResponse())
{
var assetsStream = assetsResponse.GetResponseStream();
var assetsReader = new StreamReader(assetsStream);
return assetsReader.ReadToEnd();
}
}

189
build.ps1 Normal file
View File

@ -0,0 +1,189 @@
##########################################################################
# This is the Cake bootstrapper script for PowerShell.
# This file was downloaded from https://github.com/cake-build/resources
# Feel free to change this file to fit your needs.
##########################################################################
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Script
The build script to execute.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
.PARAMETER Experimental
Tells Cake to use the latest Roslyn release.
.PARAMETER WhatIf
Performs a dry run of the build script.
No tasks will be executed.
.PARAMETER Mono
Tells Cake to use the Mono scripting engine.
.PARAMETER SkipToolPackageRestore
Skips restoring of packages.
.PARAMETER ScriptArgs
Remaining arguments are added here.
.LINK
http://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Script = "build.cake",
[string]$Target = "Default",
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[switch]$Experimental,
[Alias("DryRun","Noop")]
[switch]$WhatIf,
[switch]$Mono,
[switch]$SkipToolPackageRestore,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
function MD5HashFile([string] $filePath)
{
if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf))
{
return $null
}
[System.IO.Stream] $file = $null;
[System.Security.Cryptography.MD5] $md5 = $null;
try
{
$md5 = [System.Security.Cryptography.MD5]::Create()
$file = [System.IO.File]::OpenRead($filePath)
return [System.BitConverter]::ToString($md5.ComputeHash($file))
}
finally
{
if ($file -ne $null)
{
$file.Dispose()
}
}
}
Write-Host "Preparing to run build script..."
if(!$PSScriptRoot){
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
}
$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config"
$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum"
# Should we use mono?
$UseMono = "";
if($Mono.IsPresent) {
Write-Verbose -Message "Using the Mono based scripting engine."
$UseMono = "-mono"
}
# Should we use the new Roslyn?
$UseExperimental = "";
if($Experimental.IsPresent -and !($Mono.IsPresent)) {
Write-Verbose -Message "Using experimental version of Roslyn."
$UseExperimental = "-experimental"
}
# Is this a dry run?
$UseDryRun = "";
if($WhatIf.IsPresent) {
$UseDryRun = "-dryrun"
}
# Make sure tools folder exists
if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
Write-Verbose -Message "Creating tools directory..."
New-Item -Path $TOOLS_DIR -Type directory | out-null
}
# Make sure that packages.config exist.
if (!(Test-Path $PACKAGES_CONFIG)) {
Write-Verbose -Message "Downloading packages.config..."
try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch {
Throw "Could not download packages.config."
}
}
# Try find NuGet.exe in path if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Trying to find nuget.exe in PATH..."
$existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) }
$NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1
if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) {
Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)."
$NUGET_EXE = $NUGET_EXE_IN_PATH.FullName
}
}
# Try download NuGet.exe if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Downloading NuGet.exe..."
try {
(New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
} catch {
Throw "Could not download NuGet.exe."
}
}
# Save nuget.exe path to environment to be available to child processed
$ENV:NUGET_EXE = $NUGET_EXE
# Restore tools from NuGet?
if(-Not $SkipToolPackageRestore.IsPresent) {
Push-Location
Set-Location $TOOLS_DIR
# Check for changes in packages.config and remove installed tools if true.
[string] $md5Hash = MD5HashFile($PACKAGES_CONFIG)
if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or
($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) {
Write-Verbose -Message "Missing or changed package.config hash..."
Remove-Item * -Recurse -Exclude packages.config,nuget.exe
}
Write-Verbose -Message "Restoring tools from NuGet..."
$NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
if ($LASTEXITCODE -ne 0) {
Throw "An error occured while restoring NuGet tools."
}
else
{
$md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII"
}
Write-Verbose -Message ($NuGetOutput | out-string)
Pop-Location
}
# Make sure that Cake has been installed.
if (!(Test-Path $CAKE_EXE)) {
Throw "Could not find Cake.exe at $CAKE_EXE"
}
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs"
exit $LASTEXITCODE

22
build.readme.md Normal file
View File

@ -0,0 +1,22 @@
#1. Overview
This document summarises the build and release process for the project. The build scripts are written using [Cake](http://cakebuild.net/), and are defined in `./build.cake`. The scripts have been designed to be run by either developers locally or by a build server (currently [AppVeyor](https://www.appveyor.com/)), with minimal logic defined in the build server itself.
#2. Building
* You'll generally want to run the `./build.ps1` script. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory.
* You can view the current commit's [SemVer](http://semver.org/) build information by running `./version.ps1`.
* The other `./*.ps1` scripts perform subsets of the build process, if you don't want to run the full build.
* The release process works best with GitFlow branching; this allows us to publish every development commit to an unstable feed with a unique SemVer version, and then choose when to release to a stable feed.
#3. Release process
This section defines the release process for the maintainers of the project.
* Merge pull requests to the `release` branch.
* Every commit pushed to the Origin repo will kick off the [ocelot-build](https://ci.appveyor.com/project/binarymash/ocelot) project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed.
* When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory.
* When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub.
* In Github, navigate to the [release](https://github.com/binarymash/Ocelot/releases). Modify the release name and tag as desired.
* When you're ready, publish the release. This will tag the commit with the specified release number.
* The [ocelot-release](https://ci.appveyor.com/project/binarymash/ocelot-wtaj9) project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed.
* When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing.
* Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub.

101
build.sh Executable file
View File

@ -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

View File

@ -80,12 +80,24 @@
# the caching a lot. # the caching a lot.
"FileCacheOptions": { "TtlSeconds": 15 }, "FileCacheOptions": { "TtlSeconds": 15 },
# The value of this is used when matching the upstream template to an upstream url. # 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 # This section is meant to be for global configuration settings
"GlobalConfiguration": { "GlobalConfiguration": {
# If this is set it will override any route specific request id keys, behaves the same # If this is set it will override any route specific request id keys, behaves the same
# otherwise # otherwise
"RequestIdKey": "OcRequestId", "RequestIdKey": "OcRequestId",
# If set Ocelot will try and use service discovery to locate downstream hosts and ports
"ServiceDiscoveryProvider":
{
"Provider":"Consul",
"Host":"localhost",
"Port":8500
}
} }
} }

View File

@ -1,11 +0,0 @@
echo -------------------------
echo Packing Ocelot Version %1
nuget pack .\Ocelot.nuspec -version %1
echo Publishing Ocelot
nuget push Ocelot.%1.nupkg -ApiKey %2 -Source https://www.nuget.org/api/v2/package

1
release.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target Release

View File

@ -1,8 +0,0 @@
echo Running Ocelot.AcceptanceTests
cd test/Ocelot.AcceptanceTests/
dotnet restore
dotnet test
cd ../../
echo Restoring Ocelot.ManualTest
dotnet restore test/Ocelot.ManualTest/

1
run-acceptance-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build -target RunAcceptanceTests

View File

@ -1,15 +0,0 @@
echo -------------------------
echo Running Ocelot.Benchmarks
cd test/Ocelot.Benchmarks
dotnet restore
dotnet run
cd ../../

1
run-benchmarks.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunBenchmarkTests

View File

@ -1,2 +0,0 @@
./run-unit-tests.bat
./run-acceptance-tests.bat

View File

@ -1,8 +0,0 @@
echo -------------------------
echo Restoring Ocelot
dotnet restore src/Ocelot
echo Running Ocelot.UnitTests
dotnet restore test/Ocelot.UnitTests/
dotnet test test/Ocelot.UnitTests/

1
run-unit-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunUnitTests

View File

@ -6,6 +6,7 @@ namespace Ocelot.Configuration.Builder
{ {
public class ReRouteBuilder public class ReRouteBuilder
{ {
private string _loadBalancerKey;
private string _downstreamPathTemplate; private string _downstreamPathTemplate;
private string _upstreamTemplate; private string _upstreamTemplate;
private string _upstreamTemplatePattern; private string _upstreamTemplatePattern;
@ -35,12 +36,22 @@ namespace Ocelot.Configuration.Builder
private int _exceptionsAllowedBeforeBreaking; private int _exceptionsAllowedBeforeBreaking;
private int _durationOfBreak; private int _durationOfBreak;
private int _timeoutValue; private int _timeoutValue;
private string _loadBalancer;
private string _serviceProviderHost;
private int _serviceProviderPort;
public ReRouteBuilder() public ReRouteBuilder()
{ {
_additionalScopes = new List<string>(); _additionalScopes = new List<string>();
} }
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
{
_loadBalancer = loadBalancer;
return this;
}
public ReRouteBuilder WithDownstreamScheme(string downstreamScheme) public ReRouteBuilder WithDownstreamScheme(string downstreamScheme)
{ {
_downstreamScheme = downstreamScheme; _downstreamScheme = downstreamScheme;
@ -213,16 +224,33 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ReRouteBuilder WithLoadBalancerKey(string loadBalancerKey)
{
_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() public ReRoute Build()
{ {
Func<HostAndPort> downstreamHostFunc = () => new HostAndPort(_downstreamHost, _dsPort);
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
_isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName,
_requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement,
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName, _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer,
_useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, downstreamHostFunc, _downstreamScheme, _downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery, _serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort)
_exceptionsAllowedBeforeBreaking,_durationOfBreak, _timeoutValue); _exceptionsAllowedBeforeBreaking,_durationOfBreak, _timeoutValue);
} }
} }
} }

View File

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator; using Ocelot.Configuration.Validator;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Utilities; using Ocelot.Utilities;
using Ocelot.Values; using Ocelot.Values;
@ -25,22 +27,28 @@ namespace Ocelot.Configuration.Creator
private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser;
private readonly ILogger<FileOcelotConfigurationCreator> _logger; private readonly ILogger<FileOcelotConfigurationCreator> _logger;
private readonly ILoadBalancerFactory _loadBalanceFactory;
private readonly ILoadBalancerHouse _loadBalancerHouse;
public FileOcelotConfigurationCreator( public FileOcelotConfigurationCreator(
IOptions<FileConfiguration> options, IOptions<FileConfiguration> options,
IConfigurationValidator configurationValidator, IConfigurationValidator configurationValidator,
IClaimToThingConfigurationParser claimToThingConfigurationParser, IClaimToThingConfigurationParser claimToThingConfigurationParser,
ILogger<FileOcelotConfigurationCreator> logger) ILogger<FileOcelotConfigurationCreator> logger,
ILoadBalancerFactory loadBalancerFactory,
ILoadBalancerHouse loadBalancerHouse)
{ {
_loadBalanceFactory = loadBalancerFactory;
_loadBalancerHouse = loadBalancerHouse;
_options = options; _options = options;
_configurationValidator = configurationValidator; _configurationValidator = configurationValidator;
_claimToThingConfigurationParser = claimToThingConfigurationParser; _claimToThingConfigurationParser = claimToThingConfigurationParser;
_logger = logger; _logger = logger;
} }
public Response<IOcelotConfiguration> Create() public async Task<Response<IOcelotConfiguration>> Create()
{ {
var config = SetUpConfiguration(); var config = await SetUpConfiguration();
return new OkResponse<IOcelotConfiguration>(config); return new OkResponse<IOcelotConfiguration>(config);
} }
@ -49,7 +57,7 @@ namespace Ocelot.Configuration.Creator
/// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see /// 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 /// will need a refactor at some point as its crap
/// </summary> /// </summary>
private IOcelotConfiguration SetUpConfiguration() private async Task<IOcelotConfiguration> SetUpConfiguration()
{ {
var response = _configurationValidator.IsValid(_options.Value); var response = _configurationValidator.IsValid(_options.Value);
@ -69,64 +77,87 @@ namespace Ocelot.Configuration.Creator
foreach (var reRoute in _options.Value.ReRoutes) 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); reRoutes.Add(ocelotReRoute);
} }
return new OcelotConfiguration(reRoutes); return new OcelotConfiguration(reRoutes);
} }
private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) private async Task<ReRoute> SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration)
{ {
var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); 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 var requestIdKey = globalRequestIdConfiguration
? globalConfiguration.RequestIdKey ? globalConfiguration.RequestIdKey
: reRoute.RequestIdKey; : fileReRoute.RequestIdKey;
var useServiceDiscovery = !string.IsNullOrEmpty(reRoute.ServiceName) var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName)
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address)
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider);
Func<HostAndPort> downstreamHostAndPortFunc = () => new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort); Func<HostAndPort> downstreamHostAndPortFunc = () => new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort);
//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}";
ReRoute reRoute;
var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName,
fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, useServiceDiscovery,
globalConfiguration?.ServiceDiscoveryProvider?.Provider, globalConfiguration?.ServiceDiscoveryProvider?.Host,
serviceProviderPort);
if (isAuthenticated) if (isAuthenticated)
{ {
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, var authOptionsForRoute = new AuthenticationOptions(fileReRoute.AuthenticationOptions.Provider,
reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName, fileReRoute.AuthenticationOptions.ProviderRootUrl, fileReRoute.AuthenticationOptions.ScopeName,
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, fileReRoute.AuthenticationOptions.RequireHttps, fileReRoute.AuthenticationOptions.AdditionalScopes,
reRoute.AuthenticationOptions.ScopeSecret); fileReRoute.AuthenticationOptions.ScopeSecret);
var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest); var claimsToHeaders = GetAddThingsToRequest(fileReRoute.AddHeadersToRequest);
var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest); var claimsToClaims = GetAddThingsToRequest(fileReRoute.AddClaimsToRequest);
var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest); var claimsToQueries = GetAddThingsToRequest(fileReRoute.AddQueriesToRequest);
return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate),
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, fileReRoute.UpstreamTemplate,
fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, claimsToHeaders, claimsToClaims, authOptionsForRoute, claimsToHeaders, claimsToClaims,
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, fileReRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds)
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, , fileReRoute.DownstreamScheme,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme, fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
reRoute.ExceptionsAllowedBeforeBreaking, reRoute.DurationOfBreak, reRoute.TimeoutValue); serviceProviderConfiguration, serviceProviderConfiguration ,reRoute.ExceptionsAllowedBeforeBreaking,
reRoute.DurationOfBreak, reRoute.TimeoutValue);
}
else
{
reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate),
fileReRoute.UpstreamTemplate,
fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
null, new List<ClaimToThing>(), new List<ClaimToThing>(),
fileReRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds),
fileReRoute.DownstreamScheme,
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
serviceProviderConfiguration, serviceProviderConfiguration ,reRoute.ExceptionsAllowedBeforeBreaking,
reRoute.DurationOfBreak, reRoute.TimeoutValue);
} }
return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, var loadBalancer = await _loadBalanceFactory.Get(reRoute);
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, _loadBalancerHouse.Add(reRoute.LoadBalancerKey, loadBalancer);
null, new List<ClaimToThing>(), new List<ClaimToThing>(), return reRoute;
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds),
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme,
reRoute.ExceptionsAllowedBeforeBreaking, reRoute.DurationOfBreak, reRoute.TimeoutValue);
} }
private string BuildUpstreamTemplate(FileReRoute reRoute) private string BuildUpstreamTemplate(FileReRoute reRoute)

View File

@ -1,9 +1,10 @@
using System.Threading.Tasks;
using Ocelot.Responses; using Ocelot.Responses;
namespace Ocelot.Configuration.Creator namespace Ocelot.Configuration.Creator
{ {
public interface IOcelotConfigurationCreator public interface IOcelotConfigurationCreator
{ {
Response<IOcelotConfiguration> Create(); Task<Response<IOcelotConfiguration>> Create();
} }
} }

View File

@ -32,5 +32,6 @@ namespace Ocelot.Configuration.File
public int ExceptionsAllowedBeforeBreaking { get; set; } public int ExceptionsAllowedBeforeBreaking { get; set; }
public int DurationOfBreak { get; set; } public int DurationOfBreak { get; set; }
public int TimeoutValue { get; set; } public int TimeoutValue { get; set; }
public string LoadBalancer {get;set;}
} }
} }

View File

@ -3,6 +3,7 @@ namespace Ocelot.Configuration.File
public class FileServiceDiscoveryProvider public class FileServiceDiscoveryProvider
{ {
public string Provider {get;set;} public string Provider {get;set;}
public string Address {get;set;} public string Host {get;set;}
public int Port { get; set; }
} }
} }

View File

@ -1,9 +1,10 @@
using Ocelot.Responses; using System.Threading.Tasks;
using Ocelot.Responses;
namespace Ocelot.Configuration.Provider namespace Ocelot.Configuration.Provider
{ {
public interface IOcelotConfigurationProvider public interface IOcelotConfigurationProvider
{ {
Response<IOcelotConfiguration> Get(); Task<Response<IOcelotConfiguration>> Get();
} }
} }

View File

@ -1,4 +1,5 @@
using Ocelot.Configuration.Creator; using System.Threading.Tasks;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.Repository; using Ocelot.Configuration.Repository;
using Ocelot.Responses; using Ocelot.Responses;
@ -19,7 +20,7 @@ namespace Ocelot.Configuration.Provider
_creator = creator; _creator = creator;
} }
public Response<IOcelotConfiguration> Get() public async Task<Response<IOcelotConfiguration>> Get()
{ {
var repoConfig = _repo.Get(); var repoConfig = _repo.Get();
@ -30,7 +31,7 @@ namespace Ocelot.Configuration.Provider
if (repoConfig.Data == null) if (repoConfig.Data == null)
{ {
var creatorConfig = _creator.Create(); var creatorConfig = await _creator.Create();
if (creatorConfig.IsError) if (creatorConfig.IsError)
{ {

View File

@ -6,13 +6,24 @@ namespace Ocelot.Configuration
{ {
public class ReRoute public class ReRoute
{ {
public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, public ReRoute(DownstreamPathTemplate downstreamPathTemplate,
bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties, string upstreamTemplate, string upstreamHttpMethod,
List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries, string upstreamTemplatePattern,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery, bool isAuthenticated, AuthenticationOptions authenticationOptions,
string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func<HostAndPort> downstreamHostAndPort, string downstreamScheme, List<ClaimToThing> configurationHeaderExtractorProperties,
List<ClaimToThing> claimsToClaims,
Dictionary<string, string> routeClaimsRequirement, bool isAuthorised,
List<ClaimToThing> claimsToQueries,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions,
string downstreamScheme, string loadBalancer, string downstreamHost,
int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion
int exceptionsAllowedBeforeBreaking =3, int durationofBreak =8, int timeoutValue = 5000) int exceptionsAllowedBeforeBreaking =3, int durationofBreak =8, int timeoutValue = 5000)
{ {
LoadBalancerKey = loadBalancerKey;
ServiceProviderConfiguraion = serviceProviderConfiguraion;
LoadBalancer = loadBalancer;
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
DownstreamPathTemplate = downstreamPathTemplate; DownstreamPathTemplate = downstreamPathTemplate;
UpstreamTemplate = upstreamTemplate; UpstreamTemplate = upstreamTemplate;
UpstreamHttpMethod = upstreamHttpMethod; UpstreamHttpMethod = upstreamHttpMethod;
@ -30,17 +41,13 @@ namespace Ocelot.Configuration
?? new List<ClaimToThing>(); ?? new List<ClaimToThing>();
ClaimsToHeaders = configurationHeaderExtractorProperties ClaimsToHeaders = configurationHeaderExtractorProperties
?? new List<ClaimToThing>(); ?? new List<ClaimToThing>();
ServiceName = serviceName;
UseServiceDiscovery = useServiceDiscovery;
ServiceDiscoveryProvider = serviceDiscoveryProvider;
ServiceDiscoveryAddress = serviceDiscoveryAddress;
DownstreamHostAndPort = downstreamHostAndPort;
DownstreamScheme = downstreamScheme; DownstreamScheme = downstreamScheme;
ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking;
DurationOfBreak = durationofBreak; DurationOfBreak = durationofBreak;
TimeoutValue = timeoutValue; TimeoutValue = timeoutValue;
} }
public string LoadBalancerKey {get;private set;}
public DownstreamPathTemplate DownstreamPathTemplate { get; private set; } public DownstreamPathTemplate DownstreamPathTemplate { get; private set; }
public string UpstreamTemplate { get; private set; } public string UpstreamTemplate { get; private set; }
public string UpstreamTemplatePattern { get; private set; } public string UpstreamTemplatePattern { get; private set; }
@ -55,14 +62,13 @@ namespace Ocelot.Configuration
public string RequestIdKey { get; private set; } public string RequestIdKey { get; private set; }
public bool IsCached { get; private set; } public bool IsCached { get; private set; }
public CacheOptions FileCacheOptions { 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<HostAndPort> DownstreamHostAndPort {get;private set;}
public string DownstreamScheme {get;private set;} public string DownstreamScheme {get;private set;}
public int ExceptionsAllowedBeforeBreaking { get; private set; } public int ExceptionsAllowedBeforeBreaking { get; private set; }
public int DurationOfBreak { get; private set; } public int DurationOfBreak { get; private set; }
public int TimeoutValue { get; private set; } public int TimeoutValue { 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; }
} }
} }

View File

@ -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; }
}
}

View File

@ -23,11 +23,13 @@ using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Headers; using Ocelot.Headers;
using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.QueryStrings; using Ocelot.QueryStrings;
using Ocelot.Request.Builder; using Ocelot.Request.Builder;
using Ocelot.Requester; using Ocelot.Requester;
using Ocelot.Responder; using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
namespace Ocelot.DependencyInjection namespace Ocelot.DependencyInjection
{ {
@ -59,6 +61,9 @@ namespace Ocelot.DependencyInjection
{ {
services.AddMvcCore().AddJsonFormatters(); services.AddMvcCore().AddJsonFormatters();
services.AddLogging(); services.AddLogging();
services.AddSingleton<IServiceDiscoveryProviderFactory, ServiceDiscoveryProviderFactory>();
services.AddSingleton<ILoadBalancerFactory, LoadBalancerFactory>();
services.AddSingleton<ILoadBalancerHouse, LoadBalancerHouse>();
services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>(); services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
services.AddSingleton<IUrlBuilder, UrlBuilder>(); services.AddSingleton<IUrlBuilder, UrlBuilder>();
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>(); services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Ocelot.Configuration.Provider; using Ocelot.Configuration.Provider;
using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Errors; using Ocelot.Errors;
@ -21,9 +22,9 @@ namespace Ocelot.DownstreamRouteFinder.Finder
_urlPathPlaceholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; _urlPathPlaceholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder;
} }
public Response<DownstreamRoute> FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod) public async Task<Response<DownstreamRoute>> 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)); var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase));

View File

@ -1,9 +1,10 @@
using Ocelot.Responses; using System.Threading.Tasks;
using Ocelot.Responses;
namespace Ocelot.DownstreamRouteFinder.Finder namespace Ocelot.DownstreamRouteFinder.Finder
{ {
public interface IDownstreamRouteFinder public interface IDownstreamRouteFinder
{ {
Response<DownstreamRoute> FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod); Task<Response<DownstreamRoute>> FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod);
} }
} }

View File

@ -34,7 +34,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware
_logger.LogDebug("upstream url path is {upstreamUrlPath}", upstreamUrlPath); _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) if (downstreamRoute.IsError)
{ {

View File

@ -6,6 +6,7 @@ using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.Middleware namespace Ocelot.DownstreamUrlCreator.Middleware
{ {
@ -46,7 +47,7 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme; var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme;
var dsHostAndPort = DownstreamRoute.ReRoute.DownstreamHostAndPort(); var dsHostAndPort = HostAndPort;
var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort); var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort);

View File

@ -41,13 +41,13 @@ namespace Ocelot.Errors.Middleware
var message = CreateMessage(context, e); var message = CreateMessage(context, e);
_logger.LogError(message, e); _logger.LogError(message, e);
await SetInternalServerErrorOnResponse(context); SetInternalServerErrorOnResponse(context);
} }
_logger.LogDebug("ocelot pipeline finished"); _logger.LogDebug("ocelot pipeline finished");
} }
private async Task SetInternalServerErrorOnResponse(HttpContext context) private void SetInternalServerErrorOnResponse(HttpContext context)
{ {
context.Response.OnStarting(x => context.Response.OnStarting(x =>
{ {

View File

@ -21,6 +21,10 @@
DownstreamPathTemplateContainsSchemeError, DownstreamPathTemplateContainsSchemeError,
DownstreamPathNullOrEmptyError, DownstreamPathNullOrEmptyError,
DownstreamSchemeNullOrEmptyError, DownstreamSchemeNullOrEmptyError,
DownstreamHostNullOrEmptyError DownstreamHostNullOrEmptyError,
ServicesAreNullError,
ServicesAreEmptyError,
UnableToFindServiceDiscoveryProviderError,
UnableToFindLoadBalancerError
} }
} }

View File

@ -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;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancer
{
Task<Response<HostAndPort>> Lease();
void Release(HostAndPort hostAndPort);
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ocelot.Configuration;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancerFactory
{
Task<ILoadBalancer> Get(ReRoute reRoute);
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Responses;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancerHouse
{
Response<ILoadBalancer> Get(string key);
Response Add(string key, ILoadBalancer loadBalancer);
}
}

View File

@ -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; }
}
}

View File

@ -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<Task<List<Service>>> _services;
private readonly List<Lease> _leases;
private readonly string _serviceName;
private static readonly object _syncLock = new object();
public LeastConnectionLoadBalancer(Func<Task<List<Service>>> services, string serviceName)
{
_services = services;
_serviceName = serviceName;
_leases = new List<Lease>();
}
public async Task<Response<HostAndPort>> Lease()
{
var services = await _services.Invoke();
if (services == null)
{
return new ErrorResponse<HostAndPort>(new List<Error>() { new ServicesAreNullError($"services were null for {_serviceName}") });
}
if (!services.Any())
{
return new ErrorResponse<HostAndPort>(new List<Error>() { 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<HostAndPort>(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<Service> services)
{
if (_leases.Count > 0)
{
var leasesToRemove = new List<Lease>();
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();
}
}
}

View File

@ -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<ILoadBalancer> 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());
}
}
}
}

View File

@ -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<string, ILoadBalancer> _loadBalancers;
public LoadBalancerHouse()
{
_loadBalancers = new Dictionary<string, ILoadBalancer>();
}
public Response<ILoadBalancer> Get(string key)
{
ILoadBalancer loadBalancer;
if(_loadBalancers.TryGetValue(key, out loadBalancer))
{
return new OkResponse<ILoadBalancer>(_loadBalancers[key]);
}
return new ErrorResponse<ILoadBalancer>(new List<Ocelot.Errors.Error>()
{
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();
}
}
}

View File

@ -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<Service> _services;
public NoLoadBalancer(List<Service> services)
{
_services = services;
}
public async Task<Response<HostAndPort>> Lease()
{
var service = await Task.FromResult(_services.FirstOrDefault());
return new OkResponse<HostAndPort>(service.HostAndPort);
}
public void Release(HostAndPort hostAndPort)
{
}
}
}

View File

@ -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<Service> _services;
private int _last;
public RoundRobinLoadBalancer(List<Service> services)
{
_services = services;
}
public async Task<Response<HostAndPort>> Lease()
{
if (_last >= _services.Count)
{
_last = 0;
}
var next = await Task.FromResult(_services[_last]);
_last++;
return new OkResponse<HostAndPort>(next.HostAndPort);
}
public void Release(HostAndPort hostAndPort)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class ServicesAreEmptyError : Error
{
public ServicesAreEmptyError(string message)
: base(message, OcelotErrorCode.ServicesAreEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class ServicesAreNullError : Error
{
public ServicesAreNullError(string message)
: base(message, OcelotErrorCode.ServicesAreNullError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class UnableToFindLoadBalancerError : Errors.Error
{
public UnableToFindLoadBalancerError(string message)
: base(message, OcelotErrorCode.UnableToFindLoadBalancerError)
{
}
}
}

View File

@ -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<QueryStringBuilderMiddleware>();
_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");
}
}
}

View File

@ -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<LoadBalancingMiddleware>();
}
}
}

View File

@ -3,6 +3,7 @@ using System.Net.Http;
using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder;
using Ocelot.Errors; using Ocelot.Errors;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Values;
namespace Ocelot.Middleware namespace Ocelot.Middleware
{ {
@ -69,6 +70,20 @@ namespace Ocelot.Middleware
} }
} }
public HostAndPort HostAndPort
{
get
{
var hostAndPort = _requestScopedDataRepository.Get<HostAndPort>("HostAndPort");
return hostAndPort.Data;
}
}
public void SetHostAndPortForThisRequest(HostAndPort hostAndPort)
{
_requestScopedDataRepository.Add("HostAndPort", hostAndPort);
}
public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute) public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute)
{ {
_requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute); _requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute);

View File

@ -18,6 +18,8 @@ namespace Ocelot.Middleware
using System.Threading.Tasks; using System.Threading.Tasks;
using Authorisation.Middleware; using Authorisation.Middleware;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.Provider;
using Ocelot.LoadBalancer.Middleware;
public static class OcelotMiddlewareExtensions public static class OcelotMiddlewareExtensions
{ {
@ -28,6 +30,7 @@ namespace Ocelot.Middleware
/// <returns></returns> /// <returns></returns>
public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder)
{ {
CreateConfiguration(builder);
builder.UseOcelot(new OcelotMiddlewareConfiguration()); builder.UseOcelot(new OcelotMiddlewareConfiguration());
return builder; return builder;
} }
@ -40,6 +43,8 @@ namespace Ocelot.Middleware
/// <returns></returns> /// <returns></returns>
public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration)
{ {
CreateConfiguration(builder);
// This is registered to catch any global exceptions that are not handled // This is registered to catch any global exceptions that are not handled
builder.UseExceptionHandlerMiddleware(); builder.UseExceptionHandlerMiddleware();
@ -98,6 +103,9 @@ namespace Ocelot.Middleware
// Now we can run any query string transformation logic // Now we can run any query string transformation logic
builder.UseQueryStringBuilderMiddleware(); 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 // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used
builder.UseDownstreamUrlCreatorMiddleware(); builder.UseDownstreamUrlCreatorMiddleware();
@ -114,6 +122,18 @@ namespace Ocelot.Middleware
return builder; 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<HttpContext, Func<Task>, Task> middleware) private static void UseIfNotNull(this IApplicationBuilder builder, Func<HttpContext, Func<Task>, Task> middleware)
{ {
if (middleware != null) if (middleware != null)

View File

@ -24,7 +24,7 @@ namespace Ocelot.Responder
_removeOutputHeaders = removeOutputHeaders; _removeOutputHeaders = removeOutputHeaders;
} }
public async Task<Response> SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response)
{ {
_removeOutputHeaders.Remove(response.Headers); _removeOutputHeaders.Remove(response.Headers);
@ -56,7 +56,6 @@ namespace Ocelot.Responder
{ {
await stream.CopyToAsync(context.Response.Body); await stream.CopyToAsync(context.Response.Body);
} }
return new OkResponse();
} }
private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair<string, IEnumerable<string>> httpResponseHeader) private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair<string, IEnumerable<string>> httpResponseHeader)
@ -67,14 +66,13 @@ namespace Ocelot.Responder
} }
} }
public async Task<Response> SetErrorResponseOnContext(HttpContext context, int statusCode) public void SetErrorResponseOnContext(HttpContext context, int statusCode)
{ {
context.Response.OnStarting(x => context.Response.OnStarting(x =>
{ {
context.Response.StatusCode = statusCode; context.Response.StatusCode = statusCode;
return Task.CompletedTask; return Task.CompletedTask;
}, context); }, context);
return new OkResponse();
} }
} }
} }

View File

@ -1,13 +1,12 @@
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Ocelot.Responses;
namespace Ocelot.Responder namespace Ocelot.Responder
{ {
public interface IHttpResponder public interface IHttpResponder
{ {
Task<Response> SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response);
Task<Response> SetErrorResponseOnContext(HttpContext context, int statusCode); void SetErrorResponseOnContext(HttpContext context, int statusCode);
} }
} }

View File

@ -46,34 +46,27 @@ namespace Ocelot.Responder.Middleware
_logger.LogDebug("received errors setting error response"); _logger.LogDebug("received errors setting error response");
await SetErrorResponse(context, errors); SetErrorResponse(context, errors);
} }
else else
{ {
_logger.LogDebug("no pipeline error, setting response"); _logger.LogDebug("no pipeline error, setting response");
var setResponse = await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); await _responder.SetResponseOnHttpContext(context, HttpResponseMessage);
if (setResponse.IsError)
{
_logger.LogDebug("error setting response, returning error to client");
await SetErrorResponse(context, setResponse.Errors);
}
} }
} }
private async Task SetErrorResponse(HttpContext context, List<Error> errors) private void SetErrorResponse(HttpContext context, List<Error> errors)
{ {
var statusCode = _codeMapper.Map(errors); var statusCode = _codeMapper.Map(errors);
if (!statusCode.IsError) if (!statusCode.IsError)
{ {
await _responder.SetErrorResponseOnContext(context, statusCode.Data); _responder.SetErrorResponseOnContext(context, statusCode.Data);
} }
else else
{ {
await _responder.SetErrorResponseOnContext(context, 500); _responder.SetErrorResponseOnContext(context, 500);
} }
} }
} }

View File

@ -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<Service> _services;
public ConfigurationServiceProvider(List<Service> services)
{
_services = services;
}
public async Task<List<Service>> Get()
{
return await Task.FromResult(_services);
}
}
}

View File

@ -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; }
}
}

View File

@ -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<List<Service>> 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<string>());
}
private string GetVersionFromStrings(IEnumerable<string> strings)
{
return strings
?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal))
.TrimStart(VersionPrefix);
}
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ocelot.Values;
namespace Ocelot.ServiceDiscovery
{
public interface IServiceDiscoveryProvider
{
Task<List<Service>> Get();
}
}

View File

@ -0,0 +1,10 @@
using System;
using Ocelot.Configuration;
namespace Ocelot.ServiceDiscovery
{
public interface IServiceDiscoveryProviderFactory
{
IServiceDiscoveryProvider Get(ServiceProviderConfiguraion serviceConfig);
}
}

View File

@ -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<Service>()
{
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);
}
}
}

View File

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

View File

@ -4,7 +4,7 @@
{ {
public HostAndPort(string downstreamHost, int downstreamPort) public HostAndPort(string downstreamHost, int downstreamPort)
{ {
DownstreamHost = downstreamHost; DownstreamHost = downstreamHost?.Trim('/');
DownstreamPort = downstreamPort; DownstreamPort = downstreamPort;
} }

View File

@ -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<string> 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<string> Tags { get; private set; }
public HostAndPort HostAndPort { get; private set; }
}
}

View File

@ -1,6 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
@ -28,15 +27,16 @@
"CacheManager.Core": "0.9.2", "CacheManager.Core": "0.9.2",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.2", "CacheManager.Microsoft.Extensions.Configuration": "0.9.2",
"CacheManager.Microsoft.Extensions.Logging": "0.9.2", "CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1"
"Polly": "5.0.3" "Polly": "5.0.3"
}, }
"runtimes": { "runtimes": {
"win10-x64": {}, "win10-x64": {},
"osx.10.11-x64":{}, "osx.10.11-x64":{},
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {
"netcoreapp1.4": { "netcoreapp1.1": {
"imports": [ "imports": [
] ]
} }

BIN
test/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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<ServiceEntry> _serviceEntries;
private int _counterOne;
private int _counterTwo;
private static readonly object _syncLock = new object();
public ServiceDiscoveryTests()
{
_steps = new Steps();
_serviceEntries = new List<ServiceEntry>();
}
[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<FileReRoute>
{
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();
}
}
}

View File

@ -5,6 +5,8 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using CacheManager.Core; using CacheManager.Core;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
@ -29,6 +31,12 @@ namespace Ocelot.AcceptanceTests
private BearerToken _token; private BearerToken _token;
public HttpClient OcelotClient => _ocelotClient; public HttpClient OcelotClient => _ocelotClient;
public string RequestIdKey = "OcRequestId"; public string RequestIdKey = "OcRequestId";
private Random _random;
public Steps()
{
_random = new Random();
}
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
{ {
@ -153,6 +161,28 @@ namespace Ocelot.AcceptanceTests
_response = _ocelotClient.GetAsync(url).Result; _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) public void WhenIGetUrlOnTheApiGateway(string url, string requestId)
{ {
_ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId);

View File

@ -4,15 +4,13 @@
public static class TestConfiguration public static class TestConfiguration
{ {
public static double Version => 1.4; public static double Version => 1.1;
public static string ConfigurationPath => GetConfigurationPath(); public static string ConfigurationPath => GetConfigurationPath();
public static string GetConfigurationPath() public static string GetConfigurationPath()
{ {
var osArchitecture = RuntimeInformation.OSArchitecture.ToString(); var osArchitecture = RuntimeInformation.OSArchitecture.ToString();
var oSDescription = string.Empty;
if(RuntimeInformation.OSDescription.ToLower().Contains("darwin")) if(RuntimeInformation.OSDescription.ToLower().Contains("darwin"))
{ {
return FormatConfigurationPath("osx.10.11", osArchitecture); return FormatConfigurationPath("osx.10.11", osArchitecture);

View File

@ -1 +1 @@
{"ReRoutes":[{"DownstreamPathTemplate":"/","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,"ExceptionsAllowedBeforeBreaking":3,"DurationOfBreak":5,"TimeoutValue":5000}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Address":null}}} {"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, ,"ExceptionsAllowedBeforeBreaking":3,"DurationOfBreak":5,"TimeoutValue":5000}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0}}}

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"buildOptions": { "buildOptions": {
"copyToOutput": { "copyToOutput": {
@ -22,17 +22,18 @@
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
"Microsoft.DotNet.InternalAbstractions": "1.0.0", "Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"xunit": "2.2.0-beta2-build3300", "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029", "dotnet-test-xunit": "2.2.0-preview2-build1029",
"Ocelot.ManualTest": "1.0.0-*", "Ocelot.ManualTest": "0.0.0-dev",
"Microsoft.AspNetCore.TestHost": "1.1.0", "Microsoft.AspNetCore.TestHost": "1.1.0",
"IdentityServer4": "1.0.1", "IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Mvc": "1.1.0", "Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0", "Microsoft.NETCore.App": "1.1.0",
"Shouldly": "2.8.2", "Shouldly": "2.8.2",
"TestStack.BDDfy": "4.3.2" "TestStack.BDDfy": "4.3.2",
"Consul": "0.7.2.1"
}, },
"runtimes": { "runtimes": {
"win10-x64": {}, "win10-x64": {},
@ -40,7 +41,7 @@
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {
"netcoreapp1.4": { "netcoreapp1.1": {
"imports": [ "imports": [
] ]
} }

View File

@ -1,12 +1,12 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"buildOptions": { "buildOptions": {
"emitEntryPoint": true "emitEntryPoint": true
}, },
"dependencies": { "dependencies": {
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"BenchmarkDotNet": "0.10.1" "BenchmarkDotNet": "0.10.2"
}, },
"runtimes": { "runtimes": {
"win10-x64": {}, "win10-x64": {},
@ -14,7 +14,7 @@
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {
"netcoreapp1.4": { "netcoreapp1.1": {
"imports": [ "imports": [
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
@ -10,7 +10,7 @@
"Microsoft.Extensions.Logging.Console": "1.1.0", "Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0", "Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0" "Microsoft.NETCore.App": "1.1.0"
}, },
@ -24,7 +24,7 @@
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {
"netcoreapp1.4": { "netcoreapp1.1": {
"imports": [ "imports": [
] ]
} }

View File

@ -8,6 +8,7 @@ using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator; using Ocelot.Configuration.Validator;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses; using Ocelot.Responses;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
@ -24,6 +25,9 @@ namespace Ocelot.UnitTests.Configuration
private readonly Mock<IClaimToThingConfigurationParser> _configParser; private readonly Mock<IClaimToThingConfigurationParser> _configParser;
private readonly Mock<ILogger<FileOcelotConfigurationCreator>> _logger; private readonly Mock<ILogger<FileOcelotConfigurationCreator>> _logger;
private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator; private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator;
private readonly Mock<ILoadBalancerFactory> _loadBalancerFactory;
private readonly Mock<ILoadBalancerHouse> _loadBalancerHouse;
private readonly Mock<ILoadBalancer> _loadBalancer;
public FileConfigurationCreatorTests() public FileConfigurationCreatorTests()
{ {
@ -31,8 +35,37 @@ namespace Ocelot.UnitTests.Configuration
_configParser = new Mock<IClaimToThingConfigurationParser>(); _configParser = new Mock<IClaimToThingConfigurationParser>();
_validator = new Mock<IConfigurationValidator>(); _validator = new Mock<IConfigurationValidator>();
_fileConfig = new Mock<IOptions<FileConfiguration>>(); _fileConfig = new Mock<IOptions<FileConfiguration>>();
_loadBalancerFactory = new Mock<ILoadBalancerFactory>();
_loadBalancerHouse = new Mock<ILoadBalancerHouse>();
_loadBalancer = new Mock<ILoadBalancer>();
_ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _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<FileReRoute>
{
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] [Fact]
@ -66,6 +99,7 @@ namespace Ocelot.UnitTests.Configuration
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_use_downstream_scheme() public void should_use_downstream_scheme()
{ {
this.Given(x => x.GivenTheConfigIs(new FileConfiguration this.Given(x => x.GivenTheConfigIs(new FileConfiguration
@ -117,7 +151,7 @@ namespace Ocelot.UnitTests.Configuration
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
{ {
Provider = "consul", Provider = "consul",
Address = "127.0.0.1" Host = "127.0.0.1"
} }
} }
})) }))
@ -376,6 +410,7 @@ namespace Ocelot.UnitTests.Configuration
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => x.GivenTheConfigHeaderExtractorReturns(new ClaimToThing("CustomerId", "CustomerId", "", 0))) .And(x => x.GivenTheConfigHeaderExtractorReturns(new ClaimToThing("CustomerId", "CustomerId", "", 0)))
.And(x => x.GivenTheLoadBalancerFactoryReturns())
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(expected)) .Then(x => x.ThenTheReRoutesAre(expected))
.And(x => x.ThenTheAuthenticationOptionsAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected))
@ -430,6 +465,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => x.GivenTheLoadBalancerFactoryReturns())
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(expected)) .Then(x => x.ThenTheReRoutesAre(expected))
.And(x => x.ThenTheAuthenticationOptionsAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected))
@ -543,7 +579,7 @@ namespace Ocelot.UnitTests.Configuration
private void WhenICreateTheConfig() private void WhenICreateTheConfig()
{ {
_config = _ocelotConfigurationCreator.Create(); _config = _ocelotConfigurationCreator.Create().Result;
} }
private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes) private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes)
@ -576,5 +612,24 @@ namespace Ocelot.UnitTests.Configuration
} }
} }
private void GivenTheLoadBalancerFactoryReturns()
{
_loadBalancerFactory
.Setup(x => x.Get(It.IsAny<ReRoute>()))
.ReturnsAsync(_loadBalancer.Object);
}
private void TheLoadBalancerFactoryIsCalledCorrectly()
{
_loadBalancerFactory
.Verify(x => x.Get(It.IsAny<ReRoute>()), Times.Once);
}
private void ThenTheLoadBalancerHouseIsCalledCorrectly()
{
_loadBalancerHouse
.Verify(x => x.Add(It.IsAny<string>(), _loadBalancer.Object), Times.Once);
}
} }
} }

View File

@ -81,7 +81,7 @@ namespace Ocelot.UnitTests.Configuration
{ {
_creator _creator
.Setup(x => x.Create()) .Setup(x => x.Create())
.Returns(config); .ReturnsAsync(config);
} }
private void GivenTheRepoReturns(Response<IOcelotConfiguration> config) private void GivenTheRepoReturns(Response<IOcelotConfiguration> config)
@ -93,7 +93,7 @@ namespace Ocelot.UnitTests.Configuration
private void WhenIGetTheConfig() private void WhenIGetTheConfig()
{ {
_result = _ocelotConfigurationProvider.Get(); _result = _ocelotConfigurationProvider.Get().Result;
} }
private void TheFollowingIsReturned(Response<IOcelotConfiguration> expected) private void TheFollowingIsReturned(Response<IOcelotConfiguration> expected)

View File

@ -84,7 +84,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute); _downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
_downstreamRouteFinder _downstreamRouteFinder
.Setup(x => x.FindDownstreamRoute(It.IsAny<string>(), It.IsAny<string>())) .Setup(x => x.FindDownstreamRoute(It.IsAny<string>(), It.IsAny<string>()))
.Returns(_downstreamRoute); .ReturnsAsync(_downstreamRoute);
} }
public void Dispose() public void Dispose()

View File

@ -159,7 +159,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder
_reRoutesConfig = reRoutesConfig; _reRoutesConfig = reRoutesConfig;
_mockConfig _mockConfig
.Setup(x => x.Get()) .Setup(x => x.Get())
.Returns(new OkResponse<IOcelotConfiguration>(new OcelotConfiguration(_reRoutesConfig))); .ReturnsAsync(new OkResponse<IOcelotConfiguration>(new OcelotConfiguration(_reRoutesConfig)));
} }
private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath)
@ -169,7 +169,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder
private void WhenICallTheFinder() private void WhenICallTheFinder()
{ {
_result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath, _upstreamHttpMethod); _result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath, _upstreamHttpMethod).Result;
} }
private void ThenTheFollowingIsReturned(DownstreamRoute expected) private void ThenTheFollowingIsReturned(DownstreamRoute expected)

View File

@ -36,6 +36,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator
private HttpResponseMessage _result; private HttpResponseMessage _result;
private OkResponse<DownstreamPath> _downstreamPath; private OkResponse<DownstreamPath> _downstreamPath;
private OkResponse<DownstreamUrl> _downstreamUrl; private OkResponse<DownstreamUrl> _downstreamUrl;
private HostAndPort _hostAndPort;
public DownstreamUrlCreatorMiddlewareTests() public DownstreamUrlCreatorMiddlewareTests()
{ {
@ -69,14 +70,25 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator
[Fact] [Fact]
public void should_call_dependencies_correctly() public void should_call_dependencies_correctly()
{ {
var hostAndPort = new HostAndPort("127.0.0.1", 80);
this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List<UrlPathPlaceholderNameAndValue>(), new ReRouteBuilder().WithDownstreamPathTemplate("any old string").Build()))) this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List<UrlPathPlaceholderNameAndValue>(), new ReRouteBuilder().WithDownstreamPathTemplate("any old string").Build())))
.And(x => x.GivenTheHostAndPortIs(hostAndPort))
.And(x => x.TheUrlReplacerReturns("/api/products/1")) .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()) .When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly())
.BDDfy(); .BDDfy();
} }
private void GivenTheHostAndPortIs(HostAndPort hostAndPort)
{
_hostAndPort = hostAndPort;
_scopedRepository
.Setup(x => x.Get<HostAndPort>("HostAndPort"))
.Returns(new OkResponse<HostAndPort>(_hostAndPort));
}
private void TheUrlBuilderReturns(string dsUrl) private void TheUrlBuilderReturns(string dsUrl)
{ {
_downstreamUrl = new OkResponse<DownstreamUrl>(new DownstreamUrl(dsUrl)); _downstreamUrl = new OkResponse<DownstreamUrl>(new DownstreamUrl(dsUrl));

View File

@ -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<HostAndPort> _result;
private LeastConnectionLoadBalancer _leastConnection;
private List<Service> _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<Service>
{
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<Service>
{
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<Service>
{
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<Service>
{
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<Service>
{
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<Service>(), serviceName))
.When(x => x.WhenIGetTheNextHostAndPort())
.Then(x => x.ThenServiceAreEmptyErrorIsReturned())
.BDDfy();
}
private void ThenServiceAreNullErrorIsReturned()
{
_result.IsError.ShouldBeTrue();
_result.Errors[0].ShouldBeOfType<ServicesAreNullError>();
}
private void ThenServiceAreEmptyErrorIsReturned()
{
_result.IsError.ShouldBeTrue();
_result.Errors[0].ShouldBeOfType<ServicesAreEmptyError>();
}
private void GivenTheLoadBalancerStarts(List<Service> services, string serviceName)
{
_services = services;
_leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName);
}
private void WhenTheLoadBalancerStarts(List<Service> 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);
}
}
}

View File

@ -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<IServiceDiscoveryProviderFactory> _serviceProviderFactory;
private Mock<IServiceDiscoveryProvider> _serviceProvider;
public LoadBalancerFactoryTests()
{
_serviceProviderFactory = new Mock<IServiceDiscoveryProviderFactory>();
_serviceProvider = new Mock<IServiceDiscoveryProvider>();
_factory = new LoadBalancerFactory(_serviceProviderFactory.Object);
}
private void GivenTheServiceProviderFactoryReturns()
{
_serviceProviderFactory
.Setup(x => x.Get(It.IsAny<ServiceProviderConfiguraion>()))
.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<NoLoadBalancer>())
.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<RoundRobinLoadBalancer>())
.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<LeastConnectionLoadBalancer>())
.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<ServiceProviderConfiguraion>()), Times.Once);
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheLoadBalancer()
{
_result = _factory.Get(_reRoute).Result;
}
private void ThenTheLoadBalancerIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
}

View File

@ -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<ILoadBalancer> _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<FakeLoadBalancer>())
.When(x => x.WhenWeGetTheLoadBalancer(keyTwo))
.Then(x => x.ThenTheLoadBalancerIs<FakeRoundRobinLoadBalancer>())
.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<UnableToFindLoadBalancerError>();
}
private void ThenTheLoadBalancerIs<T>()
{
_getResult.Data.ShouldBeOfType<T>();
}
private void ThenItIsAdded()
{
_addResult.IsError.ShouldBe(false);
_addResult.ShouldBeOfType<OkResponse>();
}
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<Response<HostAndPort>> Lease()
{
throw new NotImplementedException();
}
public void Release(HostAndPort hostAndPort)
{
throw new NotImplementedException();
}
}
class FakeRoundRobinLoadBalancer : ILoadBalancer
{
public Task<Response<HostAndPort>> Lease()
{
throw new NotImplementedException();
}
public void Release(HostAndPort hostAndPort)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -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<ILoadBalancerHouse> _loadBalancerHouse;
private readonly Mock<IRequestScopedDataRepository> _scopedRepository;
private readonly Mock<ILoadBalancer> _loadBalancer;
private readonly string _url;
private readonly TestServer _server;
private readonly HttpClient _client;
private HttpResponseMessage _result;
private HostAndPort _hostAndPort;
private OkResponse<Ocelot.Request.Request> _request;
private OkResponse<string> _downstreamUrl;
private OkResponse<DownstreamRoute> _downstreamRoute;
private ErrorResponse<ILoadBalancer> _getLoadBalancerHouseError;
private ErrorResponse<HostAndPort> _getHostAndPortError;
public LoadBalancerMiddlewareTests()
{
_url = "http://localhost:51879";
_loadBalancerHouse = new Mock<ILoadBalancerHouse>();
_scopedRepository = new Mock<IRequestScopedDataRepository>();
_loadBalancer = new Mock<ILoadBalancer>();
_loadBalancerHouse = new Mock<ILoadBalancerHouse>();
var builder = new WebHostBuilder()
.ConfigureServices(x =>
{
x.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
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<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
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<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
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<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
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<HostAndPort>(new List<Error>() { 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>(_hostAndPort));
}
private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
{
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
_scopedRepository
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
.Returns(_downstreamRoute);
}
private void GivenTheLoadBalancerHouseReturns()
{
_loadBalancerHouse
.Setup(x => x.Get(It.IsAny<string>()))
.Returns(new OkResponse<ILoadBalancer>(_loadBalancer.Object));
}
private void GivenTheLoadBalancerHouseReturnsAnError()
{
_getLoadBalancerHouseError = new ErrorResponse<ILoadBalancer>(new List<Ocelot.Errors.Error>()
{
new UnableToFindLoadBalancerError($"unabe to find load balancer for bah")
});
_loadBalancerHouse
.Setup(x => x.Get(It.IsAny<string>()))
.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<List<Error>>()), 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<string>(downstreamUrl);
_scopedRepository
.Setup(x => x.Get<string>(It.IsAny<string>()))
.Returns(_downstreamUrl);
}
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
}
}

View File

@ -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<Service> _services;
private NoLoadBalancer _loadBalancer;
private Response<HostAndPort> _result;
[Fact]
public void should_return_host_and_port()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
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<Service> services)
{
_services = services;
}
private void WhenIGetTheNextHostAndPort()
{
_loadBalancer = new NoLoadBalancer(_services);
_result = _loadBalancer.Lease().Result;
}
private void ThenTheHostAndPortIs(HostAndPort expected)
{
_result.Data.ShouldBe(expected);
}
}
}

View File

@ -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<Service> _services;
private Response<HostAndPort> _hostAndPort;
public RoundRobinTests()
{
_services = new List<Service>
{
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);
}
}
}

View File

@ -62,19 +62,11 @@ namespace Ocelot.UnitTests.Responder
{ {
this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage()))
.And(x => x.GivenThereAreNoPipelineErrors()) .And(x => x.GivenThereAreNoPipelineErrors())
.And(x => x.GivenTheResponderReturns())
.When(x => x.WhenICallTheMiddleware()) .When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenThereAreNoErrors()) .Then(x => x.ThenThereAreNoErrors())
.BDDfy(); .BDDfy();
} }
private void GivenTheResponderReturns()
{
_responder
.Setup(x => x.SetResponseOnHttpContext(It.IsAny<HttpContext>(), It.IsAny<HttpResponseMessage>()))
.ReturnsAsync(new OkResponse());
}
private void GivenThereAreNoPipelineErrors() private void GivenThereAreNoPipelineErrors()
{ {
_scopedRepository _scopedRepository

View File

@ -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<Service> _result;
private List<Service> _expected;
[Fact]
public void should_return_services()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
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<Service> services)
{
_expected = services;
}
private void WhenIGetTheService()
{
_serviceProvider = new ConfigurationServiceProvider(_expected);
_result = _serviceProvider.Get().Result;
}
private void ThenTheFollowingIsReturned(List<Service> 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);
}
}
}

View File

@ -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<ConfigurationServiceProvider>())
.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<ConsulServiceDiscoveryProvider>())
.BDDfy();
}
private void GivenTheReRoute(ServiceProviderConfiguraion serviceConfig)
{
_serviceConfig = serviceConfig;
}
private void WhenIGetTheServiceProvider()
{
_result = _factory.Get(_serviceConfig);
}
private void ThenTheServiceProviderIs<T>()
{
_result.ShouldBeOfType<T>();
}
}
}

View File

@ -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<Service> _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<Service> 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<Service> Lookup(string name)
{
return _repository.Get(name);
}
}
public interface IServiceRepository
{
List<Service> Get(string serviceName);
void Set(Service serviceNameAndAddress);
}
public class ServiceRepository : IServiceRepository
{
private Dictionary<string, List<Service>> _registeredServices;
public ServiceRepository()
{
_registeredServices = new Dictionary<string, List<Service>>();
}
public List<Service> Get(string serviceName)
{
return _registeredServices[serviceName];
}
public void Set(Service serviceNameAndAddress)
{
List<Service> services;
if(_registeredServices.TryGetValue(serviceNameAndAddress.Name, out services))
{
services.Add(serviceNameAndAddress);
_registeredServices[serviceNameAndAddress.Name] = services;
}
else
{
_registeredServices[serviceNameAndAddress.Name] = new List<Service>(){ serviceNameAndAddress };
}
}
}
}

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"testRunner": "xunit", "testRunner": "xunit",
@ -13,7 +13,7 @@
"Microsoft.Extensions.Logging.Debug": "1.1.0", "Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"xunit": "2.2.0-beta2-build3300", "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029", "dotnet-test-xunit": "2.2.0-preview2-build1029",
"Moq": "4.6.38-alpha", "Moq": "4.6.38-alpha",
@ -32,7 +32,7 @@
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {
"netcoreapp1.4": { "netcoreapp1.1": {
"imports": [ "imports": [
] ]
} }

1
version.ps1 Normal file
View File

@ -0,0 +1 @@
.\tools\GitVersion.CommandLine\tools\GitVersion.exe