diff --git a/Ocelot.sln b/Ocelot.sln index d20e939e..f4b3aef3 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,90 +1,139 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - build-and-release-unstable.ps1 = build-and-release-unstable.ps1 - build-and-run-tests.ps1 = build-and-run-tests.ps1 - build.cake = build.cake - build.ps1 = build.ps1 - codeanalysis.ruleset = codeanalysis.ruleset - GitVersion.yml = GitVersion.yml - global.json = global.json - LICENSE.md = LICENSE.md - README.md = README.md - release.ps1 = release.ps1 - ReleaseNotes.md = ReleaseNotes.md - run-acceptance-tests.ps1 = run-acceptance-tests.ps1 - run-benchmarks.ps1 = run-benchmarks.ps1 - run-unit-tests.ps1 = run-unit-tests.ps1 - version.ps1 = version.ps1 - Dockerfile = Dockerfile - .dockerignore = .dockerignore - docker-compose.yaml = docker-compose.yaml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU - {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU - {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU - {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU - {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU - {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU - {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU - {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU - {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} - {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {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} - {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .gitignore = .gitignore + build-and-release-unstable.ps1 = build-and-release-unstable.ps1 + build-and-run-tests.ps1 = build-and-run-tests.ps1 + build.cake = build.cake + build.ps1 = build.ps1 + codeanalysis.ruleset = codeanalysis.ruleset + docker-compose.yaml = docker-compose.yaml + Dockerfile = Dockerfile + GitVersion.yml = GitVersion.yml + global.json = global.json + LICENSE.md = LICENSE.md + README.md = README.md + release.ps1 = release.ps1 + ReleaseNotes.md = ReleaseNotes.md + run-acceptance-tests.ps1 = run-acceptance-tests.ps1 + run-benchmarks.ps1 = run-benchmarks.ps1 + run-unit-tests.ps1 = run-unit-tests.ps1 + version.ps1 = version.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" +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 + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = 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.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 + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {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} + {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 + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 83b9431f..702948e0 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,3 @@ If you think this project is worth supporting financially please make a contribu ## Things that are currently annoying me [![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) - - - diff --git a/build.cake b/build.cake index ef8cb5c6..86843018 100644 --- a/build.cake +++ b/build.cake @@ -17,7 +17,7 @@ var artifactsDir = Directory("artifacts"); // unit testing var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; -var minCodeCoverage = 82d; +var minCodeCoverage = 80d; var coverallsRepoToken = "coveralls-repo-token-ocelot"; var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot"; @@ -263,14 +263,31 @@ Task("CreatePackages") .Does(() => { EnsureDirectoryExists(packagesDir); - CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir); + + CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir); //GenerateReleaseNotes(releaseNotesFile); - System.IO.File.WriteAllLines(artifactsFile, new[]{ - "nuget:Ocelot." + buildVersion + ".nupkg", - //"releaseNotes:releasenotes.md" - }); + var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg"); + + foreach(var projectFile in projectFiles) + { + System.IO.File.AppendAllLines(artifactsFile, new[]{ + projectFile.GetFilename().FullPath, + //"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) { @@ -345,8 +362,6 @@ Task("DownloadGitHubReleaseArtifacts") Information("Release url " + releaseUrl); - //var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)); - var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) .GetValue("assets_url") .Value(); @@ -451,21 +466,23 @@ private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFi { var artifacts = System.IO.File .ReadAllLines(artifactsFile) - .Select(l => l.Split(':')) - .ToDictionary(v => v[0], v => v[1]); - - var codePackage = packagesDir + File(artifacts["nuget"]); - - Information("Pushing package " + codePackage); + .Distinct(); - Information("Calling NuGetPush"); + foreach(var artifact in artifacts) + { + var codePackage = packagesDir + File(artifact); - NuGetPush( - codePackage, - new NuGetPushSettings { - ApiKey = feedApiKey, - Source = codeFeedUrl - }); + Information("Pushing package " + codePackage); + + Information("Calling NuGetPush"); + + NuGetPush( + codePackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); + } } /// gets the resource from the specified url diff --git a/build.ps1 b/build.ps1 index 1287451b..c6c91b25 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,235 +1,242 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER ShowDescription -Shows description about tasks. -.PARAMETER DryRun -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 -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -https://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [switch]$Experimental, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) -{ - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } - - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } -} - -function GetProxyEnabledWebClient -{ - $wc = New-Object System.Net.WebClient - $proxy = [System.Net.WebRequest]::GetSystemWebProxy() - $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials - $wc.Proxy = $proxy - return $wc -} - -Write-Host "Preparing to run build script..." - -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} - -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" -$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" -$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" -$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" - -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null -} - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { - Throw "Could not download packages.config." - } -} - -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } -} - -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | - Remove-Item -Recurse - } - - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet tools." - } - else - { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" - } - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Restore addins from NuGet -if (Test-Path $ADDINS_PACKAGES_CONFIG) { - Push-Location - Set-Location $ADDINS_DIR - - Write-Verbose -Message "Restoring addins from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet addins." - } - - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Restore modules from NuGet -if (Test-Path $MODULES_PACKAGES_CONFIG) { - Push-Location - Set-Location $MODULES_DIR - - Write-Verbose -Message "Restoring modules from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet modules." - } - - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} - - - -# Build Cake arguments -$cakeArguments = @("$Script"); -if ($Target) { $cakeArguments += "-target=$Target" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -if ($Experimental) { $cakeArguments += "-experimental" } -if ($Mono) { $cakeArguments += "-mono" } -$cakeArguments += $ScriptArgs - -# Start Cake -Write-Host "Running build script..." -&$CAKE_EXE $cakeArguments -exit $LASTEXITCODE \ No newline at end of file +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER ShowDescription +Shows description about tasks. +.PARAMETER DryRun +Performs a dry run. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. + +.LINK +https://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [string]$Target, + [string]$Configuration, + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity, + [switch]$ShowDescription, + [Alias("WhatIf", "Noop")] + [switch]$DryRun, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [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 +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +function GetProxyEnabledWebClient +{ + $wc = New-Object System.Net.WebClient + $proxy = [System.Net.WebRequest]::GetSystemWebProxy() + $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials + $wc.Proxy = $proxy + return $wc +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" +$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" +$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" +$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { + $wc = GetProxyEnabledWebClient + $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) + } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + $wc = GetProxyEnabledWebClient + $wc.DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | + Remove-Item -Recurse + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location +} + +# Restore addins from NuGet +if (Test-Path $ADDINS_PACKAGES_CONFIG) { + Push-Location + Set-Location $ADDINS_DIR + + Write-Verbose -Message "Restoring addins from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred while restoring NuGet addins." + } + + Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location +} + +# Restore modules from NuGet +if (Test-Path $MODULES_PACKAGES_CONFIG) { + Push-Location + Set-Location $MODULES_DIR + + Write-Verbose -Message "Restoring modules from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred while restoring NuGet modules." + } + + Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + + + +# Build Cake arguments +$cakeArguments = @("$Script"); +if ($Target) { $cakeArguments += "-target=$Target" } +if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } +if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } +if ($ShowDescription) { $cakeArguments += "-showdescription" } +if ($DryRun) { $cakeArguments += "-dryrun" } +$cakeArguments += $ScriptArgs + +# Start Cake +Write-Host "Running build script..." +&$CAKE_EXE $cakeArguments +exit $LASTEXITCODE diff --git a/docs/building/releaseprocess.rst b/docs/building/releaseprocess.rst index 3cfed2f0..b107afc0 100644 --- a/docs/building/releaseprocess.rst +++ b/docs/building/releaseprocess.rst @@ -1,23 +1,23 @@ -Release process -=============== - -This section defines the release process for the maintainers of the project. -* Merge pull requests to the `release` branch. - -* Every commit pushed to the Origin repo will kick off the `ocelot-build `_ project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed. - -* When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory. - -* When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub. - -* In Github, navigate to the `release `_. Modify the release name and tag as desired. - -* When you're ready, publish the release. This will tag the commit with the specified release number. - -* The `ocelot-release `_ project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed. - -* When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing. - -* Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub. - - +Release process +=============== + +This section defines the release process for the maintainers of the project. +* Merge pull requests to the `release` branch. + +* Every commit pushed to the Origin repo will kick off the `ocelot-build `_ project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed. + +* When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory. + +* When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub. + +* In Github, navigate to the `release `_. Modify the release name and tag as desired. + +* When you're ready, publish the release. This will tag the commit with the specified release number. + +* The `ocelot-release `_ project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed. + +* When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing. + +* Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub. + + diff --git a/docs/features/administration.rst b/docs/features/administration.rst index 9d907585..4d891994 100644 --- a/docs/features/administration.rst +++ b/docs/features/administration.rst @@ -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. -This feature was implemented for `issue 228 `_. It is useful because the IdentityServer authentication +This feature was implemented for `issue 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. Internal IdentityServer diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f7ba7719..4e692fff 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -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. -If you look at the example `here `_ you can see how the cache manager +If you look at the example `here `_ 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 the CacheManager package and just pass them in. diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index dfe30c5a..8e295032 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,7 +1,7 @@ Configuration ============ -An example configuration can be found `here `_. +An example configuration can be found `here `_. 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 configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful diff --git a/docs/features/delegatinghandlers.rst b/docs/features/delegatinghandlers.rst index 7c354760..1da13c5f 100644 --- a/docs/features/delegatinghandlers.rst +++ b/docs/features/delegatinghandlers.rst @@ -1,8 +1,8 @@ Delegating Handlers =================== -Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ -and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 `_. +Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ +and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 `_. Usage ^^^^^ diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index 8c1f1459..ed772d61 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -1,150 +1,150 @@ -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 `_ and I decided that it was going to be useful in various ways. - -Add to Request -^^^^^^^^^^^^^^ - -This feature was requestes in `GitHub #313 `_. - -If you want to add a header to your upstream request please add the following to a ReRoute in your ocelot.json: - -.. code-block:: json - - "UpstreamHeaderTransform": { - "Uncle": "Bob" - } - -In the example above a header with the key Uncle and value Bob would be send to to the upstream service. - -Placeholders are supported too (see below). - -Add to Response -^^^^^^^^^^^^^^^ - -This feature was requested in `GitHub #280 `_. - -If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json.. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Uncle": "Bob" - }, - -In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. - -If you want to return the Butterfly APM trace id then do something like the following.. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "AnyKey": "{TraceId}" - }, - -Find and Replace -^^^^^^^^^^^^^^^^ - -In order to transform a header first we specify the header key and then the type of transform we want e.g. - -.. code-block:: json - - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - -The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. - -Pre Downstream Request -^^^^^^^^^^^^^^^^^^^^^^ - -Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. - -.. code-block:: json - - "UpstreamHeaderTransform": { - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - -Post Downstream Request -^^^^^^^^^^^^^^^^^^^^^^^ - -Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - -Placeholders -^^^^^^^^^^^^ - -Ocelot allows placeholders that can be used in header transformation. - -{RemoteIpAddress} - This will find the clients IP address using _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString() so you will get back some IP. -{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. -{DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. -{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. - -Handling 302 Redirects -^^^^^^^^^^^^^^^^^^^^^^ -Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -or you could use the BaseUrl placeholder. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "http://localhost:6773, {BaseUrl}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -finally if you are using a load balancer with Ocelot you will get multiple downstream base urls so the above would not work. In this case you can do the following. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "{DownstreamBaseUrl}, {BaseUrl}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -X-Forwarded-For -^^^^^^^^^^^^^^^ - -An example of using {RemoteIpAddress} placeholder... - -.. code-block:: json - - "UpstreamHeaderTransform": { - "X-Forwarded-For": "{RemoteIpAddress}" - } - -Future -^^^^^^ - -Ideally this feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one. -It would also be nice if it could multi find and replace e.g. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "[{one,one},{two,two}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -If anyone wants to have a go at this please help yourself!! \ No newline at end of file +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 `_ and I decided that it was going to be useful in various ways. + +Add to Request +^^^^^^^^^^^^^^ + +This feature was requestes in `GitHub #313 `_. + +If you want to add a header to your upstream request please add the following to a ReRoute in your ocelot.json: + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Uncle": "Bob" + } + +In the example above a header with the key Uncle and value Bob would be send to to the upstream service. + +Placeholders are supported too (see below). + +Add to Response +^^^^^^^^^^^^^^^ + +This feature was requested in `GitHub #280 `_. + +If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Uncle": "Bob" + }, + +In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. + +If you want to return the Butterfly APM trace id then do something like the following.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "AnyKey": "{TraceId}" + }, + +Find and Replace +^^^^^^^^^^^^^^^^ + +In order to transform a header first we specify the header key and then the type of transform we want e.g. + +.. code-block:: json + + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + +The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. + +Pre Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Post Downstream Request +^^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Placeholders +^^^^^^^^^^^^ + +Ocelot allows placeholders that can be used in header transformation. + +{RemoteIpAddress} - This will find the clients IP address using _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString() so you will get back some IP. +{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. +{DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. +{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. + +Handling 302 Redirects +^^^^^^^^^^^^^^^^^^^^^^ +Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +or you could use the BaseUrl placeholder. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://localhost:6773, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +finally if you are using a load balancer with Ocelot you will get multiple downstream base urls so the above would not work. In this case you can do the following. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "{DownstreamBaseUrl}, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +X-Forwarded-For +^^^^^^^^^^^^^^^ + +An example of using {RemoteIpAddress} placeholder... + +.. code-block:: json + + "UpstreamHeaderTransform": { + "X-Forwarded-For": "{RemoteIpAddress}" + } + +Future +^^^^^^ + +Ideally this feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one. +It would also be nice if it could multi find and replace e.g. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "[{one,one},{two,two}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +If anyone wants to have a go at this please help yourself!! diff --git a/docs/features/raft.rst b/docs/features/raft.rst index e3793b1f..0ce09947 100644 --- a/docs/features/raft.rst +++ b/docs/features/raft.rst @@ -1,49 +1,49 @@ -Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION) -============================================ - -Ocelot has recently integrated `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). - -To get Raft support you must first install the Ocelot Rafty package. - -``Install-Package Ocelot.Provider.Rafty`` - -Then you must make the following changes to your Startup.cs / Program.cs. - -.. code-block:: csharp - - public virtual void ConfigureServices(IServiceCollection services) - { - services - .AddOcelot() - .AddAdministration("/administration", "secret") - .AddRafty(); - } - -In addition to this you must add a file called peers.json to your main project and it will look as follows - -.. code-block:: json - - { - "Peers": [{ - "HostAndPort": "http://localhost:5000" - }, - { - "HostAndPort": "http://localhost:5002" - }, - { - "HostAndPort": "http://localhost:5003" - }, - { - "HostAndPort": "http://localhost:5004" - }, - { - "HostAndPort": "http://localhost:5001" - } - ] - } - -Each instance of Ocelot must have it's address in the array so that they can communicate using Rafty. - -Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting their configuration. +Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION) +============================================ + +Ocelot has recently integrated `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). + +To get Raft support you must first install the Ocelot Rafty package. + +``Install-Package Ocelot.Provider.Rafty`` + +Then you must make the following changes to your Startup.cs / Program.cs. + +.. code-block:: csharp + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot() + .AddAdministration("/administration", "secret") + .AddRafty(); + } + +In addition to this you must add a file called peers.json to your main project and it will look as follows + +.. code-block:: json + + { + "Peers": [{ + "HostAndPort": "http://localhost:5000" + }, + { + "HostAndPort": "http://localhost:5002" + }, + { + "HostAndPort": "http://localhost:5003" + }, + { + "HostAndPort": "http://localhost:5004" + }, + { + "HostAndPort": "http://localhost:5001" + } + ] + } + +Each instance of Ocelot must have it's address in the array so that they can communicate using Rafty. + +Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting their configuration. diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index 21c5c8f9..0c5c992d 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -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 architecture with Ocelot. -This feature was requested as part of `Issue 79 `_ and further improvements were made as part of `Issue 298 `_. +This feature was requested as part of `Issue 79 `_ and further improvements were made as part of `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. 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. diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 2ac2230e..f866f60e 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -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. -This feature was requested as part of `Issue 216 `_ . +This feature was requested as part of `Issue 216 `_ . Priority ^^^^^^^^ 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 `_ for reference +See `Issue 270 `_ for reference .. code-block:: json @@ -181,7 +181,7 @@ matched /goods/{catchAll} (because this is the first ReRoute in the list!). Dynamic Routing ^^^^^^^^^^^^^^^ -This feature was requested in `issue 340 `_. +This feature was requested in `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 this sounds interesting to you. diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 5a1f85ac..0bcb611e 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -113,7 +113,7 @@ Ocelot will add this token to the consul client that it uses to make requests an Eureka ^^^^^^ -This feature was requested as part of `Issue 262 `_ . to add support for Netflix's +This feature was requested as part of `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 `_ which is something to do with `Pivotal `_! 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 ^^^^^^^^^^^^^^^ -This feature was requested in `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 `_. 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 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. diff --git a/src/Ocelot.Administration/IIdentityServerConfiguration.cs b/src/Ocelot.Administration/IIdentityServerConfiguration.cs new file mode 100644 index 00000000..eb389cf9 --- /dev/null +++ b/src/Ocelot.Administration/IIdentityServerConfiguration.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Administration +{ + using System.Collections.Generic; + + public interface IIdentityServerConfiguration + { + string ApiName { get; } + string ApiSecret { get; } + bool RequireHttps { get; } + List AllowedScopes { get; } + string CredentialsSigningCertificateLocation { get; } + string CredentialsSigningCertificatePassword { get; } + } +} diff --git a/src/Ocelot.Administration/IdentityServerConfiguration.cs b/src/Ocelot.Administration/IdentityServerConfiguration.cs new file mode 100644 index 00000000..95e64d01 --- /dev/null +++ b/src/Ocelot.Administration/IdentityServerConfiguration.cs @@ -0,0 +1,30 @@ +namespace Ocelot.Administration +{ + using System.Collections.Generic; + + public class IdentityServerConfiguration : IIdentityServerConfiguration + { + public IdentityServerConfiguration( + string apiName, + bool requireHttps, + string apiSecret, + List 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 AllowedScopes { get; } + public string ApiSecret { get; } + public string CredentialsSigningCertificateLocation { get; } + public string CredentialsSigningCertificatePassword { get; } + } +} diff --git a/src/Ocelot.Administration/IdentityServerConfigurationCreator.cs b/src/Ocelot.Administration/IdentityServerConfigurationCreator.cs new file mode 100644 index 00000000..970a4588 --- /dev/null +++ b/src/Ocelot.Administration/IdentityServerConfigurationCreator.cs @@ -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 { "admin", "openid", "offline_access" }, + credentialsSigningCertificateLocation, + credentialsSigningCertificatePassword + ); + } + } +} diff --git a/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs b/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs new file mode 100644 index 00000000..3ad31b69 --- /dev/null +++ b/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs @@ -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(); + + 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(); + + if (identityServerConfiguration != null) + { + app.UseIdentityServer(); + } + + app.UseAuthentication(); + app.UseMvc(); + }); + } + + return Task.CompletedTask; + }; + } +} diff --git a/src/Ocelot.Administration/Ocelot.Administration.csproj b/src/Ocelot.Administration/Ocelot.Administration.csproj new file mode 100644 index 00000000..ca518938 --- /dev/null +++ b/src/Ocelot.Administration/Ocelot.Administration.csproj @@ -0,0 +1,38 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use the administration API and IdentityService dependencies that come with it + Ocelot.Administration + 0.0.0-dev + Ocelot.Administration + Ocelot.Administration + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Administration + https://github.com/ThreeMammals/Ocelot.Administration + http://threemammals.com/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + all + + + + + diff --git a/src/Ocelot.Administration/OcelotBuilderExtensions.cs b/src/Ocelot.Administration/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..caafa4df --- /dev/null +++ b/src/Ocelot.Administration/OcelotBuilderExtensions.cs @@ -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(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(administrationPath); + return new OcelotAdministrationBuilder(builder.Services, builder.Configuration); + } + + public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, Action configureOptions) + { + var administrationPath = new AdministrationPath(path); + builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); + + if (configureOptions != null) + { + AddIdentityServer(configureOptions, builder); + } + + builder.Services.AddSingleton(administrationPath); + return new OcelotAdministrationBuilder(builder.Services, builder.Configuration); + } + + private static void AddIdentityServer(Action 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(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 Resources(IIdentityServerConfiguration identityServerConfiguration) + { + return new List + { + new ApiResource(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName) + { + ApiSecrets = new List + { + new Secret + { + Value = identityServerConfiguration.ApiSecret.Sha256() + } + } + }, + }; + } + + private static List Client(IIdentityServerConfiguration identityServerConfiguration) + { + return new List + { + new Client + { + ClientId = identityServerConfiguration.ApiName, + AllowedGrantTypes = GrantTypes.ClientCredentials, + ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, + AllowedScopes = { identityServerConfiguration.ApiName } + } + }; + } + } +} diff --git a/src/Ocelot.Administration/Properties/AssemblyInfo.cs b/src/Ocelot.Administration/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Administration/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj b/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj new file mode 100644 index 00000000..32271347 --- /dev/null +++ b/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj @@ -0,0 +1,38 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use CacheManager.Net + Ocelot.Cache.CacheManager + 0.0.0-dev + Ocelot.Cache.CacheManager + Ocelot.Cache.CacheManager + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Cache.CacheManager + https://github.com/ThreeMammals/Ocelot.Cache.CacheManager + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + all + + + + + + diff --git a/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..d96ce919 --- /dev/null +++ b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs @@ -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 settings) + { + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); + var ocelotOutputCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + + builder.Services.RemoveAll(typeof(ICacheManager)); + builder.Services.RemoveAll(typeof(IOcelotCache)); + builder.Services.AddSingleton>(cacheManagerOutputCache); + builder.Services.AddSingleton>(ocelotOutputCacheManager); + + var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); + var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); + builder.Services.RemoveAll(typeof(ICacheManager)); + builder.Services.RemoveAll(typeof(IOcelotCache)); + builder.Services.AddSingleton>(ocelotConfigCacheManagerOutputCache); + builder.Services.AddSingleton>(ocelotConfigCacheManager); + + var fileConfigCacheManagerOutputCache = CacheFactory.Build("FileConfigurationCache", settings); + var fileConfigCacheManager = new OcelotCacheManagerCache(fileConfigCacheManagerOutputCache); + builder.Services.RemoveAll(typeof(ICacheManager)); + builder.Services.RemoveAll(typeof(IOcelotCache)); + builder.Services.AddSingleton>(fileConfigCacheManagerOutputCache); + builder.Services.AddSingleton>(fileConfigCacheManager); + return builder; + } + } +} diff --git a/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs b/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs new file mode 100644 index 00000000..7165edb5 --- /dev/null +++ b/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs @@ -0,0 +1,42 @@ +namespace Ocelot.Cache.CacheManager +{ + using System; + using global::CacheManager.Core; + + public class OcelotCacheManagerCache : IOcelotCache + { + private readonly ICacheManager _cacheManager; + + public OcelotCacheManagerCache(ICacheManager cacheManager) + { + _cacheManager = cacheManager; + } + + public void Add(string key, T value, TimeSpan ttl, string region) + { + _cacheManager.Add(new CacheItem(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(key, region); + } + + public void ClearRegion(string region) + { + _cacheManager.ClearRegion(region); + } + } +} diff --git a/src/Ocelot.Cache.CacheManager/Properties/AssemblyInfo.cs b/src/Ocelot.Cache.CacheManager/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Cache.CacheManager/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs new file mode 100644 index 00000000..b202cd57 --- /dev/null +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -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(); + _config = config; + _consul = clientFactory.Get(_config); + } + + public async Task> Get() + { + var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + + var services = new List(); + + 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()); + } + + 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 strings) + { + return strings + ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); + } + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs new file mode 100644 index 00000000..de1f30aa --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -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; + } + }); + } + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs new file mode 100644 index 00000000..01273a35 --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -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 _cache; + private readonly IOcelotLogger _logger; + + public ConsulFileConfigurationRepository( + Cache.IOcelotCache cache, + IInternalConfigurationRepository repo, + IConsulClientFactory factory, + IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _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> Get() + { + var config = _cache.Get(_configurationKey, _configurationKey); + + if (config != null) + { + return new OkResponse(config); + } + + var queryResult = await _consul.KV.Get(_configurationKey); + + if (queryResult.Response == null) + { + return new OkResponse(null); + } + + var bytes = queryResult.Response.Value; + + var json = Encoding.UTF8.GetString(bytes); + + var consulConfig = JsonConvert.DeserializeObject(json); + + return new OkResponse(consulConfig); + } + + public async Task 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}")); + } + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs new file mode 100644 index 00000000..a25d3752 --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs @@ -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(); + var fileConfig = builder.ApplicationServices.GetService>(); + var internalConfigCreator = builder.ApplicationServices.GetService(); + var internalConfigRepo = builder.ApplicationServices.GetService(); + + 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 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 fileConfigFromConsul) + { + return fileConfigFromConsul.Data == null; + } + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs new file mode 100644 index 00000000..53095404 --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -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(); + + var consulFactory = provider.GetService(); + + 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; + }; + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs new file mode 100644 index 00000000..a84088b0 --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs @@ -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; } + } +} diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs new file mode 100644 index 00000000..3710a818 --- /dev/null +++ b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs @@ -0,0 +1,9 @@ +namespace Ocelot.Provider.Consul +{ + using global::Consul; + + public interface IConsulClientFactory + { + IConsulClient Get(ConsulRegistryConfiguration config); + } +} diff --git a/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj b/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj new file mode 100644 index 00000000..e0ae127e --- /dev/null +++ b/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj @@ -0,0 +1,37 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use Consul + Ocelot.Provider.Consul + 0.0.0-dev + Ocelot.Provider.Consul + Ocelot.Provider.Consul + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Consul + https://github.com/ThreeMammals/Ocelot.Provider.Consul + http://threemammals.com/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + + all + + + diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..addbac91 --- /dev/null +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -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(ConsulProviderFactory.Get); + builder.Services.AddSingleton(); + return builder; + } + + public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) + { + builder.Services.AddSingleton(ConsulMiddlewareConfigurationProvider.Get); + builder.Services.AddHostedService(); + builder.Services.AddSingleton(); + return builder; + } + } +} diff --git a/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs b/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs new file mode 100644 index 00000000..32470153 --- /dev/null +++ b/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs @@ -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 _services; + + public PollConsul(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) + { + _logger = factory.CreateLogger(); + _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; + _services = new List(); + + _timer = new Timer(async x => + { + if (_polling) + { + return; + } + + _polling = true; + await Poll(); + _polling = false; + }, null, pollingInterval, pollingInterval); + } + + public Task> Get() + { + return Task.FromResult(_services); + } + + private async Task Poll() + { + _services = await _consulServiceDiscoveryProvider.Get(); + } + } +} diff --git a/src/Ocelot.Provider.Consul/Properties/AssemblyInfo.cs b/src/Ocelot.Provider.Consul/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Provider.Consul/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs new file mode 100644 index 00000000..b0163916 --- /dev/null +++ b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Provider.Consul +{ + using Errors; + + public class UnableToSetConfigInConsulError : Error + { + public UnableToSetConfigInConsulError(string s) + : base(s, OcelotErrorCode.UnknownError) + { + } + } +} diff --git a/src/Ocelot.Provider.Eureka/Eureka.cs b/src/Ocelot.Provider.Eureka/Eureka.cs new file mode 100644 index 00000000..b4f5fd3b --- /dev/null +++ b/src/Ocelot.Provider.Eureka/Eureka.cs @@ -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> Get() + { + var services = new List(); + + 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()))); + } + + return Task.FromResult(services); + } + } +} diff --git a/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs new file mode 100644 index 00000000..fa19f2ea --- /dev/null +++ b/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs @@ -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(); + + 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"; + } + } +} diff --git a/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs new file mode 100644 index 00000000..b35fe7c8 --- /dev/null +++ b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs @@ -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(); + + if (config.Type?.ToLower() == "eureka" && client != null) + { + return new Eureka(name, client); + } + + return null; + }; + } +} diff --git a/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj b/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj new file mode 100644 index 00000000..c59c1a37 --- /dev/null +++ b/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj @@ -0,0 +1,37 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use Eureka + Ocelot.Provider.Eureka + 0.0.0-dev + Ocelot.Provider.Eureka + Ocelot.Provider.Eureka + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Eureka + https://github.com/ThreeMammals/Ocelot.Provider.Eureka + http://threemammals.com/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + + all + + + diff --git a/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..75a85767 --- /dev/null +++ b/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs @@ -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(EurekaProviderFactory.Get); + builder.Services.AddSingleton(EurekaMiddlewareConfigurationProvider.Get); + return builder; + } + } +} diff --git a/src/Ocelot.Provider.Eureka/Properties/AssemblyInfo.cs b/src/Ocelot.Provider.Eureka/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Provider.Eureka/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Provider.Polly/CircuitBreaker.cs b/src/Ocelot.Provider.Polly/CircuitBreaker.cs new file mode 100644 index 00000000..b47f6be7 --- /dev/null +++ b/src/Ocelot.Provider.Polly/CircuitBreaker.cs @@ -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; } + } +} diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj new file mode 100644 index 00000000..e4512cec --- /dev/null +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -0,0 +1,37 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use Polly.NET + Ocelot.Provider.Polly + 0.0.0-dev + Ocelot.Provider.Polly + Ocelot.Provider.Polly + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Polly + https://github.com/ThreeMammals/Ocelot.Provider.Polly + http://threemammals.com/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + all + + + + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..ae7e881c --- /dev/null +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -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> + { + {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; + } + } +} diff --git a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs new file mode 100644 index 00000000..4a17cd2d --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs @@ -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(); + } + + protected override async Task 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; + } + } + } +} diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs new file mode 100644 index 00000000..a95eb361 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs @@ -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(); + + Enum.TryParse(reRoute.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); + + _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(reRoute.QosOptions.TimeoutValue), strategy); + + _circuitBreakerPolicy = Policy + .Handle() + .Or() + .Or() + .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; } + } +} diff --git a/src/Ocelot.Provider.Polly/Properties/AssemblyInfo.cs b/src/Ocelot.Provider.Polly/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Provider.Polly/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Provider.Polly/RequestTimedOutError.cs b/src/Ocelot.Provider.Polly/RequestTimedOutError.cs new file mode 100644 index 00000000..4e910b12 --- /dev/null +++ b/src/Ocelot.Provider.Polly/RequestTimedOutError.cs @@ -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) + { + } + } +} diff --git a/src/Ocelot.Provider.Rafty/BearerToken.cs b/src/Ocelot.Provider.Rafty/BearerToken.cs new file mode 100644 index 00000000..c006aaf3 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/BearerToken.cs @@ -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; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FakeCommand.cs b/src/Ocelot.Provider.Rafty/FakeCommand.cs new file mode 100644 index 00000000..de611da7 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FakeCommand.cs @@ -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; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeer.cs b/src/Ocelot.Provider.Rafty/FilePeer.cs new file mode 100644 index 00000000..4bb57548 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeer.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Provider.Rafty +{ + public class FilePeer + { + public string HostAndPort { get; set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeers.cs b/src/Ocelot.Provider.Rafty/FilePeers.cs new file mode 100644 index 00000000..4d5f9e39 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeers.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Collections.Generic; + + public class FilePeers + { + public FilePeers() + { + Peers = new List(); + } + + public List Peers { get; set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeersProvider.cs b/src/Ocelot.Provider.Rafty/FilePeersProvider.cs new file mode 100644 index 00000000..1f9300bc --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeersProvider.cs @@ -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 _options; + private readonly List _peers; + private IBaseUrlFinder _finder; + private IInternalConfigurationRepository _repo; + private IIdentityServerConfiguration _identityServerConfig; + + public FilePeersProvider(IOptions options, IBaseUrlFinder finder, IInternalConfigurationRepository repo, IIdentityServerConfiguration identityServerConfig) + { + _identityServerConfig = identityServerConfig; + _repo = repo; + _finder = finder; + _options = options; + _peers = new List(); + + 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 Get() + { + return _peers; + } + } +} diff --git a/src/Ocelot.Provider.Rafty/HttpPeer.cs b/src/Ocelot.Provider.Rafty/HttpPeer.cs new file mode 100644 index 00000000..11397293 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/HttpPeer.cs @@ -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 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(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + } + + return new RequestVoteResponse(false, requestVote.Term); + } + + public async Task 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(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> Request(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>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + return new OkResponse((T)okResponse.Command); + } + + Console.WriteLine("REQUEST NOT OK...."); + return new ErrorResponse(await response.Content.ReadAsStringAsync(), command); + } + + private async Task SetToken() + { + var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", _identityServerConfiguration.ApiName), + new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), + new KeyValuePair("scope", _identityServerConfiguration.ApiName), + new KeyValuePair("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(responseContent); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj new file mode 100644 index 00000000..1e21afc3 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj @@ -0,0 +1,39 @@ + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + Provides Ocelot extensions to use Rafty + Ocelot.Provider.Rafty + 0.0.0-dev + Ocelot.Provider.Rafty + Ocelot.Provider.Rafty + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Rafty + https://github.com/ThreeMammals/Ocelot.Provider.Rafty + http://threemammals.com/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + + + full + True + + + + + + + + + + all + + + diff --git a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs new file mode 100644 index 00000000..a0487659 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs @@ -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(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(settings); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.Configure(builder.ConfigurationRoot); + return builder; + } + } +} diff --git a/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs b/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs new file mode 100644 index 00000000..c7dd3ef6 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs @@ -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); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs b/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dd8b7610 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Ocelot.Provider.Rafty/RaftController.cs b/src/Ocelot.Provider.Rafty/RaftController.cs new file mode 100644 index 00000000..9cf51dd6 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftController.cs @@ -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(); + _node = node; + } + + [Route("appendentries")] + public async Task AppendEntries() + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var appendEntries = JsonConvert.DeserializeObject(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 RequestVote() + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var requestVote = JsonConvert.DeserializeObject(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 Command() + { + try + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var command = JsonConvert.DeserializeObject(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; + } + } + } +} diff --git a/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs b/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs new file mode 100644 index 00000000..bf26ab07 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs @@ -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 Set(FileConfiguration fileConfiguration) + { + var result = await _node.Accept(new UpdateFileConfiguration(fileConfiguration)); + + if (result.GetType() == typeof(ErrorResponse)) + { + return new Responses.ErrorResponse(new UnableToSaveAcceptCommand($"unable to save file configuration to state machine")); + } + + return new Responses.OkResponse(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs new file mode 100644 index 00000000..e8c3ad5c --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs @@ -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(); + if (node != null) + { + return true; + } + + return false; + } + + private static void SetUpRafty(IApplicationBuilder builder) + { + var applicationLifetime = builder.ApplicationServices.GetService(); + applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder)); + var node = builder.ApplicationServices.GetService(); + var nodeId = builder.ApplicationServices.GetService(); + node.Start(nodeId); + } + + private static void OnShutdown(IApplicationBuilder app) + { + var node = app.ApplicationServices.GetService(); + node.Stop(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/SqlLiteLog.cs b/src/Ocelot.Provider.Rafty/SqlLiteLog.cs new file mode 100644 index 00000000..618b4851 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/SqlLiteLog.cs @@ -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(); + _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 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 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(data, jsonSerializerSettings); + if (log != null && log.Term > result) + { + result = log.Term; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task 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 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(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 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(data, jsonSerializerSettings); + + if (logEntry != null && log != null && logEntry.Term == log.Term) + { + _sempaphore.Release(); + return true; + } + } + } + + _sempaphore.Release(); + return false; + } + + public async Task 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(data, jsonSerializerSettings); + _sempaphore.Release(); + return log; + } + } + } + + public async Task> 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(data, jsonSerializerSettings); + logsToReturn.Add((id, log)); + } + } + } + + _sempaphore.Release(); + return logsToReturn; + } + } + + public async Task 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(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(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs b/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs new file mode 100644 index 00000000..888987ba --- /dev/null +++ b/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Provider.Rafty +{ + using Errors; + public class UnableToSaveAcceptCommand : Error + { + public UnableToSaveAcceptCommand(string message) + : base(message, OcelotErrorCode.UnknownError) + { + } + } +} diff --git a/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs b/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs new file mode 100644 index 00000000..894a758a --- /dev/null +++ b/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs @@ -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; } + } +} diff --git a/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs b/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs new file mode 100644 index 00000000..d8d1dd84 --- /dev/null +++ b/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs @@ -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(); + } + + 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(x.Key, x.Value.GetValue())).GetEnumerator())) + { + spanBuilder.AsChildOf(spanContext); + } + + span = _tracer.Start(spanBuilder); + httpContext.SetSpan(span); + } + + span?.Log(LogField.CreateNew().Event(@event)); + } + + public Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken, + Action addTraceIdToRepo, + Func> baseSendAsync) + { + return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken, addTraceIdToRepo, baseSendAsync)); + } + + protected virtual async Task TracingSendAsync( + ISpan span, + HttpRequestMessage request, + CancellationToken cancellationToken, + Action addTraceIdToRepo, + Func> 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; + } + } +} diff --git a/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj b/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj new file mode 100644 index 00000000..c161f183 --- /dev/null +++ b/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + 2.0.0 + 2.0.0 + true + This package provides methods to integrate Butterfly tracing with Ocelot. + Ocelot.Tracing.Butterfly + 0.0.0-dev + Ocelot.Tracing.Butterfly + Ocelot.Tracing.Butterfly + API Gateway;.NET core; Butterfly; ButterflyAPM + https://github.com/ThreeMammals/Ocelot + https://github.com/ThreeMammals/Ocelot + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + Ocelot.Tracing.Butterfly + + + full + True + + + + + + + + + + diff --git a/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs b/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs new file mode 100644 index 00000000..4a543a2e --- /dev/null +++ b/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs @@ -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 settings) + { + builder.Services.AddSingleton(); + builder.Services.AddButterfly(settings); + return builder; + } + } +} diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 81891d2f..5892aa96 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -10,8 +10,8 @@ Ocelot Ocelot API Gateway;.NET core - https://github.com/TomPallister/Ocelot - https://github.com/TomPallister/Ocelot + https://github.com/ThreeMammals/Ocelot + https://github.com/ThreeMammals/Ocelot http://threemammals.com/images/ocelot_logo.png win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 false diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs new file mode 100644 index 00000000..ea18131c --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51887, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51388, + } + }, + UpstreamPathTemplate = "/api002/values", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51387, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + }, + DownstreamHeaderTransform = new Dictionary() + { + {"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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs b/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs new file mode 100644 index 00000000..fc376445 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs @@ -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 : BaseCacheHandle + { + private readonly ICacheSerializer _serializer; + private readonly ConcurrentDictionary> _cache; + + public InMemoryJsonHandle( + ICacheManagerConfiguration managerConfiguration, + CacheHandleConfiguration configuration, + ICacheSerializer serializer, + ILoggerFactory loggerFactory) : base(managerConfiguration, configuration) + { + _cache = new ConcurrentDictionary>(); + _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 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 item) + { + Guard.NotNull(item, nameof(item)); + + var key = GetKey(item.Key, item.Region); + + var serializedItem = _serializer.SerializeCacheItem(item); + + return _cache.TryAdd(key, new Tuple(item.Value.GetType(), serializedItem)); + } + + protected override CacheItem GetCacheItemInternal(string key) => GetCacheItemInternal(key, null); + + protected override CacheItem GetCacheItemInternal(string key, string region) + { + var fullKey = GetKey(key, region); + + CacheItem deserializedResult = null; + + if (_cache.TryGetValue(fullKey, out Tuple result)) + { + deserializedResult = _serializer.DeserializeCacheItem(result.Item2, result.Item1); + + if (deserializedResult.ExpirationMode != ExpirationMode.None && IsExpired(deserializedResult, DateTime.UtcNow)) + { + _cache.TryRemove(fullKey, out Tuple removeResult); + TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, deserializedResult.Value); + return null; + } + } + + return deserializedResult; + } + + protected override void PutInternalPrepared(CacheItem item) + { + Guard.NotNull(item, nameof(item)); + + var serializedItem = _serializer.SerializeCacheItem(item); + + _cache[GetKey(item.Key, item.Region)] = new Tuple(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 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 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; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs new file mode 100644 index 00000000..b74e4051 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52839, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs new file mode 100644 index 00000000..fdbbb2d2 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -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 _consulServices; + + public ConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51779, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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(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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs new file mode 100644 index 00000000..1cc19260 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs @@ -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 _consulServices; + + public ConsulConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51779, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51779, + } + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"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 + { + new FileReRoute + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51780, + } + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"} + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Host = "localhost", + Port = consulPort + } + } + }; + + var secondConsulConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51780, + } + }, + UpstreamPathTemplate = "/cs/status/awesome", + UpstreamHttpMethod = new List {"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 + { + new FileDynamicReRoute + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + 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(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 + { + 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(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs new file mode 100644 index 00000000..10ec9880 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs @@ -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 _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ConsulWebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [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 + { + 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(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(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(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(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(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(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(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs new file mode 100644 index 00000000..75a7eaeb --- /dev/null +++ b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs @@ -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 _eurekaInstances; + private readonly ServiceHandler _serviceHandler; + + public EurekaServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _eurekaInstances = new List(); + } + + [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()); + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + 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 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 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 { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 2d0535f6..96223e52 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -25,7 +25,12 @@ + + + + + @@ -54,5 +59,10 @@ + + + + + diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs new file mode 100644 index 00000000..61e39a98 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51569, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51579, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51892, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51872, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/working", + UpstreamHttpMethod = new List { "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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs new file mode 100644 index 00000000..4bdf014b --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -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 _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(); + } + + [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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 8472b474..1aefdbc0 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -24,6 +24,7 @@ using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; using System.IO.Compression; using System.Text; + using Caching; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using Ocelot.Middleware.Multiplexer; using static Ocelot.Infrastructure.Wait; @@ -32,6 +33,13 @@ using Requester; using CookieHeaderValue = System.Net.Http.Headers.CookieHeaderValue; 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 { @@ -98,6 +106,42 @@ 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) { var configurationPath = TestConfiguration.ConfigurationPath; @@ -124,6 +168,12 @@ 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) { _webHostBuilder = new WebHostBuilder(); @@ -183,6 +233,203 @@ _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) { _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) { _response = _ocelotClient.GetAsync(url).Result; @@ -696,6 +1004,11 @@ { _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); } + + public void ThenTheContentLengthIs(int expected) + { + _response.Content.Headers.ContentLength.ShouldBe(expected); + } public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) { diff --git a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs new file mode 100644 index 00000000..de54b0e0 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs @@ -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 _serviceEntries; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private readonly ServiceHandler _serviceHandler; + + public TwoDownstreamServicesTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _serviceEntries = new List(); + } + + [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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/user/{user}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 8362, + } + }, + UpstreamPathTemplate = "/api/user/{user}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/api/product/{product}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 8330, + } + }, + UpstreamPathTemplate = "/api/product/{product}", + UpstreamHttpMethod = new List { "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(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs new file mode 100644 index 00000000..ac1ce4e8 --- /dev/null +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Geoff" + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = fooPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/foo", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = barPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/bar", + UpstreamHttpMethod = new List { "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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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 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> + { + new KeyValuePair("client_id", "api"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("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(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 + { + new ApiResource + { + Name = apiName, + Description = apiName, + Enabled = true, + DisplayName = apiName, + Scopes = new List() + { + new Scope(apiName) + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = apiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { apiName }, + AccessTokenType = AccessTokenType.Jwt, + Enabled = true + } + }) + .AddTestUsers(new List + { + 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 expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + var result = JsonConvert.DeserializeObject(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(_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> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("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(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenOcelotIsRunningWithIdentityServerSettings(Action 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(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/BearerToken.cs b/test/Ocelot.IntegrationTests/BearerToken.cs new file mode 100644 index 00000000..055f28fd --- /dev/null +++ b/test/Ocelot.IntegrationTests/BearerToken.cs @@ -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; } + } +} diff --git a/test/Ocelot.IntegrationTests/CacheManagerTests.cs b/test/Ocelot.IntegrationTests/CacheManagerTests.cs new file mode 100644 index 00000000..b2327451 --- /dev/null +++ b/test/Ocelot.IntegrationTests/CacheManagerTests.cs @@ -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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "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> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("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(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 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(); + } + + } +} diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 0366c742..3bb8f732 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -22,10 +22,17 @@ + + + + + + + all @@ -41,6 +48,8 @@ - + + + diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs new file mode 100644 index 00000000..26af5725 --- /dev/null +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -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 _builders; + private readonly List _webHostBuilders; + private readonly List _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(); + _builders = new List(); + _threads = new List(); + } + + [Fact(Skip = "Still not stable, more work required in rafty..")] + public async Task should_persist_command_to_five_servers() + { + var peers = new List + { + 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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "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 + { + 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() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "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 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 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>(content); + + if (!string.IsNullOrEmpty(errorResult.Error)) + { + return false; + } + + var okResult = JsonConvert.DeserializeObject>(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 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(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 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 AddToken() + { + try + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("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(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(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); + } + } + } + } +} diff --git a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs new file mode 100644 index 00000000..72771ed4 --- /dev/null +++ b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs @@ -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()); + _services = new ServiceCollection(); + _services.AddSingleton(); + _services.AddSingleton(_configRoot); + } + + //keep + [Fact] + public void should_set_up_administration_with_identity_server_options() + { + Action 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 options) + { + _ocelotBuilder.AddAdministration("/administration", options); + } + + private void ThenTheCorrectAdminPathIsRegitered() + { + _serviceProvider = _services.BuildServiceProvider(); + var path = _serviceProvider.GetService(); + path.Path.ShouldBe("/administration"); + } + + private void WhenISetUpOcelotServices() + { + try + { + _ocelotBuilder = _services.AddOcelot(_configRoot); + } + catch (Exception e) + { + _ex = e; + } + } + + private void ThenAnExceptionIsntThrown() + { + _ex.ShouldBeNull(); + } + } +} + diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs new file mode 100644 index 00000000..73eaf495 --- /dev/null +++ b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs @@ -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()); + _services = new ServiceCollection(); + _services.AddSingleton(); + _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)); + var outputCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); + var instance = (ICacheManager)outputCacheManager.ImplementationInstance; + var ocelotConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); + var ocelotConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); + var fileConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); + var fileConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); + + 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(); + } + } +} diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs new file mode 100644 index 00000000..bd746d99 --- /dev/null +++ b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs @@ -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 _ocelotOcelotCacheManager; + private Mock> _mockCacheManager; + private string _key; + private string _value; + private string _resultGet; + private TimeSpan _ttlSeconds; + private string _region; + + public OcelotCacheManagerCache() + { + _mockCacheManager = new Mock>(); + _ocelotOcelotCacheManager = new OcelotCacheManagerCache(_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>()), 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(It.IsAny(), It.IsAny())) + .Returns(value); + } + } +} diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs new file mode 100644 index 00000000..424c8efd --- /dev/null +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -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 _cacheManager; + private readonly OutputCacheMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; + private OcelotRequestDelegate _next; + private Mock _loggerFactory; + private Mock _logger; + + public OutputCacheMiddlewareRealCacheTests() + { + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => + { + x.WithDictionaryHandle(); + }); + _cacheManager = new OcelotCacheManagerCache(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>>(), "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 { "Get" }) + .Build(); + + _downstreamContext.DownstreamReRoute = reRoute; + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs new file mode 100644 index 00000000..5b99742d --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -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> _cache; + private Mock _internalRepo; + private Mock _factory; + private Mock _loggerFactory; + private Mock _client; + private Mock _kvEndpoint; + private FileConfiguration _fileConfiguration; + private Response _setResult; + private Response _getResult; + + public ConsulFileConfigurationRepositoryTests() + { + _cache = new Mock>(); + _internalRepo = new Mock(); + _loggerFactory = new Mock(); + + _factory = new Mock(); + _client = new Mock(); + _kvEndpoint = new Mock(); + + _client + .Setup(x => x.KV) + .Returns(_kvEndpoint.Object); + + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(_client.Object); + + _internalRepo + .Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(new List(), "", new ServiceProviderConfigurationBuilder().Build(), "", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))); + + _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()), Times.Once); + } + + private void GivenTheConfigKeyComesFromFileConfig(string key) + { + _internalRepo + .Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(new List(), "", + 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(); + response.Response = true; + + _kvEndpoint + .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); + } + + private void GivenFetchFromCacheSucceeds() + { + _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); + } + + private void GivenFetchFromConsulReturnsNull() + { + QueryResult result = new QueryResult(); + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .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(); + query.Response = kvp; + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .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(k => k.Value.SequenceEqual(bytes)), It.IsAny()), 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 + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + 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 + }; + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..954b6fb1 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -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 _serviceEntries; + private Consul _provider; + private readonly string _serviceName; + private readonly int _port; + private readonly string _consulHost; + private readonly string _fakeConsulServiceDiscoveryUrl; + private List _services; + private readonly Mock _factory; + private readonly Mock _logger; + private string _receivedToken; + private readonly IConsulClientFactory _clientFactory; + + public ConsulServiceDiscoveryProviderTests() + { + _serviceName = "test"; + _port = 8500; + _consulHost = "localhost"; + _fakeConsulServiceDiscoveryUrl = $"http://{_consulHost}:{_port}"; + _serviceEntries = new List(); + _factory = new Mock(); + _clientFactory = new ConsulClientFactory(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).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(); + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs new file mode 100644 index 00000000..90a8d793 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -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()); + _services = new ServiceCollection(); + _services.AddSingleton(); + _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(); + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..32b325ec --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -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 _services; + private readonly Mock _factory; + private readonly Mock _logger; + private readonly Mock _consulServiceDiscoveryProvider; + private List _result; + + public PollingConsulServiceDiscoveryProviderTests() + { + _services = new List(); + _delay = 1; + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _consulServiceDiscoveryProvider = new Mock(); + } + + [Fact] + public void should_return_service_from_consul() + { + var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List()); + + 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(); + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs new file mode 100644 index 00000000..995eb638 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -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(); + var logger = new Mock(); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + var consulFactory = new Mock(); + services.AddSingleton(consulFactory.Object); + services.AddSingleton(loggerFactory.Object); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void should_return_ConsulServiceDiscoveryProvider() + { + var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("", "", 1, "", "", 1), ""); + provider.ShouldBeOfType(); + } + + [Fact] + public void should_return_PollingConsulServiceDiscoveryProvider() + { + var stopsPollerFromPolling = 10000; + var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("pollconsul", "", 1, "", "", stopsPollerFromPolling), ""); + provider.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs new file mode 100644 index 00000000..3fb6bb7c --- /dev/null +++ b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs @@ -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(); + configRepo.Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null))); + var services = new ServiceCollection(); + services.AddSingleton(configRepo.Object); + var sp = services.BuildServiceProvider(); + var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); + provider.ShouldBeOfType(); + } + + [Fact] + public void should_build() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); + var client = new Mock(); + var configRepo = new Mock(); + configRepo.Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null))); + var services = new ServiceCollection(); + services.AddSingleton(configRepo.Object); + services.AddSingleton(client.Object); + var sp = services.BuildServiceProvider(); + var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); + provider.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs new file mode 100644 index 00000000..2dea63cb --- /dev/null +++ b/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs @@ -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(); + var services = new ServiceCollection(); + services.AddSingleton(client.Object); + var sp = services.BuildServiceProvider(); + var provider = EurekaProviderFactory.Get(sp, config, null); + provider.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..ea5c77f8 --- /dev/null +++ b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs @@ -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 _client; + private readonly string _serviceId; + private List _instances; + private List _result; + + public EurekaServiceDiscoveryProviderTests() + { + _serviceId = "Laura"; + _client = new Mock(); + _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 + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + 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 + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + 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 instances) + { + _instances = instances; + _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); + } + } + + public class EurekaService : IServiceInstance + { + public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary 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 Metadata { get; } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs new file mode 100644 index 00000000..9ee61f21 --- /dev/null +++ b/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs @@ -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(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); + } + } +} diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 5908bc3d..67c264e8 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -21,6 +21,13 @@ + + + + + + + @@ -57,6 +64,15 @@ + + + + + + + + + diff --git a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs new file mode 100644 index 00000000..bb9e45ed --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs @@ -0,0 +1,45 @@ +namespace Ocelot.UnitTests.Polly +{ + using System.IO; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using Ocelot.Configuration.Builder; + using Ocelot.DependencyInjection; + using Ocelot.Logging; + using Ocelot.Requester; + using Provider.Polly; + using Shouldly; + using Xunit; + + public class OcelotBuilderExtensionsTests + { + [Fact] + public void should_build() + { + var loggerFactory = new Mock(); + var services = new ServiceCollection(); + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(1) + .WithDurationOfBreak(200) + .Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosOptions(options) + .Build(); + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .Build(); + services + .AddOcelot(configuration) + .AddPolly(); + var provider = services.BuildServiceProvider(); + + var handler = provider.GetService(); + handler.ShouldNotBeNull(); + + var delgatingHandler = handler(reRoute, loggerFactory.Object); + delgatingHandler.ShouldNotBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs new file mode 100644 index 00000000..4a4cc805 --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -0,0 +1,27 @@ +namespace Ocelot.UnitTests.Polly +{ + using Moq; + using Ocelot.Configuration.Builder; + using Ocelot.Logging; + using Provider.Polly; + using Shouldly; + using Xunit; + + public class PollyQoSProviderTests + { + [Fact] + public void should_build() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(1) + .WithDurationOfBreak(200) + .Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosOptions(options) + .Build(); + var factory = new Mock(); + var pollyQoSProvider = new PollyQoSProvider(reRoute, factory.Object); + pollyQoSProvider.CircuitBreaker.ShouldNotBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs new file mode 100644 index 00000000..c34def7b --- /dev/null +++ b/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs @@ -0,0 +1,78 @@ +namespace Ocelot.UnitTests.Rafty +{ + 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.Administration; + using Ocelot.DependencyInjection; + using Provider.Rafty; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class OcelotAdministrationBuilderExtensionsTests + { + private readonly IServiceCollection _services; + private IServiceProvider _serviceProvider; + private readonly IConfiguration _configRoot; + private IOcelotBuilder _ocelotBuilder; + private Exception _ex; + + public OcelotAdministrationBuilderExtensionsTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(); + _services.AddSingleton(_configRoot); + } + + [Fact] + public void should_set_up_rafty() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpRafty()) + .Then(x => ThenAnExceptionIsntThrown()) + .Then(x => ThenTheCorrectAdminPathIsRegitered()) + .BDDfy(); + } + + private void WhenISetUpRafty() + { + try + { + _ocelotBuilder.AddAdministration("/administration", "secret").AddRafty(); + } + catch (Exception e) + { + _ex = e; + } + } + + private void WhenISetUpOcelotServices() + { + try + { + _ocelotBuilder = _services.AddOcelot(_configRoot); + } + catch (Exception e) + { + _ex = e; + } + } + + private void ThenAnExceptionIsntThrown() + { + _ex.ShouldBeNull(); + } + + private void ThenTheCorrectAdminPathIsRegitered() + { + _serviceProvider = _services.BuildServiceProvider(); + var path = _serviceProvider.GetService(); + path.Path.ShouldBe("/administration"); + } + } +} diff --git a/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs b/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs new file mode 100644 index 00000000..53dc3476 --- /dev/null +++ b/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs @@ -0,0 +1,45 @@ +namespace Ocelot.UnitTests.Rafty +{ + using Moq; + using Ocelot.Configuration.Setter; + using Provider.Rafty; + using TestStack.BDDfy; + using Xunit; + + public class OcelotFiniteStateMachineTests + { + private UpdateFileConfiguration _command; + private readonly OcelotFiniteStateMachine _fsm; + private readonly Mock _setter; + + public OcelotFiniteStateMachineTests() + { + _setter = new Mock(); + _fsm = new OcelotFiniteStateMachine(_setter.Object); + } + + [Fact] + public void should_handle_update_file_configuration_command() + { + this.Given(x => GivenACommand(new UpdateFileConfiguration(new Ocelot.Configuration.File.FileConfiguration()))) + .When(x => WhenTheCommandIsHandled()) + .Then(x => ThenTheStateIsUpdated()) + .BDDfy(); + } + + private void GivenACommand(UpdateFileConfiguration command) + { + _command = command; + } + + private void WhenTheCommandIsHandled() + { + _fsm.Handle(new global::Rafty.Log.LogEntry(_command, _command.GetType(), 0)).Wait(); + } + + private void ThenTheStateIsUpdated() + { + _setter.Verify(x => x.Set(_command.Configuration), Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs new file mode 100644 index 00000000..4307db02 --- /dev/null +++ b/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs @@ -0,0 +1,52 @@ +namespace Ocelot.UnitTests.Rafty +{ + using System.Threading.Tasks; + using global::Rafty.Concensus.Node; + using global::Rafty.Infrastructure; + using Moq; + using Ocelot.Configuration.File; + using Provider.Rafty; + using Shouldly; + using Xunit; + + public class RaftyFileConfigurationSetterTests + { + private readonly RaftyFileConfigurationSetter _setter; + private readonly Mock _node; + + public RaftyFileConfigurationSetterTests() + { + _node = new Mock(); + _setter = new RaftyFileConfigurationSetter(_node.Object); + } + + [Fact] + public async Task should_return_ok() + { + var fileConfig = new FileConfiguration(); + + var response = new OkResponse(new UpdateFileConfiguration(fileConfig)); + + _node.Setup(x => x.Accept(It.IsAny())) + .ReturnsAsync(response); + + var result = await _setter.Set(fileConfig); + result.IsError.ShouldBeFalse(); + } + + [Fact] + public async Task should_return_not_ok() + { + var fileConfig = new FileConfiguration(); + + var response = new ErrorResponse("error", new UpdateFileConfiguration(fileConfig)); + + _node.Setup(x => x.Accept(It.IsAny())) + .ReturnsAsync(response); + + var result = await _setter.Set(fileConfig); + + result.IsError.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/appsettings.json b/test/Ocelot.UnitTests/appsettings.json new file mode 100644 index 00000000..455361cb --- /dev/null +++ b/test/Ocelot.UnitTests/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Error", + "System": "Error", + "Microsoft": "Error" + } + }, + "spring": { + "application": { + "name": "ocelot" + } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true, + "shouldFetchRegistry": true, + "port": 5000, + "hostName": "localhost" + } + } +}