mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 18:22:49 +08:00
Feat/monorepo (#734)
* copied everything from repos back to ocelot repo * added src projects to sln * removed all test projects that have no tests * added all test projects to sln * removed test not on master * merged unit tests * merged acceptance tests * merged integration tests * fixed namepaces * build script creates packages for all projects * updated docs to make sure no references to external repos that we will remove * +semver: breaking
This commit is contained in:
parent
35253025c7
commit
11a2d13f18
55
Ocelot.sln
55
Ocelot.sln
@ -7,12 +7,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9D
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.dockerignore = .dockerignore
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
build-and-release-unstable.ps1 = build-and-release-unstable.ps1
|
build-and-release-unstable.ps1 = build-and-release-unstable.ps1
|
||||||
build-and-run-tests.ps1 = build-and-run-tests.ps1
|
build-and-run-tests.ps1 = build-and-run-tests.ps1
|
||||||
build.cake = build.cake
|
build.cake = build.cake
|
||||||
build.ps1 = build.ps1
|
build.ps1 = build.ps1
|
||||||
codeanalysis.ruleset = codeanalysis.ruleset
|
codeanalysis.ruleset = codeanalysis.ruleset
|
||||||
|
docker-compose.yaml = docker-compose.yaml
|
||||||
|
Dockerfile = Dockerfile
|
||||||
GitVersion.yml = GitVersion.yml
|
GitVersion.yml = GitVersion.yml
|
||||||
global.json = global.json
|
global.json = global.json
|
||||||
LICENSE.md = LICENSE.md
|
LICENSE.md = LICENSE.md
|
||||||
@ -23,9 +26,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
run-benchmarks.ps1 = run-benchmarks.ps1
|
run-benchmarks.ps1 = run-benchmarks.ps1
|
||||||
run-unit-tests.ps1 = run-unit-tests.ps1
|
run-unit-tests.ps1 = run-unit-tests.ps1
|
||||||
version.ps1 = version.ps1
|
version.ps1 = version.ps1
|
||||||
Dockerfile = Dockerfile
|
|
||||||
.dockerignore = .dockerignore
|
|
||||||
docker-compose.yaml = docker-compose.yaml
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}"
|
||||||
@ -42,6 +42,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\O
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj", "{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Rafty", "src\Ocelot.Provider.Rafty\Ocelot.Provider.Rafty.csproj", "{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -72,6 +86,34 @@ Global
|
|||||||
{D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -83,6 +125,13 @@ Global
|
|||||||
{02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
{02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
||||||
{106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
{106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
||||||
{D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
{D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B}
|
||||||
|
{F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{AC153C67-EF18-47E6-A230-F0D3CF5F0A98} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
|
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48}
|
SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48}
|
||||||
|
@ -96,6 +96,3 @@ If you think this project is worth supporting financially please make a contribu
|
|||||||
## Things that are currently annoying me
|
## Things that are currently annoying me
|
||||||
|
|
||||||
[ Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
|
[ Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
35
build.cake
35
build.cake
@ -17,7 +17,7 @@ var artifactsDir = Directory("artifacts");
|
|||||||
// unit testing
|
// unit testing
|
||||||
var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests");
|
var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests");
|
||||||
var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj";
|
var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj";
|
||||||
var minCodeCoverage = 82d;
|
var minCodeCoverage = 80d;
|
||||||
var coverallsRepoToken = "coveralls-repo-token-ocelot";
|
var coverallsRepoToken = "coveralls-repo-token-ocelot";
|
||||||
var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot";
|
var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot";
|
||||||
|
|
||||||
@ -263,14 +263,31 @@ Task("CreatePackages")
|
|||||||
.Does(() =>
|
.Does(() =>
|
||||||
{
|
{
|
||||||
EnsureDirectoryExists(packagesDir);
|
EnsureDirectoryExists(packagesDir);
|
||||||
CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir);
|
|
||||||
|
CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir);
|
||||||
|
|
||||||
//GenerateReleaseNotes(releaseNotesFile);
|
//GenerateReleaseNotes(releaseNotesFile);
|
||||||
|
|
||||||
System.IO.File.WriteAllLines(artifactsFile, new[]{
|
var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg");
|
||||||
"nuget:Ocelot." + buildVersion + ".nupkg",
|
|
||||||
|
foreach(var projectFile in projectFiles)
|
||||||
|
{
|
||||||
|
System.IO.File.AppendAllLines(artifactsFile, new[]{
|
||||||
|
projectFile.GetFilename().FullPath,
|
||||||
//"releaseNotes:releasenotes.md"
|
//"releaseNotes:releasenotes.md"
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var artifacts = System.IO.File
|
||||||
|
.ReadAllLines(artifactsFile)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
|
foreach(var artifact in artifacts)
|
||||||
|
{
|
||||||
|
var codePackage = packagesDir + File(artifact);
|
||||||
|
|
||||||
|
Information("Created package " + codePackage);
|
||||||
|
}
|
||||||
|
|
||||||
if (AppVeyor.IsRunningOnAppVeyor)
|
if (AppVeyor.IsRunningOnAppVeyor)
|
||||||
{
|
{
|
||||||
@ -345,8 +362,6 @@ Task("DownloadGitHubReleaseArtifacts")
|
|||||||
|
|
||||||
Information("Release url " + releaseUrl);
|
Information("Release url " + releaseUrl);
|
||||||
|
|
||||||
//var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl));
|
|
||||||
|
|
||||||
var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl))
|
var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl))
|
||||||
.GetValue("assets_url")
|
.GetValue("assets_url")
|
||||||
.Value<string>();
|
.Value<string>();
|
||||||
@ -451,10 +466,11 @@ private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFi
|
|||||||
{
|
{
|
||||||
var artifacts = System.IO.File
|
var artifacts = System.IO.File
|
||||||
.ReadAllLines(artifactsFile)
|
.ReadAllLines(artifactsFile)
|
||||||
.Select(l => l.Split(':'))
|
.Distinct();
|
||||||
.ToDictionary(v => v[0], v => v[1]);
|
|
||||||
|
|
||||||
var codePackage = packagesDir + File(artifacts["nuget"]);
|
foreach(var artifact in artifacts)
|
||||||
|
{
|
||||||
|
var codePackage = packagesDir + File(artifact);
|
||||||
|
|
||||||
Information("Pushing package " + codePackage);
|
Information("Pushing package " + codePackage);
|
||||||
|
|
||||||
@ -466,6 +482,7 @@ private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFi
|
|||||||
ApiKey = feedApiKey,
|
ApiKey = feedApiKey,
|
||||||
Source = codeFeedUrl
|
Source = codeFeedUrl
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// gets the resource from the specified url
|
/// gets the resource from the specified url
|
||||||
|
25
build.ps1
25
build.ps1
@ -25,10 +25,6 @@ Specifies the amount of information to be displayed.
|
|||||||
Shows description about tasks.
|
Shows description about tasks.
|
||||||
.PARAMETER DryRun
|
.PARAMETER DryRun
|
||||||
Performs a dry run.
|
Performs a dry run.
|
||||||
.PARAMETER Experimental
|
|
||||||
Uses the nightly builds of the Roslyn script engine.
|
|
||||||
.PARAMETER Mono
|
|
||||||
Uses the Mono Compiler rather than the Roslyn script engine.
|
|
||||||
.PARAMETER SkipToolPackageRestore
|
.PARAMETER SkipToolPackageRestore
|
||||||
Skips restoring of packages.
|
Skips restoring of packages.
|
||||||
.PARAMETER ScriptArgs
|
.PARAMETER ScriptArgs
|
||||||
@ -49,13 +45,25 @@ Param(
|
|||||||
[switch]$ShowDescription,
|
[switch]$ShowDescription,
|
||||||
[Alias("WhatIf", "Noop")]
|
[Alias("WhatIf", "Noop")]
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Experimental,
|
|
||||||
[switch]$Mono,
|
|
||||||
[switch]$SkipToolPackageRestore,
|
[switch]$SkipToolPackageRestore,
|
||||||
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
|
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
|
||||||
[string[]]$ScriptArgs
|
[string[]]$ScriptArgs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Attempt to set highest encryption available for SecurityProtocol.
|
||||||
|
# PowerShell will not set this by default (until maybe .NET 4.6.x). This
|
||||||
|
# will typically produce a message for PowerShell v2 (just an info
|
||||||
|
# message though)
|
||||||
|
try {
|
||||||
|
# Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48)
|
||||||
|
# Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't
|
||||||
|
# exist in .NET 4.0, even though they are addressable if .NET 4.5+ is
|
||||||
|
# installed (.NET 4.5 is an in-place upgrade).
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48
|
||||||
|
} catch {
|
||||||
|
Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3'
|
||||||
|
}
|
||||||
|
|
||||||
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
|
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
|
||||||
function MD5HashFile([string] $filePath)
|
function MD5HashFile([string] $filePath)
|
||||||
{
|
{
|
||||||
@ -118,7 +126,8 @@ if (!(Test-Path $PACKAGES_CONFIG)) {
|
|||||||
Write-Verbose -Message "Downloading packages.config..."
|
Write-Verbose -Message "Downloading packages.config..."
|
||||||
try {
|
try {
|
||||||
$wc = GetProxyEnabledWebClient
|
$wc = GetProxyEnabledWebClient
|
||||||
$wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch {
|
$wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG)
|
||||||
|
} catch {
|
||||||
Throw "Could not download packages.config."
|
Throw "Could not download packages.config."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,8 +234,6 @@ if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
|
|||||||
if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
|
if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
|
||||||
if ($ShowDescription) { $cakeArguments += "-showdescription" }
|
if ($ShowDescription) { $cakeArguments += "-showdescription" }
|
||||||
if ($DryRun) { $cakeArguments += "-dryrun" }
|
if ($DryRun) { $cakeArguments += "-dryrun" }
|
||||||
if ($Experimental) { $cakeArguments += "-experimental" }
|
|
||||||
if ($Mono) { $cakeArguments += "-mono" }
|
|
||||||
$cakeArguments += $ScriptArgs
|
$cakeArguments += $ScriptArgs
|
||||||
|
|
||||||
# Start Cake
|
# Start Cake
|
||||||
|
@ -10,7 +10,7 @@ This section defines the release process for the maintainers of the project.
|
|||||||
|
|
||||||
* 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.
|
* 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/TomPallister/Ocelot/releases>`_. Modify the release name and tag as desired.
|
* In Github, navigate to the `release <https://github.com/ThreeMammals/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.
|
* When you're ready, publish the release. This will tag the commit with the specified release number.
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ All you need to do to hook into your own IdentityServer is add the following to
|
|||||||
|
|
||||||
You now need to get a token from your IdentityServer and use in subsequent requests to Ocelot's administration API.
|
You now need to get a token from your IdentityServer and use in subsequent requests to Ocelot's administration API.
|
||||||
|
|
||||||
This feature was implemented for `issue 228 <https://github.com/TomPallister/Ocelot/issues/228>`_. It is useful because the IdentityServer authentication
|
This feature was implemented for `issue 228 <https://github.com/ThreeMammals/Ocelot/issues/228>`_. It is useful because the IdentityServer authentication
|
||||||
middleware needs the URL of the IdentityServer. If you are using the internal IdentityServer it might not alaways be possible to have the Ocelot URL.
|
middleware needs the URL of the IdentityServer. If you are using the internal IdentityServer it might not alaways be possible to have the Ocelot URL.
|
||||||
|
|
||||||
Internal IdentityServer
|
Internal IdentityServer
|
||||||
|
@ -32,7 +32,7 @@ Finally in order to use caching on a route in your ReRoute configuration add thi
|
|||||||
|
|
||||||
In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds.
|
In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds.
|
||||||
|
|
||||||
If you look at the example `here <https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/Program.cs>`_ you can see how the cache manager
|
If you look at the example `here <https://github.com/ThreeMammals/Ocelot/blob/develop/test/Ocelot.ManualTest/Program.cs>`_ you can see how the cache manager
|
||||||
is setup and then passed into the Ocelot AddCacheManager configuration method. You can use any settings supported by
|
is setup and then passed into the Ocelot AddCacheManager configuration method. You can use any settings supported by
|
||||||
the CacheManager package and just pass them in.
|
the CacheManager package and just pass them in.
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
Configuration
|
Configuration
|
||||||
============
|
============
|
||||||
|
|
||||||
An example configuration can be found `here <https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/ocelot.json>`_.
|
An example configuration can be found `here <https://github.com/ThreeMammals/Ocelot/blob/develop/test/Ocelot.ManualTest/ocelot.json>`_.
|
||||||
There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration.
|
There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration.
|
||||||
The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global
|
The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global
|
||||||
configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful
|
configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
Delegating Handlers
|
Delegating Handlers
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 <https://github.com/TomPallister/Ocelot/issues/208>`_
|
Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 <https://github.com/ThreeMammals/Ocelot/issues/208>`_
|
||||||
and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 <https://github.com/TomPallister/Ocelot/issues/264>`_.
|
and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 <https://github.com/ThreeMammals/Ocelot/issues/264>`_.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
^^^^^
|
^^^^^
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
Headers Transformation
|
Headers Transformation
|
||||||
======================
|
======================
|
||||||
|
|
||||||
Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 <https://github.com/TomPallister/Ocelot/issues/190>`_ and I decided that it was going to be useful in various ways.
|
Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 <https://github.com/ThreeMammals/Ocelot/issues/190>`_ and I decided that it was going to be useful in various ways.
|
||||||
|
|
||||||
Add to Request
|
Add to Request
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
@ -23,7 +23,7 @@ Placeholders are supported too (see below).
|
|||||||
Add to Response
|
Add to Response
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This feature was requested in `GitHub #280 <https://github.com/TomPallister/Ocelot/issues/280>`_.
|
This feature was requested in `GitHub #280 <https://github.com/ThreeMammals/Ocelot/issues/280>`_.
|
||||||
|
|
||||||
If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json..
|
If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json..
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION)
|
Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION)
|
||||||
============================================
|
============================================
|
||||||
|
|
||||||
Ocelot has recently integrated `Rafty <https://github.com/TomPallister/Rafty>`_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK.
|
Ocelot has recently integrated `Rafty <https://github.com/ThreeMammals/Rafty>`_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK.
|
||||||
|
|
||||||
Raft is a distributed concensus algorythm that allows a cluster of servers (Ocelots) to maintain local state without having a centralised database for storing state (e.g. SQL Server).
|
Raft is a distributed concensus algorythm that allows a cluster of servers (Ocelots) to maintain local state without having a centralised database for storing state (e.g. SQL Server).
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ Ocelot allows you to specify Aggregate ReRoutes that compose multiple normal ReR
|
|||||||
a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type
|
a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type
|
||||||
architecture with Ocelot.
|
architecture with Ocelot.
|
||||||
|
|
||||||
This feature was requested as part of `Issue 79 <https://github.com/TomPallister/Ocelot/pull/79>`_ and further improvements were made as part of `Issue 298 <https://github.com/TomPallister/Ocelot/issue/298>`_.
|
This feature was requested as part of `Issue 79 <https://github.com/ThreeMammals/Ocelot/pull/79>`_ and further improvements were made as part of `Issue 298 <https://github.com/ThreeMammals/Ocelot/issue/298>`_.
|
||||||
|
|
||||||
In order to set this up you must do something like the following in your ocelot.json. Here we have specified two normal ReRoutes and each one has a Key property.
|
In order to set this up you must do something like the following in your ocelot.json. Here we have specified two normal ReRoutes and each one has a Key property.
|
||||||
We then specify an Aggregate that composes the two ReRoutes using their keys in the ReRouteKeys list and says then we have the UpstreamPathTemplate which works like a normal ReRoute.
|
We then specify an Aggregate that composes the two ReRoutes using their keys in the ReRouteKeys list and says then we have the UpstreamPathTemplate which works like a normal ReRoute.
|
||||||
|
@ -140,13 +140,13 @@ The ReRoute above will only be matched when the host header value is somedomain.
|
|||||||
|
|
||||||
If you do not set UpstreamHost on a ReRoute then any host header will match it. This means that if you have two ReRoutes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set.
|
If you do not set UpstreamHost on a ReRoute then any host header will match it. This means that if you have two ReRoutes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set.
|
||||||
|
|
||||||
This feature was requested as part of `Issue 216 <https://github.com/TomPallister/Ocelot/pull/216>`_ .
|
This feature was requested as part of `Issue 216 <https://github.com/ThreeMammals/Ocelot/pull/216>`_ .
|
||||||
|
|
||||||
Priority
|
Priority
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
|
|
||||||
You can define the order you want your ReRoutes to match the Upstream HttpRequest by including a "Priority" property in ocelot.json
|
You can define the order you want your ReRoutes to match the Upstream HttpRequest by including a "Priority" property in ocelot.json
|
||||||
See `Issue 270 <https://github.com/TomPallister/Ocelot/pull/270>`_ for reference
|
See `Issue 270 <https://github.com/ThreeMammals/Ocelot/pull/270>`_ for reference
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ matched /goods/{catchAll} (because this is the first ReRoute in the list!).
|
|||||||
Dynamic Routing
|
Dynamic Routing
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This feature was requested in `issue 340 <https://github.com/TomPallister/Ocelot/issue/340>`_.
|
This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issue/340>`_.
|
||||||
|
|
||||||
The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the ReRoute config. See the docs :ref:`service-discovery` if
|
The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the ReRoute config. See the docs :ref:`service-discovery` if
|
||||||
this sounds interesting to you.
|
this sounds interesting to you.
|
||||||
|
@ -113,7 +113,7 @@ Ocelot will add this token to the consul client that it uses to make requests an
|
|||||||
Eureka
|
Eureka
|
||||||
^^^^^^
|
^^^^^^
|
||||||
|
|
||||||
This feature was requested as part of `Issue 262 <https://github.com/TomPallister/Ocelot/issue/262>`_ . to add support for Netflix's
|
This feature was requested as part of `Issue 262 <https://github.com/ThreeMammals/Ocelot/issue/262>`_ . to add support for Netflix's
|
||||||
Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something
|
Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something
|
||||||
to do with `Pivotal <https://pivotal.io/platform>`_! Anyway enough of the background.
|
to do with `Pivotal <https://pivotal.io/platform>`_! Anyway enough of the background.
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them
|
|||||||
Dynamic Routing
|
Dynamic Routing
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This feature was requested in `issue 340 <https://github.com/TomPallister/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider.
|
This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider.
|
||||||
|
|
||||||
An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of
|
An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of
|
||||||
the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal.
|
the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal.
|
||||||
|
14
src/Ocelot.Administration/IIdentityServerConfiguration.cs
Normal file
14
src/Ocelot.Administration/IIdentityServerConfiguration.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Ocelot.Administration
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public interface IIdentityServerConfiguration
|
||||||
|
{
|
||||||
|
string ApiName { get; }
|
||||||
|
string ApiSecret { get; }
|
||||||
|
bool RequireHttps { get; }
|
||||||
|
List<string> AllowedScopes { get; }
|
||||||
|
string CredentialsSigningCertificateLocation { get; }
|
||||||
|
string CredentialsSigningCertificatePassword { get; }
|
||||||
|
}
|
||||||
|
}
|
30
src/Ocelot.Administration/IdentityServerConfiguration.cs
Normal file
30
src/Ocelot.Administration/IdentityServerConfiguration.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
namespace Ocelot.Administration
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class IdentityServerConfiguration : IIdentityServerConfiguration
|
||||||
|
{
|
||||||
|
public IdentityServerConfiguration(
|
||||||
|
string apiName,
|
||||||
|
bool requireHttps,
|
||||||
|
string apiSecret,
|
||||||
|
List<string> allowedScopes,
|
||||||
|
string credentialsSigningCertificateLocation,
|
||||||
|
string credentialsSigningCertificatePassword)
|
||||||
|
{
|
||||||
|
ApiName = apiName;
|
||||||
|
RequireHttps = requireHttps;
|
||||||
|
ApiSecret = apiSecret;
|
||||||
|
AllowedScopes = allowedScopes;
|
||||||
|
CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation;
|
||||||
|
CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ApiName { get; }
|
||||||
|
public bool RequireHttps { get; }
|
||||||
|
public List<string> AllowedScopes { get; }
|
||||||
|
public string ApiSecret { get; }
|
||||||
|
public string CredentialsSigningCertificateLocation { get; }
|
||||||
|
public string CredentialsSigningCertificatePassword { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ocelot.Administration
|
||||||
|
{
|
||||||
|
public static class IdentityServerConfigurationCreator
|
||||||
|
{
|
||||||
|
public static IdentityServerConfiguration GetIdentityServerConfiguration(string secret)
|
||||||
|
{
|
||||||
|
var credentialsSigningCertificateLocation = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE");
|
||||||
|
var credentialsSigningCertificatePassword = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD");
|
||||||
|
|
||||||
|
return new IdentityServerConfiguration(
|
||||||
|
"admin",
|
||||||
|
false,
|
||||||
|
secret,
|
||||||
|
new List<string> { "admin", "openid", "offline_access" },
|
||||||
|
credentialsSigningCertificateLocation,
|
||||||
|
credentialsSigningCertificatePassword
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
namespace Ocelot.Administration
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using Configuration.Repository;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
|
||||||
|
public static class IdentityServerMiddlewareConfigurationProvider
|
||||||
|
{
|
||||||
|
public static OcelotMiddlewareConfigurationDelegate Get = builder =>
|
||||||
|
{
|
||||||
|
var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();
|
||||||
|
|
||||||
|
var config = internalConfigRepo.Get();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(config.Data.AdministrationPath))
|
||||||
|
{
|
||||||
|
builder.Map(config.Data.AdministrationPath, app =>
|
||||||
|
{
|
||||||
|
//todo - hack so we know that we are using internal identity server
|
||||||
|
var identityServerConfiguration = builder.ApplicationServices.GetService<IIdentityServerConfiguration>();
|
||||||
|
|
||||||
|
if (identityServerConfiguration != null)
|
||||||
|
{
|
||||||
|
app.UseIdentityServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseMvc();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
38
src/Ocelot.Administration/Ocelot.Administration.csproj
Normal file
38
src/Ocelot.Administration/Ocelot.Administration.csproj
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use the administration API and IdentityService dependencies that come with it</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Administration</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Administration</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Administration</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Administration</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Administration</PackageProjectUrl>
|
||||||
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
|
||||||
|
<PackageReference Include="IdentityServer4" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
128
src/Ocelot.Administration/OcelotBuilderExtensions.cs
Normal file
128
src/Ocelot.Administration/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
namespace Ocelot.Administration
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Configuration;
|
||||||
|
using Configuration.Creator;
|
||||||
|
using DependencyInjection;
|
||||||
|
using IdentityModel;
|
||||||
|
using IdentityServer4.AccessTokenValidation;
|
||||||
|
using IdentityServer4.Models;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, string secret)
|
||||||
|
{
|
||||||
|
var administrationPath = new AdministrationPath(path);
|
||||||
|
builder.Services.AddSingleton<OcelotMiddlewareConfigurationDelegate>(IdentityServerMiddlewareConfigurationProvider.Get);
|
||||||
|
|
||||||
|
//add identity server for admin area
|
||||||
|
var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret);
|
||||||
|
|
||||||
|
if (identityServerConfiguration != null)
|
||||||
|
{
|
||||||
|
AddIdentityServer(identityServerConfiguration, administrationPath, builder, builder.Configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IAdministrationPath>(administrationPath);
|
||||||
|
return new OcelotAdministrationBuilder(builder.Services, builder.Configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, Action<IdentityServerAuthenticationOptions> configureOptions)
|
||||||
|
{
|
||||||
|
var administrationPath = new AdministrationPath(path);
|
||||||
|
builder.Services.AddSingleton<OcelotMiddlewareConfigurationDelegate>(IdentityServerMiddlewareConfigurationProvider.Get);
|
||||||
|
|
||||||
|
if (configureOptions != null)
|
||||||
|
{
|
||||||
|
AddIdentityServer(configureOptions, builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IAdministrationPath>(administrationPath);
|
||||||
|
return new OcelotAdministrationBuilder(builder.Services, builder.Configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIdentityServer(Action<IdentityServerAuthenticationOptions> configOptions, IOcelotBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddIdentityServerAuthentication(configOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath, IOcelotBuilder builder, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
builder.Services.TryAddSingleton<IIdentityServerConfiguration>(identityServerConfiguration);
|
||||||
|
var identityServerBuilder = builder.Services
|
||||||
|
.AddIdentityServer(o => {
|
||||||
|
o.IssuerUri = "Ocelot";
|
||||||
|
})
|
||||||
|
.AddInMemoryApiResources(Resources(identityServerConfiguration))
|
||||||
|
.AddInMemoryClients(Client(identityServerConfiguration));
|
||||||
|
|
||||||
|
var urlFinder = new BaseUrlFinder(configuration);
|
||||||
|
var baseSchemeUrlAndPort = urlFinder.Find();
|
||||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddIdentityServerAuthentication(o =>
|
||||||
|
{
|
||||||
|
o.Authority = baseSchemeUrlAndPort + adminPath.Path;
|
||||||
|
o.ApiName = identityServerConfiguration.ApiName;
|
||||||
|
o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps;
|
||||||
|
o.SupportedTokens = SupportedTokens.Both;
|
||||||
|
o.ApiSecret = identityServerConfiguration.ApiSecret;
|
||||||
|
});
|
||||||
|
|
||||||
|
//todo - refactor naming..
|
||||||
|
if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword))
|
||||||
|
{
|
||||||
|
identityServerBuilder.AddDeveloperSigningCredential();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//todo - refactor so calls method?
|
||||||
|
var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword);
|
||||||
|
identityServerBuilder.AddSigningCredential(cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ApiResource> Resources(IIdentityServerConfiguration identityServerConfiguration)
|
||||||
|
{
|
||||||
|
return new List<ApiResource>
|
||||||
|
{
|
||||||
|
new ApiResource(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName)
|
||||||
|
{
|
||||||
|
ApiSecrets = new List<Secret>
|
||||||
|
{
|
||||||
|
new Secret
|
||||||
|
{
|
||||||
|
Value = identityServerConfiguration.ApiSecret.Sha256()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Client> Client(IIdentityServerConfiguration identityServerConfiguration)
|
||||||
|
{
|
||||||
|
return new List<Client>
|
||||||
|
{
|
||||||
|
new Client
|
||||||
|
{
|
||||||
|
ClientId = identityServerConfiguration.ApiName,
|
||||||
|
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||||
|
ClientSecrets = new List<Secret> {new Secret(identityServerConfiguration.ApiSecret.Sha256())},
|
||||||
|
AllowedScopes = { identityServerConfiguration.ApiName }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Administration/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Administration/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
@ -0,0 +1,38 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use CacheManager.Net</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Cache.CacheManager</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Cache.CacheManager</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Cache.CacheManager</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Cache.CacheManager</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Cache.CacheManager</PackageProjectUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="CacheManager.Core" Version="1.1.2" />
|
||||||
|
<PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.1.2" />
|
||||||
|
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
39
src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs
Normal file
39
src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
namespace Ocelot.Cache.CacheManager
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Configuration;
|
||||||
|
using Configuration.File;
|
||||||
|
using DependencyInjection;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotBuilder AddCacheManager(this IOcelotBuilder builder, Action<ConfigurationBuilderCachePart> settings)
|
||||||
|
{
|
||||||
|
var cacheManagerOutputCache = CacheFactory.Build<CachedResponse>("OcelotOutputCache", settings);
|
||||||
|
var ocelotOutputCacheManager = new OcelotCacheManagerCache<CachedResponse>(cacheManagerOutputCache);
|
||||||
|
|
||||||
|
builder.Services.RemoveAll(typeof(ICacheManager<CachedResponse>));
|
||||||
|
builder.Services.RemoveAll(typeof(IOcelotCache<CachedResponse>));
|
||||||
|
builder.Services.AddSingleton<ICacheManager<CachedResponse>>(cacheManagerOutputCache);
|
||||||
|
builder.Services.AddSingleton<IOcelotCache<CachedResponse>>(ocelotOutputCacheManager);
|
||||||
|
|
||||||
|
var ocelotConfigCacheManagerOutputCache = CacheFactory.Build<IInternalConfiguration>("OcelotConfigurationCache", settings);
|
||||||
|
var ocelotConfigCacheManager = new OcelotCacheManagerCache<IInternalConfiguration>(ocelotConfigCacheManagerOutputCache);
|
||||||
|
builder.Services.RemoveAll(typeof(ICacheManager<IInternalConfiguration>));
|
||||||
|
builder.Services.RemoveAll(typeof(IOcelotCache<IInternalConfiguration>));
|
||||||
|
builder.Services.AddSingleton<ICacheManager<IInternalConfiguration>>(ocelotConfigCacheManagerOutputCache);
|
||||||
|
builder.Services.AddSingleton<IOcelotCache<IInternalConfiguration>>(ocelotConfigCacheManager);
|
||||||
|
|
||||||
|
var fileConfigCacheManagerOutputCache = CacheFactory.Build<FileConfiguration>("FileConfigurationCache", settings);
|
||||||
|
var fileConfigCacheManager = new OcelotCacheManagerCache<FileConfiguration>(fileConfigCacheManagerOutputCache);
|
||||||
|
builder.Services.RemoveAll(typeof(ICacheManager<FileConfiguration>));
|
||||||
|
builder.Services.RemoveAll(typeof(IOcelotCache<FileConfiguration>));
|
||||||
|
builder.Services.AddSingleton<ICacheManager<FileConfiguration>>(fileConfigCacheManagerOutputCache);
|
||||||
|
builder.Services.AddSingleton<IOcelotCache<FileConfiguration>>(fileConfigCacheManager);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs
Normal file
42
src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
namespace Ocelot.Cache.CacheManager
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
|
||||||
|
public class OcelotCacheManagerCache<T> : IOcelotCache<T>
|
||||||
|
{
|
||||||
|
private readonly ICacheManager<T> _cacheManager;
|
||||||
|
|
||||||
|
public OcelotCacheManagerCache(ICacheManager<T> cacheManager)
|
||||||
|
{
|
||||||
|
_cacheManager = cacheManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string key, T value, TimeSpan ttl, string region)
|
||||||
|
{
|
||||||
|
_cacheManager.Add(new CacheItem<T>(key, region, value, ExpirationMode.Absolute, ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAndDelete(string key, T value, TimeSpan ttl, string region)
|
||||||
|
{
|
||||||
|
var exists = _cacheManager.Get(key);
|
||||||
|
|
||||||
|
if (exists != null)
|
||||||
|
{
|
||||||
|
_cacheManager.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Add(key, value, ttl, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get(string key, string region)
|
||||||
|
{
|
||||||
|
return _cacheManager.Get<T>(key, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearRegion(string region)
|
||||||
|
{
|
||||||
|
_cacheManager.ClearRegion(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Cache.CacheManager/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Cache.CacheManager/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
76
src/Ocelot.Provider.Consul/Consul.cs
Normal file
76
src/Ocelot.Provider.Consul/Consul.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Consul;
|
||||||
|
using Infrastructure.Extensions;
|
||||||
|
using Logging;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
|
using Values;
|
||||||
|
|
||||||
|
|
||||||
|
public class Consul : IServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
private readonly ConsulRegistryConfiguration _config;
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
private readonly IConsulClient _consul;
|
||||||
|
private const string VersionPrefix = "version-";
|
||||||
|
|
||||||
|
public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory)
|
||||||
|
{
|
||||||
|
_logger = factory.CreateLogger<Consul>();
|
||||||
|
_config = config;
|
||||||
|
_consul = clientFactory.Get(_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Service>> Get()
|
||||||
|
{
|
||||||
|
var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true);
|
||||||
|
|
||||||
|
var services = new List<Service>();
|
||||||
|
|
||||||
|
foreach (var serviceEntry in queryResult.Response)
|
||||||
|
{
|
||||||
|
if (IsValid(serviceEntry))
|
||||||
|
{
|
||||||
|
services.Add(BuildService(serviceEntry));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return services.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Service BuildService(ServiceEntry serviceEntry)
|
||||||
|
{
|
||||||
|
return new Service(
|
||||||
|
serviceEntry.Service.Service,
|
||||||
|
new ServiceHostAndPort(serviceEntry.Service.Address, serviceEntry.Service.Port),
|
||||||
|
serviceEntry.Service.ID,
|
||||||
|
GetVersionFromStrings(serviceEntry.Service.Tags),
|
||||||
|
serviceEntry.Service.Tags ?? Enumerable.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValid(ServiceEntry serviceEntry)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(serviceEntry.Service.Address) || serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetVersionFromStrings(IEnumerable<string> strings)
|
||||||
|
{
|
||||||
|
return strings
|
||||||
|
?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal))
|
||||||
|
.TrimStart(VersionPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/Ocelot.Provider.Consul/ConsulClientFactory.cs
Normal file
21
src/Ocelot.Provider.Consul/ConsulClientFactory.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using global::Consul;
|
||||||
|
|
||||||
|
public class ConsulClientFactory : IConsulClientFactory
|
||||||
|
{
|
||||||
|
public IConsulClient Get(ConsulRegistryConfiguration config)
|
||||||
|
{
|
||||||
|
return new ConsulClient(c =>
|
||||||
|
{
|
||||||
|
c.Address = new Uri($"http://{config.Host}:{config.Port}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(config?.Token))
|
||||||
|
{
|
||||||
|
c.Token = config.Token;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.File;
|
||||||
|
using Configuration.Repository;
|
||||||
|
using global::Consul;
|
||||||
|
using Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Responses;
|
||||||
|
|
||||||
|
public class ConsulFileConfigurationRepository : IFileConfigurationRepository
|
||||||
|
{
|
||||||
|
private readonly IConsulClient _consul;
|
||||||
|
private readonly string _configurationKey;
|
||||||
|
private readonly Cache.IOcelotCache<FileConfiguration> _cache;
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
|
||||||
|
public ConsulFileConfigurationRepository(
|
||||||
|
Cache.IOcelotCache<FileConfiguration> cache,
|
||||||
|
IInternalConfigurationRepository repo,
|
||||||
|
IConsulClientFactory factory,
|
||||||
|
IOcelotLoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<ConsulFileConfigurationRepository>();
|
||||||
|
_cache = cache;
|
||||||
|
|
||||||
|
var internalConfig = repo.Get();
|
||||||
|
|
||||||
|
_configurationKey = "InternalConfiguration";
|
||||||
|
|
||||||
|
string token = null;
|
||||||
|
|
||||||
|
if (!internalConfig.IsError)
|
||||||
|
{
|
||||||
|
token = internalConfig.Data.ServiceProviderConfiguration.Token;
|
||||||
|
_configurationKey = !string.IsNullOrEmpty(internalConfig.Data.ServiceProviderConfiguration.ConfigurationKey) ?
|
||||||
|
internalConfig.Data.ServiceProviderConfiguration.ConfigurationKey : _configurationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = new ConsulRegistryConfiguration(internalConfig.Data.ServiceProviderConfiguration.Host,
|
||||||
|
internalConfig.Data.ServiceProviderConfiguration.Port, _configurationKey, token);
|
||||||
|
|
||||||
|
_consul = factory.Get(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Response<FileConfiguration>> Get()
|
||||||
|
{
|
||||||
|
var config = _cache.Get(_configurationKey, _configurationKey);
|
||||||
|
|
||||||
|
if (config != null)
|
||||||
|
{
|
||||||
|
return new OkResponse<FileConfiguration>(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryResult = await _consul.KV.Get(_configurationKey);
|
||||||
|
|
||||||
|
if (queryResult.Response == null)
|
||||||
|
{
|
||||||
|
return new OkResponse<FileConfiguration>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = queryResult.Response.Value;
|
||||||
|
|
||||||
|
var json = Encoding.UTF8.GetString(bytes);
|
||||||
|
|
||||||
|
var consulConfig = JsonConvert.DeserializeObject<FileConfiguration>(json);
|
||||||
|
|
||||||
|
return new OkResponse<FileConfiguration>(consulConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Response> Set(FileConfiguration ocelotConfiguration)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
var kvPair = new KVPair(_configurationKey)
|
||||||
|
{
|
||||||
|
Value = bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _consul.KV.Put(kvPair);
|
||||||
|
|
||||||
|
if (result.Response)
|
||||||
|
{
|
||||||
|
_cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(3), _configurationKey);
|
||||||
|
|
||||||
|
return new OkResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ErrorResponse(new UnableToSetConfigInConsulError($"Unable to set FileConfiguration in consul, response status code from consul was {result.StatusCode}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.Creator;
|
||||||
|
using Configuration.File;
|
||||||
|
using Configuration.Repository;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Middleware;
|
||||||
|
using Responses;
|
||||||
|
|
||||||
|
public static class ConsulMiddlewareConfigurationProvider
|
||||||
|
{
|
||||||
|
public static OcelotMiddlewareConfigurationDelegate Get = async builder =>
|
||||||
|
{
|
||||||
|
var fileConfigRepo = builder.ApplicationServices.GetService<IFileConfigurationRepository>();
|
||||||
|
var fileConfig = builder.ApplicationServices.GetService<IOptionsMonitor<FileConfiguration>>();
|
||||||
|
var internalConfigCreator = builder.ApplicationServices.GetService<IInternalConfigurationCreator>();
|
||||||
|
var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();
|
||||||
|
|
||||||
|
if (UsingConsul(fileConfigRepo))
|
||||||
|
{
|
||||||
|
await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool UsingConsul(IFileConfigurationRepository fileConfigRepo)
|
||||||
|
{
|
||||||
|
return fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SetFileConfigInConsul(IApplicationBuilder builder,
|
||||||
|
IFileConfigurationRepository fileConfigRepo, IOptionsMonitor<FileConfiguration> fileConfig,
|
||||||
|
IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo)
|
||||||
|
{
|
||||||
|
// get the config from consul.
|
||||||
|
var fileConfigFromConsul = await fileConfigRepo.Get();
|
||||||
|
|
||||||
|
if (IsError(fileConfigFromConsul))
|
||||||
|
{
|
||||||
|
ThrowToStopOcelotStarting(fileConfigFromConsul);
|
||||||
|
}
|
||||||
|
else if (ConfigNotStoredInConsul(fileConfigFromConsul))
|
||||||
|
{
|
||||||
|
//there was no config in consul set the file in config in consul
|
||||||
|
await fileConfigRepo.Set(fileConfig.CurrentValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// create the internal config from consul data
|
||||||
|
var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data);
|
||||||
|
|
||||||
|
if (IsError(internalConfig))
|
||||||
|
{
|
||||||
|
ThrowToStopOcelotStarting(internalConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// add the internal config to the internal repo
|
||||||
|
var response = internalConfigRepo.AddOrReplace(internalConfig.Data);
|
||||||
|
|
||||||
|
if (IsError(response))
|
||||||
|
{
|
||||||
|
ThrowToStopOcelotStarting(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsError(internalConfig))
|
||||||
|
{
|
||||||
|
ThrowToStopOcelotStarting(internalConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowToStopOcelotStarting(Response config)
|
||||||
|
{
|
||||||
|
throw new Exception($"Unable to start Ocelot, errors are: {string.Join(",", config.Errors.Select(x => x.ToString()))}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsError(Response response)
|
||||||
|
{
|
||||||
|
return response == null || response.IsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ConfigNotStoredInConsul(Response<FileConfiguration> fileConfigFromConsul)
|
||||||
|
{
|
||||||
|
return fileConfigFromConsul.Data == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/Ocelot.Provider.Consul/ConsulProviderFactory.cs
Normal file
29
src/Ocelot.Provider.Consul/ConsulProviderFactory.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Logging;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ServiceDiscovery;
|
||||||
|
|
||||||
|
public static class ConsulProviderFactory
|
||||||
|
{
|
||||||
|
public static ServiceDiscoveryFinderDelegate Get = (provider, config, name) =>
|
||||||
|
{
|
||||||
|
var factory = provider.GetService<IOcelotLoggerFactory>();
|
||||||
|
|
||||||
|
var consulFactory = provider.GetService<IConsulClientFactory>();
|
||||||
|
|
||||||
|
var consulRegistryConfiguration = new ConsulRegistryConfiguration(config.Host, config.Port, name, config.Token);
|
||||||
|
|
||||||
|
var consulServiceDiscoveryProvider = new Consul(consulRegistryConfiguration, factory, consulFactory);
|
||||||
|
|
||||||
|
if (config.Type?.ToLower() == "pollconsul")
|
||||||
|
{
|
||||||
|
return new PollConsul(config.PollingInterval, factory, consulServiceDiscoveryProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return consulServiceDiscoveryProvider;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs
Normal file
18
src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
public class ConsulRegistryConfiguration
|
||||||
|
{
|
||||||
|
public ConsulRegistryConfiguration(string host, int port, string keyOfServiceInConsul, string token)
|
||||||
|
{
|
||||||
|
Host = string.IsNullOrEmpty(host) ? "localhost" : host;
|
||||||
|
Port = port > 0 ? port : 8500;
|
||||||
|
KeyOfServiceInConsul = keyOfServiceInConsul;
|
||||||
|
Token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string KeyOfServiceInConsul { get; }
|
||||||
|
public string Host { get; }
|
||||||
|
public int Port { get; }
|
||||||
|
public string Token { get; }
|
||||||
|
}
|
||||||
|
}
|
9
src/Ocelot.Provider.Consul/IConsulClientFactory.cs
Normal file
9
src/Ocelot.Provider.Consul/IConsulClientFactory.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using global::Consul;
|
||||||
|
|
||||||
|
public interface IConsulClientFactory
|
||||||
|
{
|
||||||
|
IConsulClient Get(ConsulRegistryConfiguration config);
|
||||||
|
}
|
||||||
|
}
|
37
src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj
Normal file
37
src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use Consul</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Provider.Consul</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Provider.Consul</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Provider.Consul</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Consul</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Consul</PackageProjectUrl>
|
||||||
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Consul" Version="0.7.2.6" />
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
26
src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs
Normal file
26
src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using Configuration.Repository;
|
||||||
|
using DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Middleware;
|
||||||
|
using ServiceDiscovery;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotBuilder AddConsul(this IOcelotBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<ServiceDiscoveryFinderDelegate>(ConsulProviderFactory.Get);
|
||||||
|
builder.Services.AddSingleton<IConsulClientFactory, ConsulClientFactory>();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<OcelotMiddlewareConfigurationDelegate>(ConsulMiddlewareConfigurationProvider.Get);
|
||||||
|
builder.Services.AddHostedService<FileConfigurationPoller>();
|
||||||
|
builder.Services.AddSingleton<IFileConfigurationRepository, ConsulFileConfigurationRepository>();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Logging;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
|
using Values;
|
||||||
|
|
||||||
|
public class PollConsul : IServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider;
|
||||||
|
private readonly Timer _timer;
|
||||||
|
private bool _polling;
|
||||||
|
private List<Service> _services;
|
||||||
|
|
||||||
|
public PollConsul(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider)
|
||||||
|
{
|
||||||
|
_logger = factory.CreateLogger<PollConsul>();
|
||||||
|
_consulServiceDiscoveryProvider = consulServiceDiscoveryProvider;
|
||||||
|
_services = new List<Service>();
|
||||||
|
|
||||||
|
_timer = new Timer(async x =>
|
||||||
|
{
|
||||||
|
if (_polling)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_polling = true;
|
||||||
|
await Poll();
|
||||||
|
_polling = false;
|
||||||
|
}, null, pollingInterval, pollingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<Service>> Get()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_services);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Poll()
|
||||||
|
{
|
||||||
|
_services = await _consulServiceDiscoveryProvider.Get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Provider.Consul/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Provider.Consul/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
12
src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs
Normal file
12
src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Ocelot.Provider.Consul
|
||||||
|
{
|
||||||
|
using Errors;
|
||||||
|
|
||||||
|
public class UnableToSetConfigInConsulError : Error
|
||||||
|
{
|
||||||
|
public UnableToSetConfigInConsulError(string s)
|
||||||
|
: base(s, OcelotErrorCode.UnknownError)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
src/Ocelot.Provider.Eureka/Eureka.cs
Normal file
35
src/Ocelot.Provider.Eureka/Eureka.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
namespace Ocelot.Provider.Eureka
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using Values;
|
||||||
|
|
||||||
|
public class Eureka : IServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
private readonly IDiscoveryClient _client;
|
||||||
|
private readonly string _serviceName;
|
||||||
|
|
||||||
|
public Eureka(string serviceName, IDiscoveryClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_serviceName = serviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<Service>> Get()
|
||||||
|
{
|
||||||
|
var services = new List<Service>();
|
||||||
|
|
||||||
|
var instances = _client.GetInstances(_serviceName);
|
||||||
|
|
||||||
|
if (instances != null && instances.Any())
|
||||||
|
{
|
||||||
|
services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port), "", "", new List<string>())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
namespace Ocelot.Provider.Eureka
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using Configuration.Repository;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Middleware;
|
||||||
|
using Pivotal.Discovery.Client;
|
||||||
|
|
||||||
|
public class EurekaMiddlewareConfigurationProvider
|
||||||
|
{
|
||||||
|
public static OcelotMiddlewareConfigurationDelegate Get = builder =>
|
||||||
|
{
|
||||||
|
var internalConfigRepo = builder.ApplicationServices.GetService<IInternalConfigurationRepository>();
|
||||||
|
|
||||||
|
var config = internalConfigRepo.Get();
|
||||||
|
|
||||||
|
if (UsingEurekaServiceDiscoveryProvider(config.Data))
|
||||||
|
{
|
||||||
|
builder.UseDiscoveryClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool UsingEurekaServiceDiscoveryProvider(IInternalConfiguration configuration)
|
||||||
|
{
|
||||||
|
return configuration?.ServiceProviderConfiguration != null && configuration.ServiceProviderConfiguration.Type?.ToLower() == "eureka";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs
Normal file
22
src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace Ocelot.Provider.Eureka
|
||||||
|
{
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ServiceDiscovery;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
|
||||||
|
public static class EurekaProviderFactory
|
||||||
|
{
|
||||||
|
public static ServiceDiscoveryFinderDelegate Get = (provider, config, name) =>
|
||||||
|
{
|
||||||
|
var client = provider.GetService<IDiscoveryClient>();
|
||||||
|
|
||||||
|
if (config.Type?.ToLower() == "eureka" && client != null)
|
||||||
|
{
|
||||||
|
return new Eureka(name, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
37
src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj
Normal file
37
src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use Eureka</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Provider.Eureka</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Provider.Eureka</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Provider.Eureka</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Eureka</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Eureka</PackageProjectUrl>
|
||||||
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.0.1" />
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
23
src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs
Normal file
23
src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace Ocelot.Provider.Eureka
|
||||||
|
{
|
||||||
|
using System.Linq;
|
||||||
|
using DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Middleware;
|
||||||
|
using Pivotal.Discovery.Client;
|
||||||
|
using ServiceDiscovery;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotBuilder AddEureka(this IOcelotBuilder builder)
|
||||||
|
{
|
||||||
|
var service = builder.Services.First(x => x.ServiceType == typeof(IConfiguration));
|
||||||
|
var configuration = (IConfiguration)service.ImplementationInstance;
|
||||||
|
builder.Services.AddDiscoveryClient(configuration);
|
||||||
|
builder.Services.AddSingleton<ServiceDiscoveryFinderDelegate>(EurekaProviderFactory.Get);
|
||||||
|
builder.Services.AddSingleton<OcelotMiddlewareConfigurationDelegate>(EurekaMiddlewareConfigurationProvider.Get);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Provider.Eureka/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Provider.Eureka/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
17
src/Ocelot.Provider.Polly/CircuitBreaker.cs
Normal file
17
src/Ocelot.Provider.Polly/CircuitBreaker.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Polly.CircuitBreaker;
|
||||||
|
using Polly.Timeout;
|
||||||
|
|
||||||
|
namespace Ocelot.Provider.Polly
|
||||||
|
{
|
||||||
|
public class CircuitBreaker
|
||||||
|
{
|
||||||
|
public CircuitBreaker(CircuitBreakerPolicy circuitBreakerPolicy, TimeoutPolicy timeoutPolicy)
|
||||||
|
{
|
||||||
|
CircuitBreakerPolicy = circuitBreakerPolicy;
|
||||||
|
TimeoutPolicy = timeoutPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircuitBreakerPolicy CircuitBreakerPolicy { get; private set; }
|
||||||
|
public TimeoutPolicy TimeoutPolicy { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
37
src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj
Normal file
37
src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use Polly.NET</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Provider.Polly</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Provider.Polly</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Provider.Polly</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Polly</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Polly</PackageProjectUrl>
|
||||||
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Polly" Version="6.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
38
src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs
Normal file
38
src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
namespace Ocelot.Provider.Polly
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using DependencyInjection;
|
||||||
|
using Errors;
|
||||||
|
using global::Polly.CircuitBreaker;
|
||||||
|
using global::Polly.Timeout;
|
||||||
|
using Logging;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Requester;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotBuilder AddPolly(this IOcelotBuilder builder)
|
||||||
|
{
|
||||||
|
var errorMapping = new Dictionary<Type, Func<Exception, Error>>
|
||||||
|
{
|
||||||
|
{typeof(TaskCanceledException), e => new RequestTimedOutError(e)},
|
||||||
|
{typeof(TimeoutRejectedException), e => new RequestTimedOutError(e)},
|
||||||
|
{typeof(BrokenCircuitException), e => new RequestTimedOutError(e)}
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(errorMapping);
|
||||||
|
|
||||||
|
DelegatingHandler QosDelegatingHandlerDelegate(DownstreamReRoute reRoute, IOcelotLoggerFactory logger)
|
||||||
|
{
|
||||||
|
return new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(reRoute, logger), logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton((QosDelegatingHandlerDelegate) QosDelegatingHandlerDelegate);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Polly;
|
||||||
|
using Polly.CircuitBreaker;
|
||||||
|
|
||||||
|
namespace Ocelot.Provider.Polly
|
||||||
|
{
|
||||||
|
public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly PollyQoSProvider _qoSProvider;
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
|
||||||
|
public PollyCircuitBreakingDelegatingHandler(
|
||||||
|
PollyQoSProvider qoSProvider,
|
||||||
|
IOcelotLoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_qoSProvider = qoSProvider;
|
||||||
|
_logger = loggerFactory.CreateLogger<PollyCircuitBreakingDelegatingHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Policy
|
||||||
|
.WrapAsync(_qoSProvider.CircuitBreaker.CircuitBreakerPolicy, _qoSProvider.CircuitBreaker.TimeoutPolicy)
|
||||||
|
.ExecuteAsync(() => base.SendAsync(request,cancellationToken));
|
||||||
|
}
|
||||||
|
catch (BrokenCircuitException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Reached to allowed number of exceptions. Circuit is open",ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/Ocelot.Provider.Polly/PollyQoSProvider.cs
Normal file
52
src/Ocelot.Provider.Polly/PollyQoSProvider.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
namespace Ocelot.Provider.Polly
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using global::Polly;
|
||||||
|
using global::Polly.CircuitBreaker;
|
||||||
|
using global::Polly.Timeout;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
|
||||||
|
public class PollyQoSProvider
|
||||||
|
{
|
||||||
|
private readonly CircuitBreakerPolicy _circuitBreakerPolicy;
|
||||||
|
private readonly TimeoutPolicy _timeoutPolicy;
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
|
||||||
|
public PollyQoSProvider(DownstreamReRoute reRoute, IOcelotLoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<PollyQoSProvider>();
|
||||||
|
|
||||||
|
Enum.TryParse(reRoute.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy);
|
||||||
|
|
||||||
|
_timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(reRoute.QosOptions.TimeoutValue), strategy);
|
||||||
|
|
||||||
|
_circuitBreakerPolicy = Policy
|
||||||
|
.Handle<HttpRequestException>()
|
||||||
|
.Or<TimeoutRejectedException>()
|
||||||
|
.Or<TimeoutException>()
|
||||||
|
.CircuitBreakerAsync(
|
||||||
|
exceptionsAllowedBeforeBreaking: reRoute.QosOptions.ExceptionsAllowedBeforeBreaking,
|
||||||
|
durationOfBreak: TimeSpan.FromMilliseconds(reRoute.QosOptions.DurationOfBreak),
|
||||||
|
onBreak: (ex, breakDelay) =>
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex);
|
||||||
|
},
|
||||||
|
onReset: () =>
|
||||||
|
{
|
||||||
|
_logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again.");
|
||||||
|
},
|
||||||
|
onHalfOpen: () =>
|
||||||
|
{
|
||||||
|
_logger.LogDebug(".Breaker logging: Half-open; next call is a trial.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CircuitBreaker = new CircuitBreaker(_circuitBreakerPolicy, _timeoutPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CircuitBreaker CircuitBreaker { get; }
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Provider.Polly/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Provider.Polly/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
13
src/Ocelot.Provider.Polly/RequestTimedOutError.cs
Normal file
13
src/Ocelot.Provider.Polly/RequestTimedOutError.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Ocelot.Provider.Polly
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Errors;
|
||||||
|
|
||||||
|
public class RequestTimedOutError : Error
|
||||||
|
{
|
||||||
|
public RequestTimedOutError(Exception exception)
|
||||||
|
: base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/Ocelot.Provider.Rafty/BearerToken.cs
Normal file
16
src/Ocelot.Provider.Rafty/BearerToken.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
internal class BearerToken
|
||||||
|
{
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("token_type")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
src/Ocelot.Provider.Rafty/FakeCommand.cs
Normal file
14
src/Ocelot.Provider.Rafty/FakeCommand.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
|
||||||
|
public class FakeCommand : ICommand
|
||||||
|
{
|
||||||
|
public FakeCommand(string value)
|
||||||
|
{
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
7
src/Ocelot.Provider.Rafty/FilePeer.cs
Normal file
7
src/Ocelot.Provider.Rafty/FilePeer.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
public class FilePeer
|
||||||
|
{
|
||||||
|
public string HostAndPort { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
src/Ocelot.Provider.Rafty/FilePeers.cs
Normal file
14
src/Ocelot.Provider.Rafty/FilePeers.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class FilePeers
|
||||||
|
{
|
||||||
|
public FilePeers()
|
||||||
|
{
|
||||||
|
Peers = new List<FilePeer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FilePeer> Peers { get; set; }
|
||||||
|
}
|
||||||
|
}
|
45
src/Ocelot.Provider.Rafty/FilePeersProvider.cs
Normal file
45
src/Ocelot.Provider.Rafty/FilePeersProvider.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Net.Http;
|
||||||
|
using Configuration;
|
||||||
|
using Configuration.Repository;
|
||||||
|
using global::Rafty.Concensus.Peers;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Middleware;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Administration;
|
||||||
|
|
||||||
|
public class FilePeersProvider : IPeersProvider
|
||||||
|
{
|
||||||
|
private readonly IOptions<FilePeers> _options;
|
||||||
|
private readonly List<IPeer> _peers;
|
||||||
|
private IBaseUrlFinder _finder;
|
||||||
|
private IInternalConfigurationRepository _repo;
|
||||||
|
private IIdentityServerConfiguration _identityServerConfig;
|
||||||
|
|
||||||
|
public FilePeersProvider(IOptions<FilePeers> options, IBaseUrlFinder finder, IInternalConfigurationRepository repo, IIdentityServerConfiguration identityServerConfig)
|
||||||
|
{
|
||||||
|
_identityServerConfig = identityServerConfig;
|
||||||
|
_repo = repo;
|
||||||
|
_finder = finder;
|
||||||
|
_options = options;
|
||||||
|
_peers = new List<IPeer>();
|
||||||
|
|
||||||
|
var config = _repo.Get();
|
||||||
|
foreach (var item in _options.Value.Peers)
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
//todo what if this errors?
|
||||||
|
var httpPeer = new HttpPeer(item.HostAndPort, httpClient, _finder, config.Data, _identityServerConfig);
|
||||||
|
_peers.Add(httpPeer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IPeer> Get()
|
||||||
|
{
|
||||||
|
return _peers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
src/Ocelot.Provider.Rafty/HttpPeer.cs
Normal file
130
src/Ocelot.Provider.Rafty/HttpPeer.cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using global::Rafty.Concensus.Messages;
|
||||||
|
using global::Rafty.Concensus.Peers;
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
using Middleware;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Administration;
|
||||||
|
|
||||||
|
public class HttpPeer : IPeer
|
||||||
|
{
|
||||||
|
private readonly string _hostAndPort;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly JsonSerializerSettings _jsonSerializerSettings;
|
||||||
|
private readonly string _baseSchemeUrlAndPort;
|
||||||
|
private BearerToken _token;
|
||||||
|
private readonly IInternalConfiguration _config;
|
||||||
|
private readonly IIdentityServerConfiguration _identityServerConfiguration;
|
||||||
|
|
||||||
|
public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration)
|
||||||
|
{
|
||||||
|
_identityServerConfiguration = identityServerConfiguration;
|
||||||
|
_config = config;
|
||||||
|
Id = hostAndPort;
|
||||||
|
_hostAndPort = hostAndPort;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
_baseSchemeUrlAndPort = finder.Find();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public async Task<RequestVoteResponse> Request(RequestVote requestVote)
|
||||||
|
{
|
||||||
|
if (_token == null)
|
||||||
|
{
|
||||||
|
await SetToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings);
|
||||||
|
var content = new StringContent(json);
|
||||||
|
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||||
|
var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<RequestVoteResponse>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RequestVoteResponse(false, requestVote.Term);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppendEntriesResponse> Request(AppendEntries appendEntries)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_token == null)
|
||||||
|
{
|
||||||
|
await SetToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings);
|
||||||
|
var content = new StringContent(json);
|
||||||
|
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||||
|
var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<AppendEntriesResponse>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppendEntriesResponse(appendEntries.Term, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
return new AppendEntriesResponse(appendEntries.Term, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Response<T>> Request<T>(T command)
|
||||||
|
where T : ICommand
|
||||||
|
{
|
||||||
|
Console.WriteLine("SENDING REQUEST....");
|
||||||
|
if (_token == null)
|
||||||
|
{
|
||||||
|
await SetToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings);
|
||||||
|
var content = new StringContent(json);
|
||||||
|
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||||
|
var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine("REQUEST OK....");
|
||||||
|
var okResponse = JsonConvert.DeserializeObject<OkResponse<ICommand>>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings);
|
||||||
|
return new OkResponse<T>((T)okResponse.Command);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("REQUEST NOT OK....");
|
||||||
|
return new ErrorResponse<T>(await response.Content.ReadAsStringAsync(), command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetToken()
|
||||||
|
{
|
||||||
|
var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token";
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", _identityServerConfiguration.ApiName),
|
||||||
|
new KeyValuePair<string, string>("client_secret", _identityServerConfiguration.ApiSecret),
|
||||||
|
new KeyValuePair<string, string>("scope", _identityServerConfiguration.ApiName),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
var response = await _httpClient.PostAsync(tokenUrl, content);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj
Normal file
39
src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>Provides Ocelot extensions to use Rafty</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Provider.Rafty</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Provider.Rafty</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Provider.Rafty</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Rafty</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot.Provider.Rafty</PackageProjectUrl>
|
||||||
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
<ProjectReference Include="..\Ocelot.Administration\Ocelot.Administration.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Data.SQLite" Version="2.2.0"/>
|
||||||
|
<PackageReference Include="Rafty" Version="0.4.4"/>
|
||||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -0,0 +1,28 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using Configuration.Setter;
|
||||||
|
using DependencyInjection;
|
||||||
|
using global::Rafty.Concensus.Node;
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
using global::Rafty.Log;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
public static class OcelotAdministrationBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotAdministrationBuilder AddRafty(this IOcelotAdministrationBuilder builder)
|
||||||
|
{
|
||||||
|
var settings = new InMemorySettings(4000, 6000, 100, 10000);
|
||||||
|
builder.Services.RemoveAll<IFileConfigurationSetter>();
|
||||||
|
builder.Services.AddSingleton<IFileConfigurationSetter, RaftyFileConfigurationSetter>();
|
||||||
|
builder.Services.AddSingleton<ILog, SqlLiteLog>();
|
||||||
|
builder.Services.AddSingleton<IFiniteStateMachine, OcelotFiniteStateMachine>();
|
||||||
|
builder.Services.AddSingleton<ISettings>(settings);
|
||||||
|
builder.Services.AddSingleton<IPeersProvider, FilePeersProvider>();
|
||||||
|
builder.Services.AddSingleton<INode, Node>();
|
||||||
|
builder.Services.Configure<FilePeers>(builder.ConfigurationRoot);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs
Normal file
25
src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.Setter;
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
using global::Rafty.Log;
|
||||||
|
|
||||||
|
public class OcelotFiniteStateMachine : IFiniteStateMachine
|
||||||
|
{
|
||||||
|
private readonly IFileConfigurationSetter _setter;
|
||||||
|
|
||||||
|
public OcelotFiniteStateMachine(IFileConfigurationSetter setter)
|
||||||
|
{
|
||||||
|
_setter = setter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(LogEntry log)
|
||||||
|
{
|
||||||
|
//todo - handle an error
|
||||||
|
//hack it to just cast as at the moment we know this is the only command :P
|
||||||
|
var hack = (UpdateFileConfiguration)log.CommandData;
|
||||||
|
await _setter.Set(hack.Configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs
Normal file
18
src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("Ocelot")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")]
|
96
src/Ocelot.Provider.Rafty/RaftController.cs
Normal file
96
src/Ocelot.Provider.Rafty/RaftController.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Rafty.Concensus.Messages;
|
||||||
|
using global::Rafty.Concensus.Node;
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
using Logging;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Middleware;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[Route("raft")]
|
||||||
|
public class RaftController : Controller
|
||||||
|
{
|
||||||
|
private readonly INode _node;
|
||||||
|
private readonly IOcelotLogger _logger;
|
||||||
|
private readonly string _baseSchemeUrlAndPort;
|
||||||
|
private readonly JsonSerializerSettings _jsonSerialiserSettings;
|
||||||
|
|
||||||
|
public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IBaseUrlFinder finder)
|
||||||
|
{
|
||||||
|
_jsonSerialiserSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
_baseSchemeUrlAndPort = finder.Find();
|
||||||
|
_logger = loggerFactory.CreateLogger<RaftController>();
|
||||||
|
_node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("appendentries")]
|
||||||
|
public async Task<IActionResult> AppendEntries()
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(HttpContext.Request.Body))
|
||||||
|
{
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var appendEntries = JsonConvert.DeserializeObject<AppendEntries>(json, _jsonSerialiserSettings);
|
||||||
|
|
||||||
|
_logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}");
|
||||||
|
|
||||||
|
var appendEntriesResponse = await _node.Handle(appendEntries);
|
||||||
|
|
||||||
|
return new OkObjectResult(appendEntriesResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("requestvote")]
|
||||||
|
public async Task<IActionResult> RequestVote()
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(HttpContext.Request.Body))
|
||||||
|
{
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var requestVote = JsonConvert.DeserializeObject<RequestVote>(json, _jsonSerialiserSettings);
|
||||||
|
|
||||||
|
_logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}");
|
||||||
|
|
||||||
|
var requestVoteResponse = await _node.Handle(requestVote);
|
||||||
|
|
||||||
|
return new OkObjectResult(requestVoteResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("command")]
|
||||||
|
public async Task<IActionResult> Command()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(HttpContext.Request.Body))
|
||||||
|
{
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var command = JsonConvert.DeserializeObject<ICommand>(json, _jsonSerialiserSettings);
|
||||||
|
|
||||||
|
_logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}");
|
||||||
|
|
||||||
|
var commandResponse = await _node.Accept(command);
|
||||||
|
|
||||||
|
json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings);
|
||||||
|
|
||||||
|
return StatusCode(200, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs
Normal file
30
src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.File;
|
||||||
|
using Configuration.Setter;
|
||||||
|
using global::Rafty.Concensus.Node;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
|
||||||
|
public class RaftyFileConfigurationSetter : IFileConfigurationSetter
|
||||||
|
{
|
||||||
|
private readonly INode _node;
|
||||||
|
|
||||||
|
public RaftyFileConfigurationSetter(INode node)
|
||||||
|
{
|
||||||
|
_node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Responses.Response> Set(FileConfiguration fileConfiguration)
|
||||||
|
{
|
||||||
|
var result = await _node.Accept(new UpdateFileConfiguration(fileConfiguration));
|
||||||
|
|
||||||
|
if (result.GetType() == typeof(ErrorResponse<UpdateFileConfiguration>))
|
||||||
|
{
|
||||||
|
return new Responses.ErrorResponse(new UnableToSaveAcceptCommand($"unable to save file configuration to state machine"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Responses.OkResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Rafty.Concensus.Node;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Middleware;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
||||||
|
public static class RaftyMiddlewareConfigurationProvider
|
||||||
|
{
|
||||||
|
public static OcelotMiddlewareConfigurationDelegate Get = builder =>
|
||||||
|
{
|
||||||
|
if (UsingRafty(builder))
|
||||||
|
{
|
||||||
|
SetUpRafty(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool UsingRafty(IApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
var node = builder.ApplicationServices.GetService<INode>();
|
||||||
|
if (node != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetUpRafty(IApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
var applicationLifetime = builder.ApplicationServices.GetService<IApplicationLifetime>();
|
||||||
|
applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder));
|
||||||
|
var node = builder.ApplicationServices.GetService<INode>();
|
||||||
|
var nodeId = builder.ApplicationServices.GetService<NodeId>();
|
||||||
|
node.Start(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnShutdown(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
var node = app.ApplicationServices.GetService<INode>();
|
||||||
|
node.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
334
src/Ocelot.Provider.Rafty/SqlLiteLog.cs
Normal file
334
src/Ocelot.Provider.Rafty/SqlLiteLog.cs
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Rafty.Infrastructure;
|
||||||
|
using global::Rafty.Log;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
public class SqlLiteLog : ILog
|
||||||
|
{
|
||||||
|
private readonly string _path;
|
||||||
|
private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly NodeId _nodeId;
|
||||||
|
|
||||||
|
public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<SqlLiteLog>();
|
||||||
|
_nodeId = nodeId;
|
||||||
|
_path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db";
|
||||||
|
_sempaphore.Wait();
|
||||||
|
|
||||||
|
if (!File.Exists(_path))
|
||||||
|
{
|
||||||
|
var fs = File.Create(_path);
|
||||||
|
|
||||||
|
fs.Dispose();
|
||||||
|
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
const string sql = @"create table logs (
|
||||||
|
id integer primary key,
|
||||||
|
data text not null
|
||||||
|
)";
|
||||||
|
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var result = command.ExecuteNonQuery();
|
||||||
|
|
||||||
|
_logger.LogInformation(result == 0
|
||||||
|
? $"id: {_nodeId.Id} create database, result: {result}"
|
||||||
|
: $"id: {_nodeId.Id} did not create database., result: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> LastLogIndex()
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
var result = 1;
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
var sql = @"select id from logs order by id desc limit 1";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var index = Convert.ToInt32(await command.ExecuteScalarAsync());
|
||||||
|
if (index > result)
|
||||||
|
{
|
||||||
|
result = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> LastLogTerm()
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
long result = 0;
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
var sql = @"select data from logs order by id desc limit 1";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var data = Convert.ToString(await command.ExecuteScalarAsync());
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
if (log != null && log.Term > result)
|
||||||
|
{
|
||||||
|
result = log.Term;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> Count()
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
var result = 0;
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
var sql = @"select count(id) from logs";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var index = Convert.ToInt32(await command.ExecuteScalarAsync());
|
||||||
|
if (index > result)
|
||||||
|
{
|
||||||
|
result = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> Apply(LogEntry log)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
var data = JsonConvert.SerializeObject(log, jsonSerializerSettings);
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"insert into logs (data) values ('{data}')";
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}");
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var result = await command.ExecuteNonQueryAsync();
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = "select last_insert_rowid()";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var result = await command.ExecuteScalarAsync();
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore");
|
||||||
|
_sempaphore.Release();
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite");
|
||||||
|
return Convert.ToInt32(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"select data from logs where id = {index};";
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id} sql: {sql}");
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var data = Convert.ToString(await command.ExecuteScalarAsync());
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}");
|
||||||
|
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
if (logEntry != null && log != null && logEntry.Term != log.Term)
|
||||||
|
{
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var deleteSql = $"delete from logs where id >= {index};";
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}");
|
||||||
|
using (var deleteCommand = new SqliteCommand(deleteSql, connection))
|
||||||
|
{
|
||||||
|
var result = await deleteCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsDuplicate(int index, LogEntry logEntry)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"select data from logs where id = {index};";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var data = Convert.ToString(await command.ExecuteScalarAsync());
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
|
||||||
|
if (logEntry != null && log != null && logEntry.Term == log.Term)
|
||||||
|
{
|
||||||
|
_sempaphore.Release();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogEntry> Get(int index)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"select data from logs where id = {index}";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var data = Convert.ToString(await command.ExecuteScalarAsync());
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
_sempaphore.Release();
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<(int index, LogEntry logEntry)>> GetFrom(int index)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
var logsToReturn = new List<(int, LogEntry)>();
|
||||||
|
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"select id, data from logs where id >= {index}";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
using (var reader = await command.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var id = Convert.ToInt32(reader[0]);
|
||||||
|
var data = (string)reader[1];
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
logsToReturn.Add((id, log));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return logsToReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> GetTermAtIndex(int index)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
long result = 0;
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var sql = $"select data from logs where id = {index}";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var data = Convert.ToString(await command.ExecuteScalarAsync());
|
||||||
|
var jsonSerializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
};
|
||||||
|
var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
|
||||||
|
if (log != null && log.Term > result)
|
||||||
|
{
|
||||||
|
result = log.Term;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Remove(int indexOfCommand)
|
||||||
|
{
|
||||||
|
_sempaphore.Wait();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={_path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
//todo - sql injection dont copy this..
|
||||||
|
var deleteSql = $"delete from logs where id >= {indexOfCommand};";
|
||||||
|
_logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}");
|
||||||
|
using (var deleteCommand = new SqliteCommand(deleteSql, connection))
|
||||||
|
{
|
||||||
|
var result = await deleteCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sempaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs
Normal file
11
src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using Errors;
|
||||||
|
public class UnableToSaveAcceptCommand : Error
|
||||||
|
{
|
||||||
|
public UnableToSaveAcceptCommand(string message)
|
||||||
|
: base(message, OcelotErrorCode.UnknownError)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs
Normal file
15
src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace Ocelot.Provider.Rafty
|
||||||
|
{
|
||||||
|
using Configuration.File;
|
||||||
|
using global::Rafty.FiniteStateMachine;
|
||||||
|
|
||||||
|
public class UpdateFileConfiguration : ICommand
|
||||||
|
{
|
||||||
|
public UpdateFileConfiguration(FileConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileConfiguration Configuration { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
103
src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs
Normal file
103
src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
namespace Ocelot.Tracing.Butterfly
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Butterfly.Client.AspNetCore;
|
||||||
|
using global::Butterfly.Client.Tracing;
|
||||||
|
using global::Butterfly.OpenTracing;
|
||||||
|
using Infrastructure.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public class ButterflyTracer : DelegatingHandler, Logging.ITracer
|
||||||
|
{
|
||||||
|
private readonly IServiceTracer _tracer;
|
||||||
|
private const string PrefixSpanId = "ot-spanId";
|
||||||
|
|
||||||
|
public ButterflyTracer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
_tracer = services.GetService<IServiceTracer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Event(HttpContext httpContext, string @event)
|
||||||
|
{
|
||||||
|
// todo - if the user isnt using tracing the code gets here and will blow up on
|
||||||
|
// _tracer.Tracer.TryExtract..
|
||||||
|
if (_tracer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = httpContext.GetSpan();
|
||||||
|
|
||||||
|
if (span == null)
|
||||||
|
{
|
||||||
|
var spanBuilder = new SpanBuilder($"server {httpContext.Request.Method} {httpContext.Request.Path}");
|
||||||
|
if (_tracer.Tracer.TryExtract(out var spanContext, httpContext.Request.Headers, (c, k) => c[k].GetValue(),
|
||||||
|
c => c.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.GetValue())).GetEnumerator()))
|
||||||
|
{
|
||||||
|
spanBuilder.AsChildOf(spanContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
span = _tracer.Start(spanBuilder);
|
||||||
|
httpContext.SetSpan(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
span?.Log(LogField.CreateNew().Event(@event));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HttpResponseMessage> SendAsync(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Action<string> addTraceIdToRepo,
|
||||||
|
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> baseSendAsync)
|
||||||
|
{
|
||||||
|
return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken, addTraceIdToRepo, baseSendAsync));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task<HttpResponseMessage> TracingSendAsync(
|
||||||
|
ISpan span,
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Action<string> addTraceIdToRepo,
|
||||||
|
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> baseSendAsync)
|
||||||
|
{
|
||||||
|
if (request.Headers.Contains(PrefixSpanId))
|
||||||
|
{
|
||||||
|
request.Headers.Remove(PrefixSpanId);
|
||||||
|
request.Headers.TryAddWithoutValidation(PrefixSpanId, span.SpanContext.SpanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTraceIdToRepo(span.SpanContext.TraceId);
|
||||||
|
|
||||||
|
span.Tags.Client().Component("HttpClient")
|
||||||
|
.HttpMethod(request.Method.Method)
|
||||||
|
.HttpUrl(request.RequestUri.OriginalString)
|
||||||
|
.HttpHost(request.RequestUri.Host)
|
||||||
|
.HttpPath(request.RequestUri.PathAndQuery)
|
||||||
|
.PeerAddress(request.RequestUri.OriginalString)
|
||||||
|
.PeerHostName(request.RequestUri.Host)
|
||||||
|
.PeerPort(request.RequestUri.Port);
|
||||||
|
|
||||||
|
_tracer.Tracer.Inject(span.SpanContext, request.Headers, (c, k, v) =>
|
||||||
|
{
|
||||||
|
if (!c.Contains(k))
|
||||||
|
{
|
||||||
|
c.Add(k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
span.Log(LogField.CreateNew().ClientSend());
|
||||||
|
|
||||||
|
var responseMessage = await baseSendAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
span.Log(LogField.CreateNew().ClientReceive());
|
||||||
|
|
||||||
|
return responseMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj
Normal file
37
src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
|
||||||
|
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||||
|
<Description>This package provides methods to integrate Butterfly tracing with Ocelot.</Description>
|
||||||
|
<AssemblyTitle>Ocelot.Tracing.Butterfly</AssemblyTitle>
|
||||||
|
<VersionPrefix>0.0.0-dev</VersionPrefix>
|
||||||
|
<AssemblyName>Ocelot.Tracing.Butterfly</AssemblyName>
|
||||||
|
<PackageId>Ocelot.Tracing.Butterfly</PackageId>
|
||||||
|
<PackageTags>API Gateway;.NET core; Butterfly; ButterflyAPM</PackageTags>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>
|
||||||
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||||
|
<Authors>Tom Pallister</Authors>
|
||||||
|
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||||
|
<RootNamespace>Ocelot.Tracing.Butterfly</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>True</DebugSymbols>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Butterfly.Client" Version="0.0.8" />
|
||||||
|
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
18
src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs
Normal file
18
src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Ocelot.Tracing.Butterfly
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using DependencyInjection;
|
||||||
|
using global::Butterfly.Client.AspNetCore;
|
||||||
|
using Logging;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public static class OcelotBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IOcelotBuilder AddButterfly(this IOcelotBuilder builder, Action<ButterflyOptions> settings)
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<ITracer, ButterflyTracer>();
|
||||||
|
builder.Services.AddButterfly(settings);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,8 +10,8 @@
|
|||||||
<AssemblyName>Ocelot</AssemblyName>
|
<AssemblyName>Ocelot</AssemblyName>
|
||||||
<PackageId>Ocelot</PackageId>
|
<PackageId>Ocelot</PackageId>
|
||||||
<PackageTags>API Gateway;.NET core</PackageTags>
|
<PackageTags>API Gateway;.NET core</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/TomPallister/Ocelot</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>
|
||||||
<PackageProjectUrl>https://github.com/TomPallister/Ocelot</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>
|
||||||
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
|
||||||
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
|
||||||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||||
|
259
test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs
Normal file
259
test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using Butterfly.Client.AspNetCore;
|
||||||
|
using Configuration.File;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Rafty.Infrastructure;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
public class ButterflyTracingTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _serviceOneBuilder;
|
||||||
|
private IWebHost _serviceTwoBuilder;
|
||||||
|
private IWebHost _fakeButterfly;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private string _downstreamPathOne;
|
||||||
|
private string _downstreamPathTwo;
|
||||||
|
private int _butterflyCalled;
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public ButterflyTracingTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_forward_tracing_information_from_ocelot_and_downstream_services()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/values",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51887,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/api001/values",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||||
|
{
|
||||||
|
UseTracing = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/values",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51388,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/api002/values",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||||
|
{
|
||||||
|
UseTracing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var butterflyUrl = "http://localhost:9618";
|
||||||
|
|
||||||
|
this.Given(x => GivenFakeButterfly(butterflyUrl))
|
||||||
|
.And(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl))
|
||||||
|
.And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||||
|
.BDDfy();
|
||||||
|
|
||||||
|
var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => _butterflyCalled >= 4);
|
||||||
|
|
||||||
|
_output.WriteLine($"_butterflyCalled is {_butterflyCalled}");
|
||||||
|
|
||||||
|
commandOnAllStateMachines.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_tracing_header()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/values",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51387,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/api001/values",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||||
|
{
|
||||||
|
UseTracing = true
|
||||||
|
},
|
||||||
|
DownstreamHeaderTransform = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Trace-Id", "{TraceId}"},
|
||||||
|
{"Tom", "Laura"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var butterflyUrl = "http://localhost:9618";
|
||||||
|
|
||||||
|
this.Given(x => GivenFakeButterfly(butterflyUrl))
|
||||||
|
.And(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id"))
|
||||||
|
.And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl)
|
||||||
|
{
|
||||||
|
_serviceOneBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.ConfigureServices(services => {
|
||||||
|
services.AddButterfly(option =>
|
||||||
|
{
|
||||||
|
option.CollectorUrl = butterflyUrl;
|
||||||
|
option.Service = "Service One";
|
||||||
|
option.IgnoredRoutesRegexPatterns = new string[0];
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase(basePath);
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
_downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||||
|
|
||||||
|
if(_downstreamPathOne != basePath)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_serviceOneBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFakeButterfly(string baseUrl)
|
||||||
|
{
|
||||||
|
_fakeButterfly = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
_butterflyCalled++;
|
||||||
|
await context.Response.WriteAsync("OK...");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_fakeButterfly.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl)
|
||||||
|
{
|
||||||
|
_serviceTwoBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.ConfigureServices(services => {
|
||||||
|
services.AddButterfly(option =>
|
||||||
|
{
|
||||||
|
option.CollectorUrl = butterflyUrl;
|
||||||
|
option.Service = "Service Two";
|
||||||
|
option.IgnoredRoutesRegexPatterns = new string[0];
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase(basePath);
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
_downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||||
|
|
||||||
|
if(_downstreamPathTwo != basePath)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_serviceTwoBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceOneBuilder?.Dispose();
|
||||||
|
_serviceTwoBuilder?.Dispose();
|
||||||
|
_fakeButterfly?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
137
test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs
Normal file
137
test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests.Caching
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using CacheManager.Core;
|
||||||
|
using CacheManager.Core.Internal;
|
||||||
|
using CacheManager.Core.Logging;
|
||||||
|
using CacheManager.Core.Utility;
|
||||||
|
|
||||||
|
public class InMemoryJsonHandle<TCacheValue> : BaseCacheHandle<TCacheValue>
|
||||||
|
{
|
||||||
|
private readonly ICacheSerializer _serializer;
|
||||||
|
private readonly ConcurrentDictionary<string, Tuple<Type, byte[]>> _cache;
|
||||||
|
|
||||||
|
public InMemoryJsonHandle(
|
||||||
|
ICacheManagerConfiguration managerConfiguration,
|
||||||
|
CacheHandleConfiguration configuration,
|
||||||
|
ICacheSerializer serializer,
|
||||||
|
ILoggerFactory loggerFactory) : base(managerConfiguration, configuration)
|
||||||
|
{
|
||||||
|
_cache = new ConcurrentDictionary<string, Tuple<Type, byte[]>>();
|
||||||
|
_serializer = serializer;
|
||||||
|
Logger = loggerFactory.CreateLogger(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Count => _cache.Count;
|
||||||
|
|
||||||
|
protected override ILogger Logger { get; }
|
||||||
|
|
||||||
|
public override void Clear() => _cache.Clear();
|
||||||
|
|
||||||
|
public override void ClearRegion(string region)
|
||||||
|
{
|
||||||
|
Guard.NotNullOrWhiteSpace(region, nameof(region));
|
||||||
|
|
||||||
|
var key = string.Concat(region, ":");
|
||||||
|
foreach (var item in _cache.Where(p => p.Key.StartsWith(key, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_cache.TryRemove(item.Key, out Tuple<Type, byte[]> val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Exists(string key)
|
||||||
|
{
|
||||||
|
Guard.NotNullOrWhiteSpace(key, nameof(key));
|
||||||
|
|
||||||
|
return _cache.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Exists(string key, string region)
|
||||||
|
{
|
||||||
|
Guard.NotNullOrWhiteSpace(region, nameof(region));
|
||||||
|
var fullKey = GetKey(key, region);
|
||||||
|
return _cache.ContainsKey(fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool AddInternalPrepared(CacheItem<TCacheValue> item)
|
||||||
|
{
|
||||||
|
Guard.NotNull(item, nameof(item));
|
||||||
|
|
||||||
|
var key = GetKey(item.Key, item.Region);
|
||||||
|
|
||||||
|
var serializedItem = _serializer.SerializeCacheItem(item);
|
||||||
|
|
||||||
|
return _cache.TryAdd(key, new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CacheItem<TCacheValue> GetCacheItemInternal(string key) => GetCacheItemInternal(key, null);
|
||||||
|
|
||||||
|
protected override CacheItem<TCacheValue> GetCacheItemInternal(string key, string region)
|
||||||
|
{
|
||||||
|
var fullKey = GetKey(key, region);
|
||||||
|
|
||||||
|
CacheItem<TCacheValue> deserializedResult = null;
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(fullKey, out Tuple<Type, byte[]> result))
|
||||||
|
{
|
||||||
|
deserializedResult = _serializer.DeserializeCacheItem<TCacheValue>(result.Item2, result.Item1);
|
||||||
|
|
||||||
|
if (deserializedResult.ExpirationMode != ExpirationMode.None && IsExpired(deserializedResult, DateTime.UtcNow))
|
||||||
|
{
|
||||||
|
_cache.TryRemove(fullKey, out Tuple<Type, byte[]> removeResult);
|
||||||
|
TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, deserializedResult.Value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PutInternalPrepared(CacheItem<TCacheValue> item)
|
||||||
|
{
|
||||||
|
Guard.NotNull(item, nameof(item));
|
||||||
|
|
||||||
|
var serializedItem = _serializer.SerializeCacheItem<TCacheValue>(item);
|
||||||
|
|
||||||
|
_cache[GetKey(item.Key, item.Region)] = new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool RemoveInternal(string key) => RemoveInternal(key, null);
|
||||||
|
|
||||||
|
protected override bool RemoveInternal(string key, string region)
|
||||||
|
{
|
||||||
|
var fullKey = GetKey(key, region);
|
||||||
|
return _cache.TryRemove(fullKey, out Tuple<Type, byte[]> val);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetKey(string key, string region)
|
||||||
|
{
|
||||||
|
Guard.NotNullOrWhiteSpace(key, nameof(key));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(region))
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Concat(region, ":", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsExpired(CacheItem<TCacheValue> item, DateTime now)
|
||||||
|
{
|
||||||
|
if (item.ExpirationMode == ExpirationMode.Absolute
|
||||||
|
&& item.CreatedUtc.Add(item.ExpirationTimeout) < now)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (item.ExpirationMode == ExpirationMode.Sliding
|
||||||
|
&& item.LastAccessedUtc.Add(item.ExpirationTimeout) < now)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
225
test/Ocelot.AcceptanceTests/CachingTests.cs
Normal file
225
test/Ocelot.AcceptanceTests/CachingTests.cs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using Configuration.File;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class CachingTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public CachingTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_cached_response()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51899,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.And(x => _steps.ThenTheContentLengthIs(16))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_cached_response_with_expires_header()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 52839,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom"))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.And(x => _steps.ThenTheContentLengthIs(16))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_cached_response_when_using_jsonserialized_cache()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51899,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_return_cached_response_as_ttl_expires()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51899,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||||
|
.And(x => x.GivenTheCacheExpires())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheCacheExpires()
|
||||||
|
{
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_serviceHandler.Dispose();
|
||||||
|
GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
context.Response.Headers.Add(key, value);
|
||||||
|
}
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs
Normal file
176
test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Configuration.File;
|
||||||
|
using Consul;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ConfigurationInConsulTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _builder;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private IWebHost _fakeConsulBuilder;
|
||||||
|
private FileConfiguration _config;
|
||||||
|
private readonly List<ServiceEntry> _consulServices;
|
||||||
|
|
||||||
|
public ConfigurationInConsulTests()
|
||||||
|
{
|
||||||
|
_consulServices = new List<ServiceEntry>();
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51779,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 9502
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var fakeConsulServiceDiscoveryUrl = "http://localhost:9502";
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_fakeConsulBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_config);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
|
||||||
|
var kvp = new FakeConsulGetResponse(base64);
|
||||||
|
|
||||||
|
await context.Response.WriteJsonAsync(new FakeConsulGetResponse[] { kvp });
|
||||||
|
}
|
||||||
|
else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reader = new StreamReader(context.Request.Body);
|
||||||
|
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
_config = JsonConvert.DeserializeObject<FileConfiguration>(json);
|
||||||
|
|
||||||
|
var response = JsonConvert.SerializeObject(true);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(response);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||||
|
{
|
||||||
|
await context.Response.WriteJsonAsync(_consulServices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_fakeConsulBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FakeConsulGetResponse
|
||||||
|
{
|
||||||
|
public FakeConsulGetResponse(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CreateIndex => 100;
|
||||||
|
public int ModifyIndex => 200;
|
||||||
|
public int LockIndex => 200;
|
||||||
|
public string Key => "InternalConfiguration";
|
||||||
|
public int Flags => 0;
|
||||||
|
public string Value { get; private set; }
|
||||||
|
public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_builder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase(basePath);
|
||||||
|
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_builder?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
470
test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs
Normal file
470
test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Cache;
|
||||||
|
using Configuration.File;
|
||||||
|
using Consul;
|
||||||
|
using Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ConsulConfigurationInConsulTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _builder;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private IWebHost _fakeConsulBuilder;
|
||||||
|
private FileConfiguration _config;
|
||||||
|
private readonly List<ServiceEntry> _consulServices;
|
||||||
|
|
||||||
|
public ConsulConfigurationInConsulTests()
|
||||||
|
{
|
||||||
|
_consulServices = new List<ServiceEntry>();
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_with_simple_url()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51779,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 9500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var fakeConsulServiceDiscoveryUrl = "http://localhost:9500";
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_load_configuration_out_of_consul()
|
||||||
|
{
|
||||||
|
var consulPort = 8500;
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
|
||||||
|
var consulConfig = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/status",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51779,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/cs/status",
|
||||||
|
UpstreamHttpMethod = new List<string> {"Get"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||||
|
.And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "/status", 200, "Hello from Laura"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_load_configuration_out_of_consul_if_it_is_changed()
|
||||||
|
{
|
||||||
|
var consulPort = 8506;
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
|
||||||
|
var consulConfig = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/status",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51780,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/cs/status",
|
||||||
|
UpstreamHttpMethod = new List<string> {"Get"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var secondConsulConfig = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/status",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51780,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/cs/status/awesome",
|
||||||
|
UpstreamHttpMethod = new List<string> {"Get"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||||
|
.And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51780", "/status", 200, "Hello from Laura"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.When(x => GivenTheConsulConfigurationIs(secondConsulConfig))
|
||||||
|
.Then(x => ThenTheConfigIsUpdatedInOcelot())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit()
|
||||||
|
{
|
||||||
|
const int consulPort = 8523;
|
||||||
|
const string serviceName = "web";
|
||||||
|
const int downstreamServicePort = 8187;
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = downstreamServicePort,
|
||||||
|
ID = "web_90_0_2_224_8080",
|
||||||
|
Tags = new[] { "version-v1" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var consulConfig = new FileConfiguration
|
||||||
|
{
|
||||||
|
DynamicReRoutes = new List<FileDynamicReRoute>
|
||||||
|
{
|
||||||
|
new FileDynamicReRoute
|
||||||
|
{
|
||||||
|
ServiceName = serviceName,
|
||||||
|
RateLimitRule = new FileRateLimitRule()
|
||||||
|
{
|
||||||
|
EnableRateLimiting = true,
|
||||||
|
ClientWhitelist = new List<string>(),
|
||||||
|
Limit = 3,
|
||||||
|
Period = "1s",
|
||||||
|
PeriodTimespan = 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
},
|
||||||
|
RateLimitOptions = new FileRateLimitOptions()
|
||||||
|
{
|
||||||
|
ClientIdHeader = "ClientId",
|
||||||
|
DisableRateLimitHeaders = false,
|
||||||
|
QuotaExceededMessage = "",
|
||||||
|
RateLimitCounterPrefix = "",
|
||||||
|
HttpStatusCode = 428
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura"))
|
||||||
|
.And(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(428))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigIsUpdatedInOcelot()
|
||||||
|
{
|
||||||
|
var result = Wait.WaitFor(20000).Until(() => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome");
|
||||||
|
_steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
|
||||||
|
_steps.ThenTheResponseBodyShouldBe("Hello from Laura");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheConsulConfigurationIs(FileConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||||
|
{
|
||||||
|
foreach (var serviceEntry in serviceEntries)
|
||||||
|
{
|
||||||
|
_consulServices.Add(serviceEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_fakeConsulBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_config);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
|
||||||
|
var kvp = new FakeConsulGetResponse(base64);
|
||||||
|
json = JsonConvert.SerializeObject(new FakeConsulGetResponse[] { kvp });
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reader = new StreamReader(context.Request.Body);
|
||||||
|
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
_config = JsonConvert.DeserializeObject<FileConfiguration>(json);
|
||||||
|
|
||||||
|
var response = JsonConvert.SerializeObject(true);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(response);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_consulServices);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_fakeConsulBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FakeConsulGetResponse
|
||||||
|
{
|
||||||
|
public FakeConsulGetResponse(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CreateIndex => 100;
|
||||||
|
public int ModifyIndex => 200;
|
||||||
|
public int LockIndex => 200;
|
||||||
|
public string Key => "InternalConfiguration";
|
||||||
|
public int Flags => 0;
|
||||||
|
public string Value { get; private set; }
|
||||||
|
public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_builder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase(basePath);
|
||||||
|
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_builder?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeCache : IOcelotCache<FileConfiguration>
|
||||||
|
{
|
||||||
|
public void Add(string key, FileConfiguration value, TimeSpan ttl, string region)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileConfiguration Get(string key, string region)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearRegion(string region)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
339
test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs
Normal file
339
test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.File;
|
||||||
|
using Consul;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ConsulWebSocketTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<string> _secondRecieved;
|
||||||
|
private readonly List<string> _firstRecieved;
|
||||||
|
private readonly List<ServiceEntry> _serviceEntries;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public ConsulWebSocketTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
_firstRecieved = new List<string>();
|
||||||
|
_secondRecieved = new List<string>();
|
||||||
|
_serviceEntries = new List<ServiceEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_proxy_websocket_input_to_downstream_service_and_use_service_discovery_and_load_balancer()
|
||||||
|
{
|
||||||
|
var downstreamPort = 5007;
|
||||||
|
var downstreamHost = "localhost";
|
||||||
|
|
||||||
|
var secondDownstreamPort = 5008;
|
||||||
|
var secondDownstreamHost = "localhost";
|
||||||
|
|
||||||
|
var serviceName = "websockets";
|
||||||
|
var consulPort = 8509;
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = downstreamHost,
|
||||||
|
Port = downstreamPort,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = secondDownstreamHost,
|
||||||
|
Port = secondDownstreamPort,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var config = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
DownstreamPathTemplate = "/ws",
|
||||||
|
DownstreamScheme = "ws",
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort,
|
||||||
|
Type = "consul"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(_ => _steps.GivenThereIsAConfiguration(config))
|
||||||
|
.And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul())
|
||||||
|
.And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws"))
|
||||||
|
.And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws"))
|
||||||
|
.When(_ => WhenIStartTheClients())
|
||||||
|
.Then(_ => ThenBothDownstreamServicesAreCalled())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenBothDownstreamServicesAreCalled()
|
||||||
|
{
|
||||||
|
_firstRecieved.Count.ShouldBe(10);
|
||||||
|
_firstRecieved.ForEach(x =>
|
||||||
|
{
|
||||||
|
x.ShouldBe("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
_secondRecieved.Count.ShouldBe(10);
|
||||||
|
_secondRecieved.ForEach(x =>
|
||||||
|
{
|
||||||
|
x.ShouldBe("chocolate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||||
|
{
|
||||||
|
foreach (var serviceEntry in serviceEntries)
|
||||||
|
{
|
||||||
|
_serviceEntries.Add(serviceEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_serviceEntries);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenIStartTheClients()
|
||||||
|
{
|
||||||
|
var firstClient = StartClient("ws://localhost:5000/");
|
||||||
|
|
||||||
|
var secondClient = StartSecondClient("ws://localhost:5000/");
|
||||||
|
|
||||||
|
await Task.WhenAll(firstClient, secondClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartClient(string url)
|
||||||
|
{
|
||||||
|
var client = new ClientWebSocket();
|
||||||
|
|
||||||
|
await client.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||||
|
|
||||||
|
var sending = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
string line = "test";
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(line);
|
||||||
|
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true,
|
||||||
|
CancellationToken.None);
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
});
|
||||||
|
|
||||||
|
var receiving = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
_firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||||
|
}
|
||||||
|
else if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(sending, receiving);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartSecondClient(string url)
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
var client = new ClientWebSocket();
|
||||||
|
|
||||||
|
await client.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||||
|
|
||||||
|
var sending = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
string line = "test";
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(line);
|
||||||
|
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true,
|
||||||
|
CancellationToken.None);
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
});
|
||||||
|
|
||||||
|
var receiving = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
_secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||||
|
}
|
||||||
|
else if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(sending, receiving);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartFakeDownstreamService(string url, string path)
|
||||||
|
{
|
||||||
|
await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path == path)
|
||||||
|
{
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await Echo(webSocket);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartSecondFakeDownstreamService(string url, string path)
|
||||||
|
{
|
||||||
|
await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path == path)
|
||||||
|
{
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await Message(webSocket);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Echo(WebSocket webSocket)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
while (!result.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
|
||||||
|
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Message(WebSocket webSocket)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes("chocolate");
|
||||||
|
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
while (!result.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None);
|
||||||
|
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
283
test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs
Normal file
283
test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using Configuration.File;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class EurekaServiceDiscoveryTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private readonly List<IServiceInstance> _eurekaInstances;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public EurekaServiceDiscoveryTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
_eurekaInstances = new List<IServiceInstance>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_eureka_service_discovery_and_make_request()
|
||||||
|
{
|
||||||
|
var eurekaPort = 8761;
|
||||||
|
var serviceName = "product";
|
||||||
|
var downstreamServicePort = 50371;
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||||
|
var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}";
|
||||||
|
|
||||||
|
var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false,
|
||||||
|
new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary<string, string>());
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Type = "Eureka"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl))
|
||||||
|
.And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithEureka())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests)))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances)
|
||||||
|
{
|
||||||
|
foreach (var instance in serviceInstances)
|
||||||
|
{
|
||||||
|
_eurekaInstances.Add(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.Value == "/eureka/apps/")
|
||||||
|
{
|
||||||
|
var apps = new List<Application>();
|
||||||
|
|
||||||
|
foreach (var serviceInstance in _eurekaInstances)
|
||||||
|
{
|
||||||
|
var a = new Application
|
||||||
|
{
|
||||||
|
name = serviceName,
|
||||||
|
instance = new List<Instance>
|
||||||
|
{
|
||||||
|
new Instance
|
||||||
|
{
|
||||||
|
instanceId = $"{serviceInstance.Host}:{serviceInstance}",
|
||||||
|
hostName = serviceInstance.Host,
|
||||||
|
app = serviceName,
|
||||||
|
ipAddr = "127.0.0.1",
|
||||||
|
status = "UP",
|
||||||
|
overriddenstatus = "UNKNOWN",
|
||||||
|
port = new Port {value = serviceInstance.Port, enabled = "true"},
|
||||||
|
securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"},
|
||||||
|
countryId = 1,
|
||||||
|
dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"},
|
||||||
|
leaseInfo = new LeaseInfo
|
||||||
|
{
|
||||||
|
renewalIntervalInSecs = 30,
|
||||||
|
durationInSecs = 90,
|
||||||
|
registrationTimestamp = 1457714988223,
|
||||||
|
lastRenewalTimestamp= 1457716158319,
|
||||||
|
evictionTimestamp = 0,
|
||||||
|
serviceUpTimestamp = 1457714988223
|
||||||
|
},
|
||||||
|
metadata = new Metadata
|
||||||
|
{
|
||||||
|
value = "java.util.Collections$EmptyMap"
|
||||||
|
},
|
||||||
|
homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||||
|
statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||||
|
healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||||
|
vipAddress = serviceName,
|
||||||
|
isCoordinatingDiscoveryServer = "false",
|
||||||
|
lastUpdatedTimestamp = "1457714988223",
|
||||||
|
lastDirtyTimestamp = "1457714988172",
|
||||||
|
actionType = "ADDED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.Add(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
var applications = new EurekaApplications
|
||||||
|
{
|
||||||
|
applications = new Applications
|
||||||
|
{
|
||||||
|
application = apps,
|
||||||
|
apps__hashcode = "UP_1_",
|
||||||
|
versions__delta = "1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(applications);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenEurekaProductServiceOneIsRunning(string url)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(exception.StackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FakeEurekaService : IServiceInstance
|
||||||
|
{
|
||||||
|
public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary<string, string> metadata)
|
||||||
|
{
|
||||||
|
ServiceId = serviceId;
|
||||||
|
Host = host;
|
||||||
|
Port = port;
|
||||||
|
IsSecure = isSecure;
|
||||||
|
Uri = uri;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ServiceId { get; }
|
||||||
|
public string Host { get; }
|
||||||
|
public int Port { get; }
|
||||||
|
public bool IsSecure { get; }
|
||||||
|
public Uri Uri { get; }
|
||||||
|
public IDictionary<string, string> Metadata { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Port
|
||||||
|
{
|
||||||
|
[JsonProperty("$")]
|
||||||
|
public int value { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("@enabled")]
|
||||||
|
public string enabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecurePort
|
||||||
|
{
|
||||||
|
[JsonProperty("$")]
|
||||||
|
public int value { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("@enabled")]
|
||||||
|
public string enabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DataCenterInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("@class")]
|
||||||
|
public string value { get; set; }
|
||||||
|
|
||||||
|
public string name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LeaseInfo
|
||||||
|
{
|
||||||
|
public int renewalIntervalInSecs { get; set; }
|
||||||
|
|
||||||
|
public int durationInSecs { get; set; }
|
||||||
|
|
||||||
|
public long registrationTimestamp { get; set; }
|
||||||
|
|
||||||
|
public long lastRenewalTimestamp { get; set; }
|
||||||
|
|
||||||
|
public int evictionTimestamp { get; set; }
|
||||||
|
|
||||||
|
public long serviceUpTimestamp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Metadata
|
||||||
|
{
|
||||||
|
[JsonProperty("@class")]
|
||||||
|
public string value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Instance
|
||||||
|
{
|
||||||
|
public string instanceId { get; set; }
|
||||||
|
public string hostName { get; set; }
|
||||||
|
public string app { get; set; }
|
||||||
|
public string ipAddr { get; set; }
|
||||||
|
public string status { get; set; }
|
||||||
|
public string overriddenstatus { get; set; }
|
||||||
|
public Port port { get; set; }
|
||||||
|
public SecurePort securePort { get; set; }
|
||||||
|
public int countryId { get; set; }
|
||||||
|
public DataCenterInfo dataCenterInfo { get; set; }
|
||||||
|
public LeaseInfo leaseInfo { get; set; }
|
||||||
|
public Metadata metadata { get; set; }
|
||||||
|
public string homePageUrl { get; set; }
|
||||||
|
public string statusPageUrl { get; set; }
|
||||||
|
public string healthCheckUrl { get; set; }
|
||||||
|
public string vipAddress { get; set; }
|
||||||
|
public string isCoordinatingDiscoveryServer { get; set; }
|
||||||
|
public string lastUpdatedTimestamp { get; set; }
|
||||||
|
public string lastDirtyTimestamp { get; set; }
|
||||||
|
public string actionType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Application
|
||||||
|
{
|
||||||
|
public string name { get; set; }
|
||||||
|
public List<Instance> instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Applications
|
||||||
|
{
|
||||||
|
public string versions__delta { get; set; }
|
||||||
|
public string apps__hashcode { get; set; }
|
||||||
|
public List<Application> application { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EurekaApplications
|
||||||
|
{
|
||||||
|
public Applications applications { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,12 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
|
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj" />
|
||||||
<ProjectReference Include="..\Ocelot.ManualTest\Ocelot.ManualTest.csproj" />
|
<ProjectReference Include="..\Ocelot.ManualTest\Ocelot.ManualTest.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -54,5 +59,10 @@
|
|||||||
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
|
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
|
||||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
|
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
|
||||||
<PackageReference Include="IdentityServer4" Version="2.2.0" />
|
<PackageReference Include="IdentityServer4" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Consul" Version="0.7.2.6" />
|
||||||
|
<PackageReference Include="Rafty" Version="0.4.4" />
|
||||||
|
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.2" />
|
||||||
|
<PackageReference Include="CacheManager.Serialization.Json" Version="1.1.2" />
|
||||||
|
<PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
274
test/Ocelot.AcceptanceTests/PollyQoSTests.cs
Normal file
274
test/Ocelot.AcceptanceTests/PollyQoSTests.cs
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration.File;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class PollyQoSTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private int _requestCount;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public PollyQoSTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_timeout()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51569,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Post" },
|
||||||
|
QoSOptions = new FileQoSOptions
|
||||||
|
{
|
||||||
|
TimeoutValue = 1000,
|
||||||
|
ExceptionsAllowedBeforeBreaking = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||||
|
.And(x => _steps.GivenThePostHasContent("postContent"))
|
||||||
|
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_timeout()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51579,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Post" },
|
||||||
|
QoSOptions = new FileQoSOptions
|
||||||
|
{
|
||||||
|
TimeoutValue = 10,
|
||||||
|
ExceptionsAllowedBeforeBreaking = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||||
|
.And(x => _steps.GivenThePostHasContent("postContent"))
|
||||||
|
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_open_circuit_breaker_then_close()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51892,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
QoSOptions = new FileQoSOptions
|
||||||
|
{
|
||||||
|
ExceptionsAllowedBeforeBreaking = 1,
|
||||||
|
TimeoutValue = 500,
|
||||||
|
DurationOfBreak = 1000
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura"))
|
||||||
|
.Given(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.Given(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.Given(x => x.GivenIWaitMilliseconds(3000))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void open_circuit_should_not_effect_different_reRoute()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51872,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
QoSOptions = new FileQoSOptions
|
||||||
|
{
|
||||||
|
ExceptionsAllowedBeforeBreaking = 1,
|
||||||
|
TimeoutValue = 500,
|
||||||
|
DurationOfBreak = 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 51880,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/working",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura"))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom", 0))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/working"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||||
|
.And(x => x.GivenIWaitMilliseconds(3000))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIWaitMilliseconds(int ms)
|
||||||
|
{
|
||||||
|
Thread.Sleep(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
//circuit starts closed
|
||||||
|
if (_requestCount == 0)
|
||||||
|
{
|
||||||
|
_requestCount++;
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//request one times out and polly throws exception, circuit opens
|
||||||
|
if (_requestCount == 1)
|
||||||
|
{
|
||||||
|
_requestCount++;
|
||||||
|
await Task.Delay(1000);
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//after break closes we return 200 OK
|
||||||
|
if (_requestCount == 2)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(timeout);
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
583
test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs
Normal file
583
test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using Configuration.File;
|
||||||
|
using Consul;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ServiceDiscoveryTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private readonly List<ServiceEntry> _consulServices;
|
||||||
|
private int _counterOne;
|
||||||
|
private int _counterTwo;
|
||||||
|
private static readonly object SyncLock = new object();
|
||||||
|
private string _downstreamPath;
|
||||||
|
private string _receivedToken;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public ServiceDiscoveryTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
_consulServices = new List<ServiceEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_consul_service_discovery_and_load_balance_request()
|
||||||
|
{
|
||||||
|
var consulPort = 8502;
|
||||||
|
var serviceName = "product";
|
||||||
|
var downstreamServiceOneUrl = "http://localhost:50881";
|
||||||
|
var downstreamServiceTwoUrl = "http://localhost:50882";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 50881,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 50882,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||||
|
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50))
|
||||||
|
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50))
|
||||||
|
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_handle_request_to_consul_for_downstream_service_and_make_request()
|
||||||
|
{
|
||||||
|
const int consulPort = 8505;
|
||||||
|
const string serviceName = "web";
|
||||||
|
const string downstreamServiceOneUrl = "http://localhost:8080";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 8080,
|
||||||
|
ID = "web_90_0_2_224_8080",
|
||||||
|
Tags = new[] { "version-v1" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/home",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/home",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/home"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes()
|
||||||
|
{
|
||||||
|
const int consulPort = 8513;
|
||||||
|
const string serviceName = "web";
|
||||||
|
const int downstreamServicePort = 8087;
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = downstreamServicePort,
|
||||||
|
ID = "web_90_0_2_224_8080",
|
||||||
|
Tags = new[] { "version-v1" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = true,
|
||||||
|
UseCookieContainer = true,
|
||||||
|
UseTracing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura"))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes()
|
||||||
|
{
|
||||||
|
var consulPort = 8510;
|
||||||
|
var serviceName = "product";
|
||||||
|
var serviceOnePort = 50888;
|
||||||
|
var serviceTwoPort = 50889;
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}";
|
||||||
|
var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = serviceOnePort,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = serviceTwoPort,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
},
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
DownstreamScheme = "http"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||||
|
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50))
|
||||||
|
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50))
|
||||||
|
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_token_to_make_request_to_consul()
|
||||||
|
{
|
||||||
|
var token = "abctoken";
|
||||||
|
var consulPort = 8515;
|
||||||
|
var serviceName = "web";
|
||||||
|
var downstreamServiceOneUrl = "http://localhost:8081";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 8081,
|
||||||
|
ID = "web_90_0_2_224_8080",
|
||||||
|
Tags = new[] { "version-v1" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/home",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/home",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort,
|
||||||
|
Token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||||
|
.And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.And(_ => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(_ => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home"))
|
||||||
|
.Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.And(_ => _receivedToken.ShouldBe(token))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_send_request_to_service_after_it_becomes_available_in_consul()
|
||||||
|
{
|
||||||
|
var consulPort = 8501;
|
||||||
|
var serviceName = "product";
|
||||||
|
var downstreamServiceOneUrl = "http://localhost:50879";
|
||||||
|
var downstreamServiceTwoUrl = "http://localhost:50880";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
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",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||||
|
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||||
|
.And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10))
|
||||||
|
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6))
|
||||||
|
.And(x => WhenIRemoveAService(serviceEntryTwo))
|
||||||
|
.And(x => GivenIResetCounters())
|
||||||
|
.And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||||
|
.And(x => ThenOnlyOneServiceHasBeenCalled())
|
||||||
|
.And(x => WhenIAddAServiceBackIn(serviceEntryTwo))
|
||||||
|
.And(x => GivenIResetCounters())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||||
|
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10))
|
||||||
|
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request()
|
||||||
|
{
|
||||||
|
const int consulPort = 8518;
|
||||||
|
const string serviceName = "web";
|
||||||
|
const int downstreamServicePort = 8082;
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = downstreamServicePort,
|
||||||
|
ID = $"web_90_0_2_224_{downstreamServicePort}",
|
||||||
|
Tags = new[] { "version-v1" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/home",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/home",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||||
|
ServiceName = serviceName,
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort,
|
||||||
|
Type = "PollConsul",
|
||||||
|
PollingInterval = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||||
|
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo)
|
||||||
|
{
|
||||||
|
_consulServices.Add(serviceEntryTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenOnlyOneServiceHasBeenCalled()
|
||||||
|
{
|
||||||
|
_counterOne.ShouldBe(10);
|
||||||
|
_counterTwo.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIRemoveAService(ServiceEntry serviceEntryTwo)
|
||||||
|
{
|
||||||
|
_consulServices.Remove(serviceEntryTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIResetCounters()
|
||||||
|
{
|
||||||
|
_counterOne = 0;
|
||||||
|
_counterTwo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top)
|
||||||
|
{
|
||||||
|
_counterOne.ShouldBeInRange(bottom, top);
|
||||||
|
_counterOne.ShouldBeInRange(bottom, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected)
|
||||||
|
{
|
||||||
|
var total = _counterOne + _counterTwo;
|
||||||
|
total.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||||
|
{
|
||||||
|
foreach (var serviceEntry in serviceEntries)
|
||||||
|
{
|
||||||
|
_consulServices.Add(serviceEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values))
|
||||||
|
{
|
||||||
|
_receivedToken = values.First();
|
||||||
|
}
|
||||||
|
var json = JsonConvert.SerializeObject(_consulServices);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenProductServiceOneIsRunning(string url, int statusCode)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string response;
|
||||||
|
lock (SyncLock)
|
||||||
|
{
|
||||||
|
_counterOne++;
|
||||||
|
response = _counterOne.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(response);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(exception.StackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenProductServiceTwoIsRunning(string url, int statusCode)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string response;
|
||||||
|
lock (SyncLock)
|
||||||
|
{
|
||||||
|
_counterTwo++;
|
||||||
|
response = _counterTwo.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(response);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(exception.StackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||||
|
{
|
||||||
|
_downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||||
|
|
||||||
|
if (_downstreamPath != basePath)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@
|
|||||||
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
|
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Caching;
|
||||||
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
|
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
|
||||||
using Ocelot.Middleware.Multiplexer;
|
using Ocelot.Middleware.Multiplexer;
|
||||||
using static Ocelot.Infrastructure.Wait;
|
using static Ocelot.Infrastructure.Wait;
|
||||||
@ -32,6 +33,13 @@
|
|||||||
using Requester;
|
using Requester;
|
||||||
using CookieHeaderValue = System.Net.Http.Headers.CookieHeaderValue;
|
using CookieHeaderValue = System.Net.Http.Headers.CookieHeaderValue;
|
||||||
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
|
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Ocelot.Cache.CacheManager;
|
||||||
|
using Ocelot.Provider.Consul;
|
||||||
|
using Ocelot.Provider.Eureka;
|
||||||
|
using Ocelot.Infrastructure;
|
||||||
|
using Ocelot.Provider.Polly;
|
||||||
|
using Ocelot.Tracing.Butterfly;
|
||||||
|
|
||||||
public class Steps : IDisposable
|
public class Steps : IDisposable
|
||||||
{
|
{
|
||||||
@ -98,6 +106,42 @@
|
|||||||
await _ocelotHost.StartAsync();
|
await _ocelotHost.StartAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartFakeOcelotWithWebSocketsWithConsul()
|
||||||
|
{
|
||||||
|
_ocelotBuilder = new WebHostBuilder();
|
||||||
|
_ocelotBuilder.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddSingleton(_ocelotBuilder);
|
||||||
|
s.AddOcelot().AddConsul();
|
||||||
|
});
|
||||||
|
_ocelotBuilder.UseKestrel()
|
||||||
|
.UseUrls("http://localhost:5000")
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureLogging((hostingContext, logging) =>
|
||||||
|
{
|
||||||
|
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
|
||||||
|
logging.AddConsole();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseWebSockets();
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
})
|
||||||
|
.UseIISIntegration();
|
||||||
|
_ocelotHost = _ocelotBuilder.Build();
|
||||||
|
await _ocelotHost.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
||||||
{
|
{
|
||||||
var configurationPath = TestConfiguration.ConfigurationPath;
|
var configurationPath = TestConfiguration.ConfigurationPath;
|
||||||
@ -124,6 +168,12 @@
|
|||||||
File.WriteAllText(configurationPath, jsonConfiguration);
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ThenTheResponseBodyHeaderIs(string key, string value)
|
||||||
|
{
|
||||||
|
var header = _response.Content.Headers.GetValues(key);
|
||||||
|
header.First().ShouldBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
public void GivenOcelotIsRunningReloadingConfig(bool shouldReload)
|
public void GivenOcelotIsRunningReloadingConfig(bool shouldReload)
|
||||||
{
|
{
|
||||||
_webHostBuilder = new WebHostBuilder();
|
_webHostBuilder = new WebHostBuilder();
|
||||||
@ -183,6 +233,203 @@
|
|||||||
_ocelotClient = _ocelotServer.CreateClient();
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningWithConsul()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot().AddConsul();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ThenTheTraceHeaderIsSet(string key)
|
||||||
|
{
|
||||||
|
var header = _response.Headers.GetValues(key);
|
||||||
|
header.First().ShouldNotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl)
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddButterfly(option =>
|
||||||
|
{
|
||||||
|
//this is the url that the butterfly collector server is running on...
|
||||||
|
option.CollectorUrl = butterflyUrl;
|
||||||
|
option.Service = "Ocelot";
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
await next.Invoke();
|
||||||
|
});
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddCacheManager((x) =>
|
||||||
|
{
|
||||||
|
x.WithMicrosoftLogging(log =>
|
||||||
|
{
|
||||||
|
log.AddConsole(LogLevel.Debug);
|
||||||
|
})
|
||||||
|
.WithJsonSerializer()
|
||||||
|
.WithHandle(typeof(InMemoryJsonHandle<>));
|
||||||
|
})
|
||||||
|
.AddConsul()
|
||||||
|
.AddConfigStoredInConsul();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningUsingConsulToStoreConfig()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot().AddConsul().AddConfigStoredInConsul();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url)
|
||||||
|
{
|
||||||
|
var result = Wait.WaitFor(2000).Until(() => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_response = _ocelotClient.GetAsync(url).Result;
|
||||||
|
_response.EnsureSuccessStatusCode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningUsingJsonSerializedCache()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddCacheManager((x) =>
|
||||||
|
{
|
||||||
|
x.WithMicrosoftLogging(log =>
|
||||||
|
{
|
||||||
|
log.AddConsole(LogLevel.Debug);
|
||||||
|
})
|
||||||
|
.WithJsonSerializer()
|
||||||
|
.WithHandle(typeof(InMemoryJsonHandle<>));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache)
|
public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache)
|
||||||
{
|
{
|
||||||
_webHostBuilder = new WebHostBuilder();
|
_webHostBuilder = new WebHostBuilder();
|
||||||
@ -578,6 +825,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningWithEureka()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddEureka();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GivenOcelotIsRunningWithPolly()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddPolly();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot()
|
||||||
|
.Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void WhenIGetUrlOnTheApiGateway(string url)
|
public void WhenIGetUrlOnTheApiGateway(string url)
|
||||||
{
|
{
|
||||||
_response = _ocelotClient.GetAsync(url).Result;
|
_response = _ocelotClient.GetAsync(url).Result;
|
||||||
@ -697,6 +1005,11 @@
|
|||||||
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
|
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ThenTheContentLengthIs(int expected)
|
||||||
|
{
|
||||||
|
_response.Content.Headers.ContentLength.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||||
{
|
{
|
||||||
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
154
test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs
Normal file
154
test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using Configuration.File;
|
||||||
|
using Consul;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class TwoDownstreamServicesTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private readonly List<ServiceEntry> _serviceEntries;
|
||||||
|
private string _downstreamPathOne;
|
||||||
|
private string _downstreamPathTwo;
|
||||||
|
private readonly ServiceHandler _serviceHandler;
|
||||||
|
|
||||||
|
public TwoDownstreamServicesTests()
|
||||||
|
{
|
||||||
|
_serviceHandler = new ServiceHandler();
|
||||||
|
_steps = new Steps();
|
||||||
|
_serviceEntries = new List<ServiceEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_fix_issue_194()
|
||||||
|
{
|
||||||
|
var consulPort = 8503;
|
||||||
|
var downstreamServiceOneUrl = "http://localhost:8362";
|
||||||
|
var downstreamServiceTwoUrl = "http://localhost:8330";
|
||||||
|
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/user/{user}",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 8362,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/api/user/{user}",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
},
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/product/{product}",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 8330,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpstreamPathTemplate = "/api/product/{product}",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = consulPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, "/api/user/info", 200, "user"))
|
||||||
|
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, "/api/product/info", 200, "product"))
|
||||||
|
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/user/info?id=1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("user"))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/product/info?id=1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => _steps.ThenTheResponseBodyShouldBe("product"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.Value == "/v1/health/service/product")
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_serviceEntries);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenProductServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||||
|
{
|
||||||
|
_downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value)
|
||||||
|
? context.Request.PathBase.Value
|
||||||
|
: context.Request.Path.Value;
|
||||||
|
|
||||||
|
if (_downstreamPathOne != basePath)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenProductServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||||
|
{
|
||||||
|
_downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||||
|
|
||||||
|
if (_downstreamPathTwo != basePath)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceHandler?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
838
test/Ocelot.IntegrationTests/AdministrationTests.cs
Normal file
838
test/Ocelot.IntegrationTests/AdministrationTests.cs
Normal file
@ -0,0 +1,838 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using IdentityServer4.AccessTokenValidation;
|
||||||
|
using IdentityServer4.Models;
|
||||||
|
using IdentityServer4.Test;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Cache;
|
||||||
|
using Ocelot.Configuration.File;
|
||||||
|
using Ocelot.DependencyInjection;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
using Ocelot.Administration;
|
||||||
|
using Ocelot.IntegrationTests;
|
||||||
|
|
||||||
|
namespace Ocelot.IntegrationTests
|
||||||
|
{
|
||||||
|
public class AdministrationTests : IDisposable
|
||||||
|
{
|
||||||
|
private HttpClient _httpClient;
|
||||||
|
private readonly HttpClient _httpClientTwo;
|
||||||
|
private HttpResponseMessage _response;
|
||||||
|
private IWebHost _builder;
|
||||||
|
private IWebHostBuilder _webHostBuilder;
|
||||||
|
private string _ocelotBaseUrl;
|
||||||
|
private BearerToken _token;
|
||||||
|
private IWebHostBuilder _webHostBuilderTwo;
|
||||||
|
private IWebHost _builderTwo;
|
||||||
|
private IWebHost _identityServerBuilder;
|
||||||
|
private IWebHost _fooServiceBuilder;
|
||||||
|
private IWebHost _barServiceBuilder;
|
||||||
|
|
||||||
|
public AdministrationTests()
|
||||||
|
{
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
_httpClientTwo = new HttpClient();
|
||||||
|
_ocelotBaseUrl = "http://localhost:5000";
|
||||||
|
_httpClient.BaseAddress = new Uri(_ocelotBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_401_with_call_re_routes_controller()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration();
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_with_call_re_routes_controller()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration();
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config()
|
||||||
|
{
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
_ocelotBaseUrl = "http://localhost:5011";
|
||||||
|
_httpClient.BaseAddress = new Uri(_ocelotBaseUrl);
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
BaseUrl = _ocelotBaseUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl))
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration();
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet())
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenAnotherOcelotIsRunning("http://localhost:5007"))
|
||||||
|
.When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_file_configuration()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
RequestIdKey = "RequestId",
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10,
|
||||||
|
Region = "Geoff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/test",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10,
|
||||||
|
Region = "Dave"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => ThenTheResponseShouldBe(configuration))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_file_configuration_edit_and_post_updated_version()
|
||||||
|
{
|
||||||
|
var initialConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/"
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamPathTemplate = "/geoffrey",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/"
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "123.123.123",
|
||||||
|
Port = 443,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/blooper/{productId}",
|
||||||
|
UpstreamHttpMethod = new List<string> { "post" },
|
||||||
|
UpstreamPathTemplate = "/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(initialConfiguration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => ThenTheResponseShouldBe(updatedConfiguration))
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.And(x => ThenTheResponseShouldBe(updatedConfiguration))
|
||||||
|
.And(_ => ThenTheConfigurationIsSavedCorrectly(updatedConfiguration))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected)
|
||||||
|
{
|
||||||
|
var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json";
|
||||||
|
var resultText = File.ReadAllText(ocelotJsonPath);
|
||||||
|
var expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented);
|
||||||
|
resultText.ShouldBe(expectedText);
|
||||||
|
|
||||||
|
var environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot.Production.json";
|
||||||
|
resultText = File.ReadAllText(environmentSpecificPath);
|
||||||
|
expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented);
|
||||||
|
resultText.ShouldBe(expectedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_file_configuration_edit_and_post_updated_version_redirecting_reroute()
|
||||||
|
{
|
||||||
|
var fooPort = 47689;
|
||||||
|
var barPort = 47690;
|
||||||
|
|
||||||
|
var initialConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = fooPort,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamPathTemplate = "/foo",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = barPort,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamPathTemplate = "/bar",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(initialConfiguration))
|
||||||
|
.And(x => GivenThereIsAFooServiceRunningOn($"http://localhost:{fooPort}"))
|
||||||
|
.And(x => GivenThereIsABarServiceRunningOn($"http://localhost:{barPort}"))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => WhenIGetUrlOnTheApiGateway("/foo"))
|
||||||
|
.Then(x => ThenTheResponseBodyShouldBe("foo"))
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => ThenTheResponseShouldBe(updatedConfiguration))
|
||||||
|
.And(x => WhenIGetUrlOnTheApiGateway("/foo"))
|
||||||
|
.Then(x => ThenTheResponseBodyShouldBe("bar"))
|
||||||
|
.When(x => WhenIPostOnTheApiGateway("/administration/configuration", initialConfiguration))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => ThenTheResponseShouldBe(initialConfiguration))
|
||||||
|
.And(x => WhenIGetUrlOnTheApiGateway("/foo"))
|
||||||
|
.Then(x => ThenTheResponseBodyShouldBe("foo"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_clear_region()
|
||||||
|
{
|
||||||
|
var initialConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/test",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var regionToClear = "gettest";
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(initialConfiguration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration();
|
||||||
|
|
||||||
|
var identityServerRootUrl = "http://localhost:5123";
|
||||||
|
|
||||||
|
Action<IdentityServerAuthenticationOptions> options = o => {
|
||||||
|
o.Authority = identityServerRootUrl;
|
||||||
|
o.ApiName = "api";
|
||||||
|
o.RequireHttpsMetadata = false;
|
||||||
|
o.SupportedTokens = SupportedTokens.Both;
|
||||||
|
o.ApiSecret = "secret";
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api"))
|
||||||
|
.And(x => GivenOcelotIsRunningWithIdentityServerSettings(options))
|
||||||
|
.And(x => GivenIHaveAToken(identityServerRootUrl))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAToken(string url)
|
||||||
|
{
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", "api"),
|
||||||
|
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||||
|
new KeyValuePair<string, string>("scope", "api"),
|
||||||
|
new KeyValuePair<string, string>("username", "test"),
|
||||||
|
new KeyValuePair<string, string>("password", "test"),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "password")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
var response = httpClient.PostAsync($"{url}/connect/token", content).Result;
|
||||||
|
var responseContent = response.Content.ReadAsStringAsync().Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAnIdentityServerOn(string url, string apiName)
|
||||||
|
{
|
||||||
|
_identityServerBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddIdentityServer()
|
||||||
|
.AddDeveloperSigningCredential()
|
||||||
|
.AddInMemoryApiResources(new List<ApiResource>
|
||||||
|
{
|
||||||
|
new ApiResource
|
||||||
|
{
|
||||||
|
Name = apiName,
|
||||||
|
Description = apiName,
|
||||||
|
Enabled = true,
|
||||||
|
DisplayName = apiName,
|
||||||
|
Scopes = new List<Scope>()
|
||||||
|
{
|
||||||
|
new Scope(apiName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.AddInMemoryClients(new List<Client>
|
||||||
|
{
|
||||||
|
new Client
|
||||||
|
{
|
||||||
|
ClientId = apiName,
|
||||||
|
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
|
||||||
|
ClientSecrets = new List<Secret> {new Secret("secret".Sha256())},
|
||||||
|
AllowedScopes = new List<string> { apiName },
|
||||||
|
AccessTokenType = AccessTokenType.Jwt,
|
||||||
|
Enabled = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.AddTestUsers(new List<TestUser>
|
||||||
|
{
|
||||||
|
new TestUser
|
||||||
|
{
|
||||||
|
Username = "test",
|
||||||
|
Password = "test",
|
||||||
|
SubjectId = "1231231"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseIdentityServer();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_identityServerBuilder.Start();
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenAnotherOcelotIsRunning(string baseUrl)
|
||||||
|
{
|
||||||
|
_httpClientTwo.BaseAddress = new Uri(baseUrl);
|
||||||
|
|
||||||
|
_webHostBuilderTwo = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
x.AddOcelot()
|
||||||
|
.AddAdministration("/administration", "secret");
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_builderTwo = _webHostBuilderTwo.Build();
|
||||||
|
|
||||||
|
_builderTwo.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIdentityServerSigningEnvironmentalVariablesAreSet()
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx");
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetUrlOnTheSecondOcelot(string url)
|
||||||
|
{
|
||||||
|
_httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
_response = _httpClientTwo.GetAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(updatedConfiguration);
|
||||||
|
var content = new StringContent(json);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
_response = _httpClient.PostAsync(url, content).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResponseShouldBe(List<string> expected)
|
||||||
|
{
|
||||||
|
var content = _response.Content.ReadAsStringAsync().Result;
|
||||||
|
var result = JsonConvert.DeserializeObject<Regions>(content);
|
||||||
|
result.Value.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResponseBodyShouldBe(string expected)
|
||||||
|
{
|
||||||
|
var content = _response.Content.ReadAsStringAsync().Result;
|
||||||
|
content.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResponseShouldBe(FileConfiguration expecteds)
|
||||||
|
{
|
||||||
|
var response = JsonConvert.DeserializeObject<FileConfiguration>(_response.Content.ReadAsStringAsync().Result);
|
||||||
|
|
||||||
|
response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey);
|
||||||
|
response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host);
|
||||||
|
response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port);
|
||||||
|
|
||||||
|
for (var i = 0; i < response.ReRoutes.Count; i++)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++)
|
||||||
|
{
|
||||||
|
var result = response.ReRoutes[i].DownstreamHostAndPorts[j];
|
||||||
|
var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j];
|
||||||
|
result.Host.ShouldBe(expected.Host);
|
||||||
|
result.Port.ShouldBe(expected.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate);
|
||||||
|
response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme);
|
||||||
|
response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate);
|
||||||
|
response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAddedATokenToMyRequest()
|
||||||
|
{
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAnOcelotToken(string adminPath)
|
||||||
|
{
|
||||||
|
var tokenUrl = $"{adminPath}/connect/token";
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", "admin"),
|
||||||
|
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||||
|
new KeyValuePair<string, string>("scope", "admin"),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
var response = _httpClient.PostAsync(tokenUrl, content).Result;
|
||||||
|
var responseContent = response.Content.ReadAsStringAsync().Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
var configPath = $"{adminPath}/.well-known/openid-configuration";
|
||||||
|
response = _httpClient.GetAsync(configPath).Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenOcelotIsRunningWithIdentityServerSettings(Action<IdentityServerAuthenticationOptions> configOptions)
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(_ocelotBaseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x => {
|
||||||
|
x.AddSingleton(_webHostBuilder);
|
||||||
|
x.AddOcelot()
|
||||||
|
.AddAdministration("/administration", configOptions);
|
||||||
|
})
|
||||||
|
.Configure(app => {
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_builder = _webHostBuilder.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenOcelotIsRunning()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(_ocelotBaseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
x.AddOcelot()
|
||||||
|
.AddAdministration("/administration", "secret");
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_builder = _webHostBuilder.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl)
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(_ocelotBaseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x => {
|
||||||
|
x.AddSingleton(_webHostBuilder);
|
||||||
|
x.AddOcelot()
|
||||||
|
.AddAdministration("/administration", "secret");
|
||||||
|
})
|
||||||
|
.Configure(app => {
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_builder = _webHostBuilder.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
||||||
|
{
|
||||||
|
var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json";
|
||||||
|
|
||||||
|
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
var text = File.ReadAllText(configurationPath);
|
||||||
|
|
||||||
|
configurationPath = $"{AppContext.BaseDirectory}/ocelot.json";
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
text = File.ReadAllText(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetUrlOnTheApiGateway(string url)
|
||||||
|
{
|
||||||
|
_response = _httpClient.GetAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIDeleteOnTheApiGateway(string url)
|
||||||
|
{
|
||||||
|
_response = _httpClient.DeleteAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||||
|
{
|
||||||
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "");
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "");
|
||||||
|
_builder?.Dispose();
|
||||||
|
_httpClient?.Dispose();
|
||||||
|
_identityServerBuilder?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFooServiceRunningOn(string baseUrl)
|
||||||
|
{
|
||||||
|
_fooServiceBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase("/foo");
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
await context.Response.WriteAsync("foo");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_fooServiceBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsABarServiceRunningOn(string baseUrl)
|
||||||
|
{
|
||||||
|
_barServiceBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(baseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UsePathBase("/bar");
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
await context.Response.WriteAsync("bar");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_barServiceBuilder.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
test/Ocelot.IntegrationTests/BearerToken.cs
Normal file
16
test/Ocelot.IntegrationTests/BearerToken.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Ocelot.IntegrationTests
|
||||||
|
{
|
||||||
|
class BearerToken
|
||||||
|
{
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("token_type")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
224
test/Ocelot.IntegrationTests/CacheManagerTests.cs
Normal file
224
test/Ocelot.IntegrationTests/CacheManagerTests.cs
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.IntegrationTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Configuration.File;
|
||||||
|
using DependencyInjection;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
using Ocelot.Administration;
|
||||||
|
using Ocelot.IntegrationTests;
|
||||||
|
using Ocelot.Cache.CacheManager;
|
||||||
|
|
||||||
|
public class CacheManagerTests : IDisposable
|
||||||
|
{
|
||||||
|
private HttpClient _httpClient;
|
||||||
|
private readonly HttpClient _httpClientTwo;
|
||||||
|
private HttpResponseMessage _response;
|
||||||
|
private IWebHost _builder;
|
||||||
|
private IWebHostBuilder _webHostBuilder;
|
||||||
|
private string _ocelotBaseUrl;
|
||||||
|
private BearerToken _token;
|
||||||
|
private IWebHostBuilder _webHostBuilderTwo;
|
||||||
|
private IWebHost _builderTwo;
|
||||||
|
private IWebHost _identityServerBuilder;
|
||||||
|
private IWebHost _fooServiceBuilder;
|
||||||
|
private IWebHost _barServiceBuilder;
|
||||||
|
|
||||||
|
public CacheManagerTests()
|
||||||
|
{
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
_httpClientTwo = new HttpClient();
|
||||||
|
_ocelotBaseUrl = "http://localhost:5000";
|
||||||
|
_httpClient.BaseAddress = new Uri(_ocelotBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_clear_region()
|
||||||
|
{
|
||||||
|
var initialConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/test",
|
||||||
|
FileCacheOptions = new FileCacheOptions
|
||||||
|
{
|
||||||
|
TtlSeconds = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var regionToClear = "gettest";
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAConfiguration(initialConfiguration))
|
||||||
|
.And(x => GivenOcelotIsRunning())
|
||||||
|
.And(x => GivenIHaveAnOcelotToken("/administration"))
|
||||||
|
.And(x => GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}"))
|
||||||
|
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAddedATokenToMyRequest()
|
||||||
|
{
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAnOcelotToken(string adminPath)
|
||||||
|
{
|
||||||
|
var tokenUrl = $"{adminPath}/connect/token";
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", "admin"),
|
||||||
|
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||||
|
new KeyValuePair<string, string>("scope", "admin"),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
var response = _httpClient.PostAsync(tokenUrl, content).Result;
|
||||||
|
var responseContent = response.Content.ReadAsStringAsync().Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
var configPath = $"{adminPath}/.well-known/openid-configuration";
|
||||||
|
response = _httpClient.GetAsync(configPath).Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenOcelotIsRunning()
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(_ocelotBaseUrl)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
Action<ConfigurationBuilderCachePart> settings = (s) =>
|
||||||
|
{
|
||||||
|
s.WithMicrosoftLogging(log =>
|
||||||
|
{
|
||||||
|
log.AddConsole(LogLevel.Debug);
|
||||||
|
})
|
||||||
|
.WithDictionaryHandle();
|
||||||
|
};
|
||||||
|
|
||||||
|
x.AddOcelot()
|
||||||
|
.AddCacheManager(settings)
|
||||||
|
.AddAdministration("/administration", "secret");
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_builder = _webHostBuilder.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
||||||
|
{
|
||||||
|
var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json";
|
||||||
|
|
||||||
|
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
var text = File.ReadAllText(configurationPath);
|
||||||
|
|
||||||
|
configurationPath = $"{AppContext.BaseDirectory}/ocelot.json";
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
text = File.ReadAllText(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIDeleteOnTheApiGateway(string url)
|
||||||
|
{
|
||||||
|
_response = _httpClient.DeleteAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||||
|
{
|
||||||
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "");
|
||||||
|
Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "");
|
||||||
|
_builder?.Dispose();
|
||||||
|
_httpClient?.Dispose();
|
||||||
|
_identityServerBuilder?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -22,10 +22,17 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
|
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Administration\Ocelot.Administration.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Ocelot.Provider.Rafty\Ocelot.Provider.Rafty.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
|
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SQLite" Version="2.2.0" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@ -41,6 +48,8 @@
|
|||||||
<PackageReference Include="xunit" Version="2.3.1" />
|
<PackageReference Include="xunit" Version="2.3.1" />
|
||||||
<PackageReference Include="Shouldly" Version="3.0.0" />
|
<PackageReference Include="Shouldly" Version="3.0.0" />
|
||||||
<PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
|
<PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
|
||||||
<PackageReference Include="Microsoft.Data.SQLite" Version="2.1.0" />
|
<PackageReference Include="Microsoft.Data.SQLite" Version="2.2.0" />
|
||||||
|
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
|
||||||
|
<PackageReference Include="IdentityServer4" Version="2.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
516
test/Ocelot.IntegrationTests/RaftTests.cs
Normal file
516
test/Ocelot.IntegrationTests/RaftTests.cs
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
namespace Ocelot.IntegrationTests
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Administration;
|
||||||
|
using Configuration.File;
|
||||||
|
using DependencyInjection;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Middleware;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using Ocelot.Administration;
|
||||||
|
using Ocelot.IntegrationTests;
|
||||||
|
using Ocelot.Provider.Rafty;
|
||||||
|
using Ocelot.Infrastructure;
|
||||||
|
using Rafty.Infrastructure;
|
||||||
|
using Wait = Rafty.Infrastructure.Wait;
|
||||||
|
|
||||||
|
public class RaftTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<IWebHost> _builders;
|
||||||
|
private readonly List<IWebHostBuilder> _webHostBuilders;
|
||||||
|
private readonly List<Thread> _threads;
|
||||||
|
private FilePeers _peers;
|
||||||
|
private HttpClient _httpClient;
|
||||||
|
private readonly HttpClient _httpClientForAssertions;
|
||||||
|
private BearerToken _token;
|
||||||
|
private HttpResponseMessage _response;
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
private ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public RaftTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
_httpClientForAssertions = new HttpClient();
|
||||||
|
_webHostBuilders = new List<IWebHostBuilder>();
|
||||||
|
_builders = new List<IWebHost>();
|
||||||
|
_threads = new List<Thread>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Still not stable, more work required in rafty..")]
|
||||||
|
public async Task should_persist_command_to_five_servers()
|
||||||
|
{
|
||||||
|
var peers = new List<FilePeer>
|
||||||
|
{
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5000"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5001"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5002"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5003"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5004"}
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
},
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamPathTemplate = "/geoffrey",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/"
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "123.123.123",
|
||||||
|
Port = 443,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/blooper/{productId}",
|
||||||
|
UpstreamHttpMethod = new List<string> { "post" },
|
||||||
|
UpstreamPathTemplate = "/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var command = new UpdateFileConfiguration(updatedConfiguration);
|
||||||
|
GivenThePeersAre(peers);
|
||||||
|
GivenThereIsAConfiguration(configuration);
|
||||||
|
GivenFiveServersAreRunning();
|
||||||
|
await GivenIHaveAnOcelotToken("/administration");
|
||||||
|
await WhenISendACommandIntoTheCluster(command);
|
||||||
|
Thread.Sleep(5000);
|
||||||
|
await ThenTheCommandIsReplicatedToAllStateMachines(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Still not stable, more work required in rafty..")]
|
||||||
|
public async Task should_persist_command_to_five_servers_when_using_administration_api()
|
||||||
|
{
|
||||||
|
var peers = new List<FilePeer>
|
||||||
|
{
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5005"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5006"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5007"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5008"},
|
||||||
|
|
||||||
|
new FilePeer {HostAndPort = "http://localhost:5009"}
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedConfiguration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>()
|
||||||
|
{
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamPathTemplate = "/geoffrey",
|
||||||
|
UpstreamHttpMethod = new List<string> { "get" },
|
||||||
|
UpstreamPathTemplate = "/"
|
||||||
|
},
|
||||||
|
new FileReRoute()
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "123.123.123",
|
||||||
|
Port = 443,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/blooper/{productId}",
|
||||||
|
UpstreamHttpMethod = new List<string> { "post" },
|
||||||
|
UpstreamPathTemplate = "/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var command = new UpdateFileConfiguration(updatedConfiguration);
|
||||||
|
GivenThePeersAre(peers);
|
||||||
|
GivenThereIsAConfiguration(configuration);
|
||||||
|
GivenFiveServersAreRunning();
|
||||||
|
await GivenIHaveAnOcelotToken("/administration");
|
||||||
|
GivenIHaveAddedATokenToMyRequest();
|
||||||
|
await WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration);
|
||||||
|
await ThenTheCommandIsReplicatedToAllStateMachines(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThePeersAre(List<FilePeer> peers)
|
||||||
|
{
|
||||||
|
FilePeers filePeers = new FilePeers();
|
||||||
|
filePeers.Peers.AddRange(peers);
|
||||||
|
var json = JsonConvert.SerializeObject(filePeers);
|
||||||
|
File.WriteAllText("peers.json", json);
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
var ocelotBaseUrl = peers[0].HostAndPort;
|
||||||
|
_httpClient.BaseAddress = new Uri(ocelotBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenISendACommandIntoTheCluster(UpdateFileConfiguration command)
|
||||||
|
{
|
||||||
|
async Task<bool> SendCommand()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = _peers.Peers.First();
|
||||||
|
var json = JsonConvert.SerializeObject(command, new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.All
|
||||||
|
});
|
||||||
|
var httpContent = new StringContent(json);
|
||||||
|
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
var response = await httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var errorResult = JsonConvert.DeserializeObject<ErrorResponse<UpdateFileConfiguration>>(content);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(errorResult.Error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var okResult = JsonConvert.DeserializeObject<OkResponse<UpdateFileConfiguration>>(content);
|
||||||
|
|
||||||
|
if (okResult.Command.Configuration.ReRoutes.Count == 2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandSent = await Wait.WaitFor(40000).Until(async () =>
|
||||||
|
{
|
||||||
|
var result = await SendCommand();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
commandSent.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds)
|
||||||
|
{
|
||||||
|
async Task<bool> CommandCalledOnAllStateMachines()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var passed = 0;
|
||||||
|
foreach (var peer in _peers.Peers)
|
||||||
|
{
|
||||||
|
var path = $"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db";
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={path};"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
var sql = @"select count(id) from logs";
|
||||||
|
using (var command = new SqliteCommand(sql, connection))
|
||||||
|
{
|
||||||
|
var index = Convert.ToInt32(command.ExecuteScalar());
|
||||||
|
index.ShouldBe(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
var result = await _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration");
|
||||||
|
var json = await result.Content.ReadAsStringAsync();
|
||||||
|
var response = JsonConvert.DeserializeObject<FileConfiguration>(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
|
||||||
|
response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey);
|
||||||
|
response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host);
|
||||||
|
response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port);
|
||||||
|
|
||||||
|
for (var i = 0; i < response.ReRoutes.Count; i++)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++)
|
||||||
|
{
|
||||||
|
var res = response.ReRoutes[i].DownstreamHostAndPorts[j];
|
||||||
|
var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j];
|
||||||
|
res.Host.ShouldBe(expected.Host);
|
||||||
|
res.Port.ShouldBe(expected.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate);
|
||||||
|
response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme);
|
||||||
|
response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate);
|
||||||
|
response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return passed == 5;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
//_output.WriteLine($"{e.Message}, {e.StackTrace}");
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandOnAllStateMachines = await Wait.WaitFor(40000).Until(async () =>
|
||||||
|
{
|
||||||
|
var result = await CommandCalledOnAllStateMachines();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
commandOnAllStateMachines.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration)
|
||||||
|
{
|
||||||
|
async Task<bool> SendCommand()
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(updatedConfiguration);
|
||||||
|
|
||||||
|
var content = new StringContent(json);
|
||||||
|
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
_response = await _httpClient.PostAsync(url, content);
|
||||||
|
|
||||||
|
var responseContent = await _response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (responseContent == "There was a problem. This error message sucks raise an issue in GitHub.")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(responseContent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandSent = await Wait.WaitFor(40000).Until(async () =>
|
||||||
|
{
|
||||||
|
var result = await SendCommand();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
commandSent.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAddedATokenToMyRequest()
|
||||||
|
{
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GivenIHaveAnOcelotToken(string adminPath)
|
||||||
|
{
|
||||||
|
async Task<bool> AddToken()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tokenUrl = $"{adminPath}/connect/token";
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", "admin"),
|
||||||
|
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||||
|
new KeyValuePair<string, string>("scope", "admin"),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync(tokenUrl, content);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
var configPath = $"{adminPath}/.well-known/openid-configuration";
|
||||||
|
response = await _httpClient.GetAsync(configPath);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addToken = await Wait.WaitFor(40000).Until(async () =>
|
||||||
|
{
|
||||||
|
var result = await AddToken();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
addToken.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
||||||
|
{
|
||||||
|
var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json";
|
||||||
|
|
||||||
|
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
var text = File.ReadAllText(configurationPath);
|
||||||
|
|
||||||
|
configurationPath = $"{AppContext.BaseDirectory}/ocelot.json";
|
||||||
|
|
||||||
|
if (File.Exists(configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||||
|
|
||||||
|
text = File.ReadAllText(configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenAServerIsRunning(string url)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
IWebHostBuilder webHostBuilder = new WebHostBuilder();
|
||||||
|
webHostBuilder.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddJsonFile("peers.json", optional: true, reloadOnChange: false);
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
config.AddOcelotBaseUrl(url);
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
x.AddSingleton(new NodeId(url));
|
||||||
|
x
|
||||||
|
.AddOcelot()
|
||||||
|
.AddAdministration("/administration", "secret")
|
||||||
|
.AddRafty();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
var builder = webHostBuilder.Build();
|
||||||
|
builder.Start();
|
||||||
|
|
||||||
|
_webHostBuilders.Add(webHostBuilder);
|
||||||
|
_builders.Add(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFiveServersAreRunning()
|
||||||
|
{
|
||||||
|
var bytes = File.ReadAllText("peers.json");
|
||||||
|
_peers = JsonConvert.DeserializeObject<FilePeers>(bytes);
|
||||||
|
|
||||||
|
foreach (var peer in _peers.Peers)
|
||||||
|
{
|
||||||
|
File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", ""));
|
||||||
|
File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db");
|
||||||
|
var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort));
|
||||||
|
thread.Start();
|
||||||
|
_threads.Add(thread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var builder in _builders)
|
||||||
|
{
|
||||||
|
builder?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var peer in _peers.Peers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", ""));
|
||||||
|
File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
namespace Ocelot.UnitTests.Administration
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using IdentityServer4.AccessTokenValidation;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Internal;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ocelot.Administration;
|
||||||
|
using Ocelot.DependencyInjection;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotAdministrationBuilderTests
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _services;
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configRoot;
|
||||||
|
private IOcelotBuilder _ocelotBuilder;
|
||||||
|
private Exception _ex;
|
||||||
|
|
||||||
|
public OcelotAdministrationBuilderTests()
|
||||||
|
{
|
||||||
|
_configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_services.AddSingleton<IHostingEnvironment, HostingEnvironment>();
|
||||||
|
_services.AddSingleton(_configRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
//keep
|
||||||
|
[Fact]
|
||||||
|
public void should_set_up_administration_with_identity_server_options()
|
||||||
|
{
|
||||||
|
Action<IdentityServerAuthenticationOptions> options = o => {};
|
||||||
|
|
||||||
|
this.Given(x => WhenISetUpOcelotServices())
|
||||||
|
.When(x => WhenISetUpAdministration(options))
|
||||||
|
.Then(x => ThenAnExceptionIsntThrown())
|
||||||
|
.Then(x => ThenTheCorrectAdminPathIsRegitered())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
//keep
|
||||||
|
[Fact]
|
||||||
|
public void should_set_up_administration()
|
||||||
|
{
|
||||||
|
this.Given(x => WhenISetUpOcelotServices())
|
||||||
|
.When(x => WhenISetUpAdministration())
|
||||||
|
.Then(x => ThenAnExceptionIsntThrown())
|
||||||
|
.Then(x => ThenTheCorrectAdminPathIsRegitered())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpAdministration()
|
||||||
|
{
|
||||||
|
_ocelotBuilder.AddAdministration("/administration", "secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpAdministration(Action<IdentityServerAuthenticationOptions> options)
|
||||||
|
{
|
||||||
|
_ocelotBuilder.AddAdministration("/administration", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCorrectAdminPathIsRegitered()
|
||||||
|
{
|
||||||
|
_serviceProvider = _services.BuildServiceProvider();
|
||||||
|
var path = _serviceProvider.GetService<IAdministrationPath>();
|
||||||
|
path.Path.ShouldBe("/administration");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpOcelotServices()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ocelotBuilder = _services.AddOcelot(_configRoot);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_ex = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenAnExceptionIsntThrown()
|
||||||
|
{
|
||||||
|
_ex.ShouldBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
|||||||
|
namespace Ocelot.UnitTests.CacheManager
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Internal;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ocelot.Cache;
|
||||||
|
using Ocelot.Cache.CacheManager;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Configuration.File;
|
||||||
|
using Ocelot.DependencyInjection;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotBuilderExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _services;
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configRoot;
|
||||||
|
private IOcelotBuilder _ocelotBuilder;
|
||||||
|
private readonly int _maxRetries;
|
||||||
|
private Exception _ex;
|
||||||
|
|
||||||
|
public OcelotBuilderExtensionsTests()
|
||||||
|
{
|
||||||
|
_configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_services.AddSingleton<IHostingEnvironment, HostingEnvironment>();
|
||||||
|
_services.AddSingleton(_configRoot);
|
||||||
|
_maxRetries = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_up_cache_manager()
|
||||||
|
{
|
||||||
|
this.Given(x => WhenISetUpOcelotServices())
|
||||||
|
.When(x => WhenISetUpCacheManager())
|
||||||
|
.Then(x => ThenAnExceptionIsntThrown())
|
||||||
|
.And(x => OnlyOneVersionOfEachCacheIsRegistered())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnlyOneVersionOfEachCacheIsRegistered()
|
||||||
|
{
|
||||||
|
var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<CachedResponse>));
|
||||||
|
var outputCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager<CachedResponse>));
|
||||||
|
var instance = (ICacheManager<CachedResponse>)outputCacheManager.ImplementationInstance;
|
||||||
|
var ocelotConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<IInternalConfiguration>));
|
||||||
|
var ocelotConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager<IInternalConfiguration>));
|
||||||
|
var fileConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache<FileConfiguration>));
|
||||||
|
var fileConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager<FileConfiguration>));
|
||||||
|
|
||||||
|
instance.Configuration.MaxRetries.ShouldBe(_maxRetries);
|
||||||
|
outputCache.ShouldNotBeNull();
|
||||||
|
ocelotConfigCache.ShouldNotBeNull();
|
||||||
|
ocelotConfigCacheManager.ShouldNotBeNull();
|
||||||
|
fileConfigCache.ShouldNotBeNull();
|
||||||
|
fileConfigCacheManager.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpOcelotServices()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ocelotBuilder = _services.AddOcelot(_configRoot);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_ex = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpCacheManager()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ocelotBuilder.AddCacheManager(x => {
|
||||||
|
x.WithMaxRetries(_maxRetries);
|
||||||
|
x.WithDictionaryHandle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_ex = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenAnExceptionIsntThrown()
|
||||||
|
{
|
||||||
|
_ex.ShouldBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs
Normal file
103
test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
namespace Ocelot.UnitTests.CacheManager
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Cache.CacheManager;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotCacheManagerCache
|
||||||
|
{
|
||||||
|
private OcelotCacheManagerCache<string> _ocelotOcelotCacheManager;
|
||||||
|
private Mock<ICacheManager<string>> _mockCacheManager;
|
||||||
|
private string _key;
|
||||||
|
private string _value;
|
||||||
|
private string _resultGet;
|
||||||
|
private TimeSpan _ttlSeconds;
|
||||||
|
private string _region;
|
||||||
|
|
||||||
|
public OcelotCacheManagerCache()
|
||||||
|
{
|
||||||
|
_mockCacheManager = new Mock<ICacheManager<string>>();
|
||||||
|
_ocelotOcelotCacheManager = new OcelotCacheManagerCache<string>(_mockCacheManager.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_from_cache()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheFollowingIsCached("someKey", "someRegion", "someValue"))
|
||||||
|
.When(x => x.WhenIGetFromTheCache())
|
||||||
|
.Then(x => x.ThenTheResultIs("someValue"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_add_to_cache()
|
||||||
|
{
|
||||||
|
this.When(x => x.WhenIAddToTheCache("someKey", "someValue", TimeSpan.FromSeconds(1)))
|
||||||
|
.Then(x => x.ThenTheCacheIsCalledCorrectly())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_delete_key_from_cache()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenTheFollowingRegion("fookey"))
|
||||||
|
.When(_ => WhenIDeleteTheRegion("fookey"))
|
||||||
|
.Then(_ => ThenTheRegionIsDeleted("fookey"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIDeleteTheRegion(string region)
|
||||||
|
{
|
||||||
|
_ocelotOcelotCacheManager.ClearRegion(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheRegionIsDeleted(string region)
|
||||||
|
{
|
||||||
|
_mockCacheManager
|
||||||
|
.Verify(x => x.ClearRegion(region), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheFollowingRegion(string key)
|
||||||
|
{
|
||||||
|
_ocelotOcelotCacheManager.Add(key, "doesnt matter", TimeSpan.FromSeconds(10), "region");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIAddToTheCache(string key, string value, TimeSpan ttlSeconds)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
_value = value;
|
||||||
|
_ttlSeconds = ttlSeconds;
|
||||||
|
_ocelotOcelotCacheManager.Add(_key, _value, _ttlSeconds, "region");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCacheIsCalledCorrectly()
|
||||||
|
{
|
||||||
|
_mockCacheManager
|
||||||
|
.Verify(x => x.Add(It.IsAny<CacheItem<string>>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResultIs(string expected)
|
||||||
|
{
|
||||||
|
_resultGet.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetFromTheCache()
|
||||||
|
{
|
||||||
|
_resultGet = _ocelotOcelotCacheManager.Get(_key, _region);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheFollowingIsCached(string key, string region, string value)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
_value = value;
|
||||||
|
_region = region;
|
||||||
|
_mockCacheManager
|
||||||
|
.Setup(x => x.Get<string>(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
namespace Ocelot.UnitTests.CacheManager
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::CacheManager.Core;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Cache;
|
||||||
|
using Ocelot.Cache.CacheManager;
|
||||||
|
using Ocelot.Cache.Middleware;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Configuration.Builder;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OutputCacheMiddlewareRealCacheTests
|
||||||
|
{
|
||||||
|
private readonly IOcelotCache<CachedResponse> _cacheManager;
|
||||||
|
private readonly OutputCacheMiddleware _middleware;
|
||||||
|
private readonly DownstreamContext _downstreamContext;
|
||||||
|
private OcelotRequestDelegate _next;
|
||||||
|
private Mock<IOcelotLoggerFactory> _loggerFactory;
|
||||||
|
private Mock<IOcelotLogger> _logger;
|
||||||
|
|
||||||
|
public OutputCacheMiddlewareRealCacheTests()
|
||||||
|
{
|
||||||
|
_loggerFactory = new Mock<IOcelotLoggerFactory>();
|
||||||
|
_logger = new Mock<IOcelotLogger>();
|
||||||
|
_loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object);
|
||||||
|
var cacheManagerOutputCache = CacheFactory.Build<CachedResponse>("OcelotOutputCache", x =>
|
||||||
|
{
|
||||||
|
x.WithDictionaryHandle();
|
||||||
|
});
|
||||||
|
_cacheManager = new OcelotCacheManagerCache<CachedResponse>(cacheManagerOutputCache);
|
||||||
|
_downstreamContext = new DownstreamContext(new DefaultHttpContext());
|
||||||
|
_downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
|
||||||
|
_next = context => Task.CompletedTask;
|
||||||
|
_middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_cache_content_headers()
|
||||||
|
{
|
||||||
|
var content = new StringContent("{\"Test\": 1}")
|
||||||
|
{
|
||||||
|
Headers = { ContentType = new MediaTypeHeaderValue("application/json")}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = new DownstreamResponse(content, HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), "fooreason");
|
||||||
|
|
||||||
|
this.Given(x => x.GivenResponseIsNotCached(response))
|
||||||
|
.And(x => x.GivenTheDownstreamRouteIs())
|
||||||
|
.When(x => x.WhenICallTheMiddleware())
|
||||||
|
.Then(x => x.ThenTheContentTypeHeaderIsCached())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenICallTheMiddleware()
|
||||||
|
{
|
||||||
|
_middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheContentTypeHeaderIsCached()
|
||||||
|
{
|
||||||
|
var result = _cacheManager.Get("GET-https://some.url/blah?abcd=123", "kanken");
|
||||||
|
var header = result.ContentHeaders["Content-Type"];
|
||||||
|
header.First().ShouldBe("application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenResponseIsNotCached(DownstreamResponse response)
|
||||||
|
{
|
||||||
|
_downstreamContext.DownstreamResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheDownstreamRouteIs()
|
||||||
|
{
|
||||||
|
var reRoute = new DownstreamReRouteBuilder()
|
||||||
|
.WithIsCached(true)
|
||||||
|
.WithCacheOptions(new CacheOptions(100, "kanken"))
|
||||||
|
.WithUpstreamHttpMethod(new List<string> { "Get" })
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_downstreamContext.DownstreamReRoute = reRoute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,261 @@
|
|||||||
|
namespace Ocelot.UnitTests.Consul
|
||||||
|
{
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using global::Consul;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Cache;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Configuration.Builder;
|
||||||
|
using Ocelot.Configuration.File;
|
||||||
|
using Ocelot.Configuration.Repository;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Provider.Consul;
|
||||||
|
using Responses;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ConsulFileConfigurationRepositoryTests
|
||||||
|
{
|
||||||
|
private ConsulFileConfigurationRepository _repo;
|
||||||
|
private Mock<IOcelotCache<FileConfiguration>> _cache;
|
||||||
|
private Mock<IInternalConfigurationRepository> _internalRepo;
|
||||||
|
private Mock<IConsulClientFactory> _factory;
|
||||||
|
private Mock<IOcelotLoggerFactory> _loggerFactory;
|
||||||
|
private Mock<IConsulClient> _client;
|
||||||
|
private Mock<IKVEndpoint> _kvEndpoint;
|
||||||
|
private FileConfiguration _fileConfiguration;
|
||||||
|
private Response _setResult;
|
||||||
|
private Response<FileConfiguration> _getResult;
|
||||||
|
|
||||||
|
public ConsulFileConfigurationRepositoryTests()
|
||||||
|
{
|
||||||
|
_cache = new Mock<IOcelotCache<FileConfiguration>>();
|
||||||
|
_internalRepo = new Mock<IInternalConfigurationRepository>();
|
||||||
|
_loggerFactory = new Mock<IOcelotLoggerFactory>();
|
||||||
|
|
||||||
|
_factory = new Mock<IConsulClientFactory>();
|
||||||
|
_client = new Mock<IConsulClient>();
|
||||||
|
_kvEndpoint = new Mock<IKVEndpoint>();
|
||||||
|
|
||||||
|
_client
|
||||||
|
.Setup(x => x.KV)
|
||||||
|
.Returns(_kvEndpoint.Object);
|
||||||
|
|
||||||
|
_factory
|
||||||
|
.Setup(x => x.Get(It.IsAny<ConsulRegistryConfiguration>()))
|
||||||
|
.Returns(_client.Object);
|
||||||
|
|
||||||
|
_internalRepo
|
||||||
|
.Setup(x => x.Get())
|
||||||
|
.Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration(new List<ReRoute>(), "", new ServiceProviderConfigurationBuilder().Build(), "", It.IsAny<LoadBalancerOptions>(), It.IsAny<string>(), It.IsAny<QoSOptions>(), It.IsAny<HttpHandlerOptions>())));
|
||||||
|
|
||||||
|
_repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_config()
|
||||||
|
{
|
||||||
|
var config = FakeFileConfiguration();
|
||||||
|
|
||||||
|
this.Given(_ => GivenIHaveAConfiguration(config))
|
||||||
|
.And(_ => GivenWritingToConsulSucceeds())
|
||||||
|
.When(_ => WhenISetTheConfiguration())
|
||||||
|
.Then(_ => ThenTheConfigurationIsStoredAs(config))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_config()
|
||||||
|
{
|
||||||
|
var config = FakeFileConfiguration();
|
||||||
|
|
||||||
|
this.Given(_ => GivenIHaveAConfiguration(config))
|
||||||
|
.And(_ => GivenFetchFromConsulSucceeds())
|
||||||
|
.When(_ => WhenIGetTheConfiguration())
|
||||||
|
.Then(_ => ThenTheConfigurationIs(config))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_null_config()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenFetchFromConsulReturnsNull())
|
||||||
|
.When(_ => WhenIGetTheConfiguration())
|
||||||
|
.Then(_ => ThenTheConfigurationIsNull())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get_config_from_cache()
|
||||||
|
{
|
||||||
|
var config = FakeFileConfiguration();
|
||||||
|
|
||||||
|
this.Given(_ => GivenIHaveAConfiguration(config))
|
||||||
|
.And(_ => GivenFetchFromCacheSucceeds())
|
||||||
|
.When(_ => WhenIGetTheConfiguration())
|
||||||
|
.Then(_ => ThenTheConfigurationIs(config))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_config_key()
|
||||||
|
{
|
||||||
|
var config = FakeFileConfiguration();
|
||||||
|
|
||||||
|
this.Given(_ => GivenIHaveAConfiguration(config))
|
||||||
|
.And(_ => GivenTheConfigKeyComesFromFileConfig("Tom"))
|
||||||
|
.And(_ => GivenFetchFromConsulSucceeds())
|
||||||
|
.When(_ => WhenIGetTheConfiguration())
|
||||||
|
.And(_ => ThenTheConfigKeyIs("Tom"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_default_config_key()
|
||||||
|
{
|
||||||
|
var config = FakeFileConfiguration();
|
||||||
|
|
||||||
|
this.Given(_ => GivenIHaveAConfiguration(config))
|
||||||
|
.And(_ => GivenFetchFromConsulSucceeds())
|
||||||
|
.When(_ => WhenIGetTheConfiguration())
|
||||||
|
.And(_ => ThenTheConfigKeyIs("InternalConfiguration"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigKeyIs(string expected)
|
||||||
|
{
|
||||||
|
_kvEndpoint
|
||||||
|
.Verify(x => x.Get(expected, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheConfigKeyComesFromFileConfig(string key)
|
||||||
|
{
|
||||||
|
_internalRepo
|
||||||
|
.Setup(x => x.Get())
|
||||||
|
.Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration(new List<ReRoute>(), "",
|
||||||
|
new ServiceProviderConfigurationBuilder().WithConfigurationKey(key).Build(), "",
|
||||||
|
new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(),
|
||||||
|
new HttpHandlerOptionsBuilder().Build())));
|
||||||
|
|
||||||
|
_repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigurationIsNull()
|
||||||
|
{
|
||||||
|
_getResult.Data.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigurationIs(FileConfiguration config)
|
||||||
|
{
|
||||||
|
var expected = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||||
|
var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented);
|
||||||
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenIGetTheConfiguration()
|
||||||
|
{
|
||||||
|
_getResult = await _repo.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenWritingToConsulSucceeds()
|
||||||
|
{
|
||||||
|
var response = new WriteResult<bool>();
|
||||||
|
response.Response = true;
|
||||||
|
|
||||||
|
_kvEndpoint
|
||||||
|
.Setup(x => x.Put(It.IsAny<KVPair>(), It.IsAny<CancellationToken>())).ReturnsAsync(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFetchFromCacheSucceeds()
|
||||||
|
{
|
||||||
|
_cache.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())).Returns(_fileConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFetchFromConsulReturnsNull()
|
||||||
|
{
|
||||||
|
QueryResult<KVPair> result = new QueryResult<KVPair>();
|
||||||
|
|
||||||
|
_kvEndpoint
|
||||||
|
.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFetchFromConsulSucceeds()
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
var kvp = new KVPair("OcelotConfiguration");
|
||||||
|
kvp.Value = bytes;
|
||||||
|
|
||||||
|
var query = new QueryResult<KVPair>();
|
||||||
|
query.Response = kvp;
|
||||||
|
|
||||||
|
_kvEndpoint
|
||||||
|
.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheConfigurationIsStoredAs(FileConfiguration config)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
_kvEndpoint
|
||||||
|
.Verify(x => x.Put(It.Is<KVPair>(k => k.Value.SequenceEqual(bytes)), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenISetTheConfiguration()
|
||||||
|
{
|
||||||
|
_setResult = await _repo.Set(_fileConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAConfiguration(FileConfiguration config)
|
||||||
|
{
|
||||||
|
_fileConfiguration = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileConfiguration FakeFileConfiguration()
|
||||||
|
{
|
||||||
|
var reRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "123.12.12.12",
|
||||||
|
Port = 80,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DownstreamScheme = "https",
|
||||||
|
DownstreamPathTemplate = "/asdfs/test/{test}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var globalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||||
|
{
|
||||||
|
Port = 198,
|
||||||
|
Host = "blah"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new FileConfiguration
|
||||||
|
{
|
||||||
|
GlobalConfiguration = globalConfiguration,
|
||||||
|
ReRoutes = reRoutes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,297 @@
|
|||||||
|
namespace Ocelot.UnitTests.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using global::Consul;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Provider.Consul;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Values;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ConsulServiceDiscoveryProviderTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _fakeConsulBuilder;
|
||||||
|
private readonly List<ServiceEntry> _serviceEntries;
|
||||||
|
private Consul _provider;
|
||||||
|
private readonly string _serviceName;
|
||||||
|
private readonly int _port;
|
||||||
|
private readonly string _consulHost;
|
||||||
|
private readonly string _fakeConsulServiceDiscoveryUrl;
|
||||||
|
private List<Service> _services;
|
||||||
|
private readonly Mock<IOcelotLoggerFactory> _factory;
|
||||||
|
private readonly Mock<IOcelotLogger> _logger;
|
||||||
|
private string _receivedToken;
|
||||||
|
private readonly IConsulClientFactory _clientFactory;
|
||||||
|
|
||||||
|
public ConsulServiceDiscoveryProviderTests()
|
||||||
|
{
|
||||||
|
_serviceName = "test";
|
||||||
|
_port = 8500;
|
||||||
|
_consulHost = "localhost";
|
||||||
|
_fakeConsulServiceDiscoveryUrl = $"http://{_consulHost}:{_port}";
|
||||||
|
_serviceEntries = new List<ServiceEntry>();
|
||||||
|
_factory = new Mock<IOcelotLoggerFactory>();
|
||||||
|
_clientFactory = new ConsulClientFactory();
|
||||||
|
_logger = new Mock<IOcelotLogger>();
|
||||||
|
_factory.Setup(x => x.CreateLogger<Consul>()).Returns(_logger.Object);
|
||||||
|
_factory.Setup(x => x.CreateLogger<PollConsul>()).Returns(_logger.Object);
|
||||||
|
var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, null);
|
||||||
|
_provider = new Consul(config, _factory.Object, _clientFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_service_from_consul()
|
||||||
|
{
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 50881,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName))
|
||||||
|
.And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.When(x => WhenIGetTheServices())
|
||||||
|
.Then(x => ThenTheCountIs(1))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_token()
|
||||||
|
{
|
||||||
|
var token = "test token";
|
||||||
|
var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, token);
|
||||||
|
_provider = new Consul(config, _factory.Object, _clientFactory);
|
||||||
|
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 50881,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName))
|
||||||
|
.And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||||
|
.When(_ => WhenIGetTheServices())
|
||||||
|
.Then(_ => ThenTheCountIs(1))
|
||||||
|
.And(_ => _receivedToken.ShouldBe(token))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_return_services_with_invalid_address()
|
||||||
|
{
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "http://localhost",
|
||||||
|
Port = 50881,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "http://localhost",
|
||||||
|
Port = 50888,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName))
|
||||||
|
.And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.When(x => WhenIGetTheServices())
|
||||||
|
.Then(x => ThenTheCountIs(0))
|
||||||
|
.And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_return_services_with_empty_address()
|
||||||
|
{
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "",
|
||||||
|
Port = 50881,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = null,
|
||||||
|
Port = 50888,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName))
|
||||||
|
.And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.When(x => WhenIGetTheServices())
|
||||||
|
.Then(x => ThenTheCountIs(0))
|
||||||
|
.And(x => ThenTheLoggerHasBeenCalledCorrectlyForEmptyAddress())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_not_return_services_with_invalid_port()
|
||||||
|
{
|
||||||
|
var serviceEntryOne = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = -1,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var serviceEntryTwo = new ServiceEntry()
|
||||||
|
{
|
||||||
|
Service = new AgentService()
|
||||||
|
{
|
||||||
|
Service = _serviceName,
|
||||||
|
Address = "localhost",
|
||||||
|
Port = 0,
|
||||||
|
ID = Guid.NewGuid().ToString(),
|
||||||
|
Tags = new string[0]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName))
|
||||||
|
.And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||||
|
.When(x => WhenIGetTheServices())
|
||||||
|
.Then(x => ThenTheCountIs(0))
|
||||||
|
.And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress()
|
||||||
|
{
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: http://localhost and Port: 50881 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: http://localhost and Port: 50888 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheLoggerHasBeenCalledCorrectlyForEmptyAddress()
|
||||||
|
{
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: and Port: 50881 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: and Port: 50888 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts()
|
||||||
|
{
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: localhost and Port: -1 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_logger.Verify(
|
||||||
|
x => x.LogWarning(
|
||||||
|
"Unable to use service Address: localhost and Port: 0 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCountIs(int count)
|
||||||
|
{
|
||||||
|
_services.Count.ShouldBe(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetTheServices()
|
||||||
|
{
|
||||||
|
_services = _provider.Get().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||||
|
{
|
||||||
|
foreach (var serviceEntry in serviceEntries)
|
||||||
|
{
|
||||||
|
_serviceEntries.Add(serviceEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||||
|
{
|
||||||
|
_fakeConsulBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values))
|
||||||
|
{
|
||||||
|
_receivedToken = values.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(_serviceEntries);
|
||||||
|
context.Response.Headers.Add("Content-Type", "application/json");
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_fakeConsulBuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_fakeConsulBuilder?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs
Normal file
69
test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
namespace Ocelot.UnitTests.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Internal;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ocelot.DependencyInjection;
|
||||||
|
using Provider.Consul;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotBuilderExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _services;
|
||||||
|
private IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configRoot;
|
||||||
|
private IOcelotBuilder _ocelotBuilder;
|
||||||
|
private Exception _ex;
|
||||||
|
|
||||||
|
public OcelotBuilderExtensionsTests()
|
||||||
|
{
|
||||||
|
_configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_services.AddSingleton<IHostingEnvironment, HostingEnvironment>();
|
||||||
|
_services.AddSingleton(_configRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_up_consul()
|
||||||
|
{
|
||||||
|
this.Given(x => WhenISetUpOcelotServices())
|
||||||
|
.When(x => WhenISetUpConsul())
|
||||||
|
.Then(x => ThenAnExceptionIsntThrown())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpOcelotServices()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ocelotBuilder = _services.AddOcelot(_configRoot);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_ex = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenISetUpConsul()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ocelotBuilder.AddConsul().AddConfigStoredInConsul();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_ex = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenAnExceptionIsntThrown()
|
||||||
|
{
|
||||||
|
_ex.ShouldBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
namespace Ocelot.UnitTests.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Infrastructure;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Ocelot.ServiceDiscovery.Providers;
|
||||||
|
using Provider.Consul;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Values;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class PollingConsulServiceDiscoveryProviderTests
|
||||||
|
{
|
||||||
|
private readonly int _delay;
|
||||||
|
private PollConsul _provider;
|
||||||
|
private readonly List<Service> _services;
|
||||||
|
private readonly Mock<IOcelotLoggerFactory> _factory;
|
||||||
|
private readonly Mock<IOcelotLogger> _logger;
|
||||||
|
private readonly Mock<IServiceDiscoveryProvider> _consulServiceDiscoveryProvider;
|
||||||
|
private List<Service> _result;
|
||||||
|
|
||||||
|
public PollingConsulServiceDiscoveryProviderTests()
|
||||||
|
{
|
||||||
|
_services = new List<Service>();
|
||||||
|
_delay = 1;
|
||||||
|
_factory = new Mock<IOcelotLoggerFactory>();
|
||||||
|
_logger = new Mock<IOcelotLogger>();
|
||||||
|
_factory.Setup(x => x.CreateLogger<PollConsul>()).Returns(_logger.Object);
|
||||||
|
_consulServiceDiscoveryProvider = new Mock<IServiceDiscoveryProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_service_from_consul()
|
||||||
|
{
|
||||||
|
var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List<string>());
|
||||||
|
|
||||||
|
this.Given(x => GivenConsulReturns(service))
|
||||||
|
.When(x => WhenIGetTheServices(1))
|
||||||
|
.Then(x => ThenTheCountIs(1))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenConsulReturns(Service service)
|
||||||
|
{
|
||||||
|
_services.Add(service);
|
||||||
|
_consulServiceDiscoveryProvider.Setup(x => x.Get()).ReturnsAsync(_services);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCountIs(int count)
|
||||||
|
{
|
||||||
|
_result.Count.ShouldBe(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetTheServices(int expected)
|
||||||
|
{
|
||||||
|
_provider = new PollConsul(_delay, _factory.Object, _consulServiceDiscoveryProvider.Object);
|
||||||
|
|
||||||
|
var result = Wait.WaitFor(3000).Until(() => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_result = _provider.Get().GetAwaiter().GetResult();
|
||||||
|
if (_result.Count == expected)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs
Normal file
44
test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
namespace Ocelot.UnitTests.Consul
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Logging;
|
||||||
|
using Provider.Consul;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ProviderFactoryTests
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _provider;
|
||||||
|
|
||||||
|
public ProviderFactoryTests()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var loggerFactory = new Mock<IOcelotLoggerFactory>();
|
||||||
|
var logger = new Mock<IOcelotLogger>();
|
||||||
|
loggerFactory.Setup(x => x.CreateLogger<Consul>()).Returns(logger.Object);
|
||||||
|
loggerFactory.Setup(x => x.CreateLogger<PollConsul>()).Returns(logger.Object);
|
||||||
|
var consulFactory = new Mock<IConsulClientFactory>();
|
||||||
|
services.AddSingleton<IConsulClientFactory>(consulFactory.Object);
|
||||||
|
services.AddSingleton<IOcelotLoggerFactory>(loggerFactory.Object);
|
||||||
|
_provider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_ConsulServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("", "", 1, "", "", 1), "");
|
||||||
|
provider.ShouldBeOfType<Consul>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_PollingConsulServiceDiscoveryProvider()
|
||||||
|
{
|
||||||
|
var stopsPollerFromPolling = 10000;
|
||||||
|
var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("pollconsul", "", 1, "", "", stopsPollerFromPolling), "");
|
||||||
|
provider.ShouldBeOfType<PollConsul>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
namespace Ocelot.UnitTests.Eureka
|
||||||
|
{
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using Ocelot.Configuration.Builder;
|
||||||
|
using Ocelot.Configuration.Repository;
|
||||||
|
using Provider.Eureka;
|
||||||
|
using Responses;
|
||||||
|
using Shouldly;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class EurekaMiddlewareConfigurationProviderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void should_not_build()
|
||||||
|
{
|
||||||
|
var configRepo = new Mock<IInternalConfigurationRepository>();
|
||||||
|
configRepo.Setup(x => x.Get())
|
||||||
|
.Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration(null, null, null, null, null, null, null, null)));
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IInternalConfigurationRepository>(configRepo.Object);
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp));
|
||||||
|
provider.ShouldBeOfType<Task>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_build()
|
||||||
|
{
|
||||||
|
var serviceProviderConfig = new ServiceProviderConfigurationBuilder().WithType("eureka").Build();
|
||||||
|
var client = new Mock<IDiscoveryClient>();
|
||||||
|
var configRepo = new Mock<IInternalConfigurationRepository>();
|
||||||
|
configRepo.Setup(x => x.Get())
|
||||||
|
.Returns(new OkResponse<IInternalConfiguration>(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null)));
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IInternalConfigurationRepository>(configRepo.Object);
|
||||||
|
services.AddSingleton<IDiscoveryClient>(client.Object);
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp));
|
||||||
|
provider.ShouldBeOfType<Task>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs
Normal file
34
test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
namespace Ocelot.UnitTests.Eureka
|
||||||
|
{
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Configuration.Builder;
|
||||||
|
using Provider.Eureka;
|
||||||
|
using Shouldly;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class EurekaProviderFactoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void should_not_get()
|
||||||
|
{
|
||||||
|
var config = new ServiceProviderConfigurationBuilder().Build();
|
||||||
|
var sp = new ServiceCollection().BuildServiceProvider();
|
||||||
|
var provider = EurekaProviderFactory.Get(sp, config, null);
|
||||||
|
provider.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_get()
|
||||||
|
{
|
||||||
|
var config = new ServiceProviderConfigurationBuilder().WithType("eureka").Build();
|
||||||
|
var client = new Mock<IDiscoveryClient>();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IDiscoveryClient>(client.Object);
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
var provider = EurekaProviderFactory.Get(sp, config, null);
|
||||||
|
provider.ShouldBeOfType<Eureka>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
namespace Ocelot.UnitTests.Eureka
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Provider.Eureka;
|
||||||
|
using Shouldly;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Values;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class EurekaServiceDiscoveryProviderTests
|
||||||
|
{
|
||||||
|
private readonly Eureka _provider;
|
||||||
|
private readonly Mock<IDiscoveryClient> _client;
|
||||||
|
private readonly string _serviceId;
|
||||||
|
private List<IServiceInstance> _instances;
|
||||||
|
private List<Service> _result;
|
||||||
|
|
||||||
|
public EurekaServiceDiscoveryProviderTests()
|
||||||
|
{
|
||||||
|
_serviceId = "Laura";
|
||||||
|
_client = new Mock<IDiscoveryClient>();
|
||||||
|
_provider = new Eureka(_serviceId, _client.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_empty_services()
|
||||||
|
{
|
||||||
|
this.When(_ => WhenIGet())
|
||||||
|
.Then(_ => ThenTheCountIs(0))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_service_from_client()
|
||||||
|
{
|
||||||
|
var instances = new List<IServiceInstance>
|
||||||
|
{
|
||||||
|
new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary<string, string>())
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(_ => GivenThe(instances))
|
||||||
|
.When(_ => WhenIGet())
|
||||||
|
.Then(_ => ThenTheCountIs(1))
|
||||||
|
.And(_ => ThenTheClientIsCalledCorrectly())
|
||||||
|
.And(_ => ThenTheServiceIsMapped())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_services_from_client()
|
||||||
|
{
|
||||||
|
var instances = new List<IServiceInstance>
|
||||||
|
{
|
||||||
|
new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary<string, string>()),
|
||||||
|
new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary<string, string>())
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(_ => GivenThe(instances))
|
||||||
|
.When(_ => WhenIGet())
|
||||||
|
.Then(_ => ThenTheCountIs(2))
|
||||||
|
.And(_ => ThenTheClientIsCalledCorrectly())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheServiceIsMapped()
|
||||||
|
{
|
||||||
|
_result[0].HostAndPort.DownstreamHost.ShouldBe("somehost");
|
||||||
|
_result[0].HostAndPort.DownstreamPort.ShouldBe(801);
|
||||||
|
_result[0].Name.ShouldBe(_serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheCountIs(int expected)
|
||||||
|
{
|
||||||
|
_result.Count.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheClientIsCalledCorrectly()
|
||||||
|
{
|
||||||
|
_client.Verify(x => x.GetInstances(_serviceId), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WhenIGet()
|
||||||
|
{
|
||||||
|
_result = await _provider.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThe(List<IServiceInstance> instances)
|
||||||
|
{
|
||||||
|
_instances = instances;
|
||||||
|
_client.Setup(x => x.GetInstances(It.IsAny<string>())).Returns(instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EurekaService : IServiceInstance
|
||||||
|
{
|
||||||
|
public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary<string, string> metadata)
|
||||||
|
{
|
||||||
|
ServiceId = serviceId;
|
||||||
|
Host = host;
|
||||||
|
Port = port;
|
||||||
|
IsSecure = isSecure;
|
||||||
|
Uri = uri;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ServiceId { get; }
|
||||||
|
public string Host { get; }
|
||||||
|
public int Port { get; }
|
||||||
|
public bool IsSecure { get; }
|
||||||
|
public Uri Uri { get; }
|
||||||
|
public IDictionary<string, string> Metadata { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
namespace Ocelot.UnitTests.Eureka
|
||||||
|
{
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Ocelot.DependencyInjection;
|
||||||
|
using Ocelot.Middleware;
|
||||||
|
using Ocelot.Middleware.Pipeline;
|
||||||
|
using Pivotal.Discovery.Client;
|
||||||
|
using Shouldly;
|
||||||
|
using Steeltoe.Common.Discovery;
|
||||||
|
using Steeltoe.Discovery.Eureka;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class OcelotPipelineExtensionsTests
|
||||||
|
{
|
||||||
|
private OcelotPipelineBuilder _builder;
|
||||||
|
private OcelotRequestDelegate _handlers;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_up_pipeline()
|
||||||
|
{
|
||||||
|
this.Given(_ => GivenTheDepedenciesAreSetUp())
|
||||||
|
.When(_ => WhenIBuild())
|
||||||
|
.Then(_ => ThenThePipelineIsBuilt())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenThePipelineIsBuilt()
|
||||||
|
{
|
||||||
|
_handlers.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIBuild()
|
||||||
|
{
|
||||||
|
_handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheDepedenciesAreSetUp()
|
||||||
|
{
|
||||||
|
IConfigurationBuilder test = new ConfigurationBuilder();
|
||||||
|
var root = test.Build();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IConfiguration>(root);
|
||||||
|
services.AddDiscoveryClient(new DiscoveryOptions
|
||||||
|
{
|
||||||
|
ClientType = DiscoveryClientType.EUREKA,
|
||||||
|
ClientOptions = new EurekaClientOptions()
|
||||||
|
{
|
||||||
|
ShouldFetchRegistry = false,
|
||||||
|
ShouldRegisterWithEureka = false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
services.AddOcelot();
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
_builder = new OcelotPipelineBuilder(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user