Release/13.2.0 (#834)

* Fix formatting in getting started page (#752)

* updated release docs (#745)

* Update README.md (#756)

Fixed typo "Ocleot"

* Fixed typo there => their (#763)

* Some Typo fixes (#765)

* Typo algorythm => algorithm (#764)

* Typo querystring => query string (#766)

* Typo usual => usually (#767)

* Typos (#768)

* kubernetes provider (#772)

* feat: Kubernetes ServiceDiscoveryProvider

* 编写k8s测试例子

* feat:fix kube config

* feat: remove port

* feat : complete the k8s test

* feat :  add kubeserviceDiscovery test

* feat : add kube provider unittest

* feat :add kubetnetes docs

how to use ocelot with kubetnetes docs

* keep the configuration as simple as possible, no qos, no cache

* fix: use http

* add PollingKubeServiceDiscovery

* feat : refactor logger

* feat : add  pollkube docs

* feat:Remove unnecessary code

* feat : code-block json

* fix issue #661 for Advanced aggregations (#704)

* Add Advanced Aggregation Feature

* fix overwrite error

* distinct data for better performance

* remove constructor parameter

* fix tests issue

* fix tests

* fix tests issue

* Add UnitTest and AcceptanceTest

* fix responseKeys typo

* Update SimpleJsonResponseAggregator.cs

* change port

* Fix code example for SSL Errors (#780)

DangerousAcceptAnyServerCertificateValidator has to be set to "true" to disable certification validation, not "false".

* Changed wording for ease of reading (#776)

Just some wording changes for clarification.

* Ignore response content if null (fix #785) (#786)

* fix bug #791 (#795)

* Update loadbalancer.rst (#796)

* UriBuilder - remove leading question mark #747 (#794)

* Update qualityofservice.rst (#801)

Tiny typo

* K8s package (#804)

* feat: Kubernetes ServiceDiscoveryProvider

* 编写k8s测试例子

* feat:fix kube config

* feat: remove port

* feat : complete the k8s test

* feat :  add kubeserviceDiscovery test

* feat : add kube provider unittest

* feat :add kubetnetes docs

how to use ocelot with kubetnetes docs

* keep the configuration as simple as possible, no qos, no cache

* fix: use http

* add PollingKubeServiceDiscovery

* feat : refactor logger

* feat : add  pollkube docs

* feat:Remove unnecessary code

* feat : code-block json

* feat: publish package Ocelot.Provider.Kubernetes

* Okta integration (#807)

Okta integration

* update cliamsParser (#798)

* update cliamsParser

* update using

* IOcelotBuilder opens the IMvcCoreBuilder property for easy customization (#790)

* IOcelotBuilder opens the IMvcCoreBuilder property for easy customization

* Adjustment code

* nuget package (#809)

* feat: Kubernetes ServiceDiscoveryProvider

* 编写k8s测试例子

* feat:fix kube config

* feat: remove port

* feat : complete the k8s test

* feat :  add kubeserviceDiscovery test

* feat : add kube provider unittest

* feat :add kubetnetes docs

how to use ocelot with kubetnetes docs

* keep the configuration as simple as possible, no qos, no cache

* fix: use http

* add PollingKubeServiceDiscovery

* feat : refactor logger

* feat : add  pollkube docs

* feat:Remove unnecessary code

* feat : code-block json

* feat: publish package Ocelot.Provider.Kubernetes

* feat : nuget package

* fix: Namesapce Spelling wrong

* fix:Namesapce Spelling Wrong

* Fix: errors when using rate limiting (#811)

* Fix: errors when using rate limiting
Add: QuotaExceededError class for requesting too much
Add: QuotaExceededError error code
Add: Add an error when limit is reached
Reflact: Extract GetResponseMessage method for getting default or configured response message for requ

* Fix: modify check_we_have_considered_all_errors_in_these_tests for adding a new OcelotErrorCode

* added missing COPY csproj files (#821)

* Add note on In-Process hosting (#816)

When using ASP.NET Core 2.2 with In-Process hosting in IIS it's important to use .UseIIS() instead of .UseIISIntegration().

* Fix bug: (#810)

If the registered Consul node is unexpectedly down and not restarted immediately, other services should continue to find the registered service.

* Fixed Dockerfile (missing Kubernetes)

* Revert "Fix bug: (#810)" (#823)

This reverts commit 19c80afb05290fac3a144f652cd663c8b513a559.

* remove duplicate `IHttpRequester` register (#819)

* remove duplicate `IHttpRequester` register

* reserve the first

* fix HttpRequesterMiddleware does not call next bug (#830)

call next so that we can do something with the response, such as add some custom header etc...

* Removed Packing to fix issues, will be sorted out after create a nuget package on Nuget.Org (#831)

* Allows access to unpass node (#825)

* Fix bug:
If the registered Consul node is unexpectedly down and not restarted immediately, other services should continue to find the registered service.

* fix bug:
If the registered Consul node is unexpectedly down and not restarted immediately, other services should continue to find the registered service.

* Updated FluentValidations Nuget Package (#833)
This commit is contained in:
Thiago Loureiro 2019-03-22 23:39:53 +01:00 committed by GitHub
parent 02e5cea7b1
commit cfa3fedc53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1677 additions and 128 deletions

View File

@ -12,11 +12,20 @@ WORKDIR /build
#First we add only the project files so that we can cache nuget packages with dotnet restore #First we add only the project files so that we can cache nuget packages with dotnet restore
COPY Ocelot.sln Ocelot.sln COPY Ocelot.sln Ocelot.sln
COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj
COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj
COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj
COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj
COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj
COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj
COPY src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj
COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj
COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj
COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj
COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj
COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj
COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj
COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj
RUN dotnet restore RUN dotnet restore
#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point #Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point
COPY codeanalysis.ruleset codeanalysis.ruleset COPY codeanalysis.ruleset codeanalysis.ruleset

View File

@ -56,6 +56,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Rafty", "sr
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -114,6 +116,10 @@ Global
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU
{72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -132,6 +138,7 @@ Global
{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {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} {AC153C67-EF18-47E6-A230-F0D3CF5F0A98} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
{6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
{72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48}

View File

@ -69,7 +69,7 @@ All versions can be found [here](https://www.nuget.org/packages/Ocelot/)
## Documentation ## Documentation
Please click [here](http://ocelot.readthedocs.io/en/latest/) for the Ocleot documentation. This includes lots of information and will be helpful if you want to understand the features Ocelot currently offers. Please click [here](http://ocelot.readthedocs.io/en/latest/) for the Ocelot documentation. This includes lots of information and will be helpful if you want to understand the features Ocelot currently offers.
## Coming up ## Coming up

View File

@ -1,23 +1,38 @@
Release process Release process
=============== ===============
This section defines the release process for the maintainers of the project. Ocelot uses the following process to accept work into the NuGet packages.
* Merge pull requests to the `release` branch.
* Every commit pushed to the Origin repo will kick off the `ocelot-build <https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb>`_ project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed. 1. User creates an issue or picks up an existing issue in GitHub.
* 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. 2. User creates a fork and branches from this (unless a member of core team, they can just create a branch on the main repo) e.g. feat/xxx, fix/xxx etc. It doesn't really matter what the xxx is. It might make sense to use the issue number and maybe a short description. I don't care as long as it has (feat, fix, refactor)/xxx :)
* 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. 3. When the user is happy with their work they can create a pull request in GitHub with their changes. The user must follow the `SemVer <https://semver.org/>`_ support for this is provided by `GitVersion <https://gitversion.readthedocs.io/en/latest/>`_. So if you need to make breaking changes please make sure you use the correct commit message so GitVersion uses the correct semver tags. Do not manually tag the Ocelot repo this will break things.
* In Github, navigate to the `release <https://github.com/ThreeMammals/Ocelot/releases>`_. Modify the release name and tag as desired. 4. The Ocelot team will review the PR and if all is good merge it, else they will suggest feedback that the user will need to act on. In order to speed up getting a PR the user should think about the following.
- Have I covered all my changes with tests at unit and acceptance level?
- Have I updated any documentation that my changes may have affected?
- Does my feature make sense, have I checked all of Ocelot's other features to make sure it doesn't already exist?
In order for a PR to be merged the following must have occured.
- All new code is covered by unit tests.
- All new code has at least 1 acceptance test covering the happy path.
- Builds for Windows, Mac and Linux must have passed.
- Builds for Windows, Mac and Linux must not have slowed down dramatically.
- The main Ocelot package must not have taken on any non MS dependencies.
* When you're ready, publish the release. This will tag the commit with the specified release number. 5. After the PR is merged the GitHub issue must be labelled as merged. The merge will trigger an alpha build on the develop branch with these changes that will get pushed to NuGet. You can import this and test it manually should you wish.
* The `ocelot-release <https://ci.appveyor.com/project/TomPallister/ocelot-ayj4w>`_ 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. 6. When the Ocelot team is ready to create a release they will checkout a new release branch with the version from the latest develop build. So look in AppVeyor for the latest develop semver number and checkout a new branch e.g. git checkout -b release/13.1.0 and push this to the remote. Wait for it to build and then merge this branch back into master.
* 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. 7. Wait for the master build to complete. When it has go to the AppVeyor UI and find the build and click the Deploy link in the top right hand corner. Select release preparation from the environment drop down and click deploy. This will send the release information to GitHub releases.
* 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. 8. Go to GitHub releases and find the version you have just released. It will look something like 13.0.0+31.build.1783. Remove +31.build.1783 (or whatever you get) from all the input fields so you are just left with 13.0.0. Untick This is a pre release and click release. This will trigger a build from AppVeyor that downloads the release artifacts from GitHub and publishes them to the stable NuGet feed. All being well you should find your new package on NuGet within 30 minutes. You might also want to manually add the issue numbers of what has been merged so people can see what changed in this release.
9. The final step is to go back to GitHub and close any issues that were labelled as merged. You should see something like this in`GitHub <https://github.com/ThreeMammals/Ocelot/releases/tag/13.0.0>`_ and this in `NuGet <https://www.nuget.org/packages/Ocelot/13.0.0>`_.
Notes
-----
All NuGet package builds are done with AppVeyor `here <https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb>_` and all releases are done from `here <https://ci.appveyor.com/project/TomPallister/ocelot-ayj4w>_`. We also build Ocelot on Travis `here <https://travis-ci.org/ThreeMammals/Ocelot>`_ but this is only to make sure it is building on multiple OS.
Only Ocelot core team members can merge PRs and create release branches. Only TomPallister can merge releases into master at the moment. This is to ensure there is a final quality gate in place. Tom is mainly looking for security issues on the final merge.

View File

@ -138,8 +138,28 @@ Then map the authentication provider key to a ReRoute in your configuration e.g.
Okta Okta
^^^^ ^^^^
Add nuget package : `"Okta.AspNetCore" https://www.nuget.org/packages/Okta.AspNetCore/`_
I have not had time to write this up but we have `Issue 446 <https://github.com/ThreeMammals/Ocelot/issues/446>`_ that contains some code and examples that might help with Okta integration. In a StartUp.cs file add to a method Configure next lines:
app.UseAuthentication();
app.UseOcelot().Wait();
In a StartUp.cs file add to a method ConfigureServices lines:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultChallengeScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultSignInScheme = OktaDefaults.ApiAuthenticationScheme;
})
.AddOktaWebApi(new OktaWebApiOptions
{
OktaDomain = _cfg["Okta:OktaDomain"]
});
services.AddOcelot(_cfg);
`Issue 446 <https://github.com/ThreeMammals/Ocelot/issues/446>`_ that contains some code and examples that might help with Okta integration.
Allowed Scopes Allowed Scopes
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -88,7 +88,7 @@ to you
.AddEnvironmentVariables(); .AddEnvironmentVariables();
}) })
Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isnt one. Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isn't one.
You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs <https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments>`_. You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs <https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments>`_.
@ -131,7 +131,7 @@ You can also give Ocelot a specific path to look in for the configuration files
.AddEnvironmentVariables(); .AddEnvironmentVariables();
}) })
Ocelot needs the HostingEnvironment so it know's to exclude anything environment specific from the algorithm. Ocelot needs the HostingEnvironment so it knows to exclude anything environment specific from the algorithm.
Store configuration in consul Store configuration in consul
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -161,7 +161,7 @@ finds your Consul agent and interacts to load and store the configuration from C
} }
} }
I decided to create this feature after working on the raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! I decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this!
I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now.
This feature has a 3 second ttl cache before making a new request to your local consul agent. This feature has a 3 second ttl cache before making a new request to your local consul agent.
@ -202,22 +202,23 @@ Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior:
1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically 1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically
follow redirection responses from the Downstream resource; otherwise false. The default value is false. follow redirection responses from the Downstream resource; otherwise false. The default value is false.
2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer 2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer
property to store server cookies and uses these cookies when sending requests. The default value is false. Please note property to store server cookies and uses these cookies when sending requests. The default value is false. Please note
that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests
to that DownstreamService will share the same cookies. `Issue 274 <https://github.com/ThreeMammals/Ocelot/issues/274>`_ was created because a user to that DownstreamService will share the same cookies. `Issue 274 <https://github.com/ThreeMammals/Ocelot/issues/274>`_ was created because a user
noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients
that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight
requests. This would also mean that subsequent requests dont use the cookies from the previous response! All in all not a great situation. I would avoid setting requests. This would also mean that subsequent requests don't use the cookies from the previous response! All in all not a great situation. I would avoid setting
UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request!
SSL Errors SSL Errors
^^^^^^^^^^ ^^^^^^^^^^
Id you want to ignore SSL warnings / errors set the following in your ReRoute config. If you want to ignore SSL warnings / errors set the following in your ReRoute config.
.. code-block:: json .. code-block:: json
"DangerousAcceptAnyServerCertificateValidator": false "DangerousAcceptAnyServerCertificateValidator": true
I don't reccomend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. I don't recommend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can.

View File

@ -0,0 +1,63 @@
Kubernetes
==============
This feature was requested as part of `Issue 345 <https://github.com/ThreeMammals/Ocelot/issues/345>`_ . to add support for kubernetes's service discovery provider.
The first thing you need to do is install the NuGet package that provides kubernetes support in Ocelot.
``Install-Package Ocelot.Provider.Kubernetes``
Then add the following to your ConfigureServices method.
.. code-block:: csharp
s.AddOcelot()
.AddKubernetes();
If you have services deployed in kubernetes you will normally use the naming service to access them.
The following example shows how to set up a ReRoute that will work in kubernetes. The most important thing is the ServiceName which is made up of the
kubernetes service name. We also need to set up the ServiceDiscoveryProvider in
GlobalConfiguration. The example here shows a typical configuration. It assumes kubernetes api server is running on 192.168.0.13 and that api service is on port 443.
.. code-block:: json
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/values",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/values",
"ServiceName": "downstreamservice",
"UpstreamHttpMethod": [ "Get" ]
}
],
"GlobalConfiguration": {
"ServiceDiscoveryProvider": {
"Host": "192.168.0.13",
"Port": 443,
"Token": "txpc696iUhbVoudg164r93CxDTrKRVWG",
"Namespace": "dev",
"Type": "kube"
}
}
}
You use Ocelot to poll kubernetes for latest service information rather than per request. If you want to poll kubernetes for the latest services rather than per request (default behaviour) then you need to set the following configuration.
.. code-block:: json
"ServiceDiscoveryProvider": {
"Host": "192.168.0.13",
"Port": 443,
"Token": "txpc696iUhbVoudg164r93CxDTrKRVWG",
"Namespace": "dev",
"Type": "pollkube"
"PollingInterval": 100
}
The polling interval is in milliseconds and tells Ocelot how often to call kubernetes for changes in service configuration.
Please note there are tradeoffs here. If you poll kubernetes it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling kubernetes per request.
There is no way for Ocelot to work these out for you.

View File

@ -18,7 +18,7 @@ You must choose in your configuration which load balancer to use.
Configuration Configuration
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeadConnection load balancer. This is the simplest way to get load balancing set up. The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeastConnection load balancer. This is the simplest way to get load balancing set up.
.. code-block:: json .. code-block:: json

View File

@ -5,7 +5,7 @@ Ocelot supports one QoS capability at the current time. You can set on a per ReR
want to use a circuit breaker when making requests to a downstream service. This uses an awesome want to use a circuit breaker when making requests to a downstream service. This uses an awesome
.NET library called Polly check them out `here <https://github.com/App-vNext/Polly>`_. .NET library called Polly check them out `here <https://github.com/App-vNext/Polly>`_.
The first thing you need to do if you want to use the administration API is bring in the relavent NuGet package.. The first thing you need to do if you want to use the administration API is bring in the relevant NuGet package..
``Install-Package Ocelot.Provider.Polly`` ``Install-Package Ocelot.Provider.Polly``

View File

@ -3,7 +3,7 @@ Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION)
Ocelot has recently integrated `Rafty <https://github.com/ThreeMammals/Rafty>`_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK. Ocelot has recently integrated `Rafty <https://github.com/ThreeMammals/Rafty>`_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK.
Raft is a distributed concensus algorythm that allows a cluster of servers (Ocelots) to maintain local state without having a centralised database for storing state (e.g. SQL Server). Raft is a distributed concensus algorithm 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. To get Raft support you must first install the Ocelot Rafty package.

View File

@ -1,7 +1,7 @@
Request Aggregation Request Aggregation
=================== ===================
Ocelot allows you to specify Aggregate ReRoutes that compose multiple normal ReRoutes and map their responses into one object. This is usual where you have Ocelot allows you to specify Aggregate ReRoutes that compose multiple normal ReRoutes and map their responses into one object. This is usually where you have
a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type
architecture with Ocelot. architecture with Ocelot.

View File

@ -189,7 +189,7 @@ this sounds interesting to you.
Query Strings Query Strings
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
Ocelot allows you to specify a querystring as part of the DownstreamPathTemplate like the example below. Ocelot allows you to specify a query string as part of the DownstreamPathTemplate like the example below.
.. code-block:: json .. code-block:: json
@ -241,5 +241,5 @@ Ocelot will also allow you to put query string parameters in the UpstreamPathTem
} }
} }
In this example Ocelot will only match requests that have a matching url path and the querystring starts with unitId=something. You can have other queries after this In this example Ocelot will only match requests that have a matching url path and the query string starts with unitId=something. You can have other queries after this
but you must start with the matching parameter. Also Ocelot will swap the {unitId} parameter from the query string and use it in the downstream request path. but you must start with the matching parameter. Also Ocelot will swap the {unitId} parameter from the query string and use it in the downstream request path.

View File

@ -54,7 +54,7 @@ and LeastConnection algorithm you can use. If no load balancer is specified Ocel
When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services.
A lot of people have asked me to implement a feature where Ocelot polls consul for latest service information rather than per request. If you want to poll consul for the latest services rather than per request (default behaviour) then you need to set the following configuration. A lot of people have asked me to implement a feature where Ocelot polls Consul for latest service information rather than per request. If you want to poll Consul for the latest services rather than per request (default behaviour) then you need to set the following configuration.
.. code-block:: json .. code-block:: json
@ -67,9 +67,9 @@ A lot of people have asked me to implement a feature where Ocelot polls consul f
The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration.
Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volitile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling consul per request (as sidecar agent). If you are calling a remote consul agent then polling will be a good performance improvement. Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling Consul per request (as sidecar agent). If you are calling a remote Consul agent then polling will be a good performance improvement.
You services need to be added to Consul something like below (c# style but hopefully this make sense)...The only important thing to note Your services need to be added to Consul something like below (C# style but hopefully this make sense)...The only important thing to note
is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme
in address. After reading `this <https://www.consul.io/docs/agent/services.html>`_ I don't think the scheme should be in there. in address. After reading `this <https://www.consul.io/docs/agent/services.html>`_ I don't think the scheme should be in there.
@ -108,7 +108,7 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade
"Type": "Consul" "Type": "Consul"
} }
Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request.
Eureka Eureka
^^^^^^ ^^^^^^
@ -160,8 +160,8 @@ Dynamic Routing
This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider. This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issue/340>`_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider.
An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of An example of this would be calling Ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of
the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal. the path which is product and use it as a key to look up the service in Consul. If Consul returns a service Ocelot will request it on whatever host and port comes back from Consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal.
In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme.

View File

@ -24,6 +24,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n
features/graphql features/graphql
features/servicediscovery features/servicediscovery
features/servicefabric features/servicefabric
features/kubernetes
features/authentication features/authentication
features/authorisation features/authorisation
features/websockets features/websockets

View File

@ -2,7 +2,7 @@ Getting Started
=============== ===============
Ocelot is designed to work with .NET Core only and is currently Ocelot is designed to work with .NET Core only and is currently
built to netstandard2.0 `this <https://docs.microsoft.com/en-us/dotnet/articles/standard/library>`_ documentation may prove helpful when working out if Ocelot would be suitable for you. built to netstandard2.0. `This <https://docs.microsoft.com/en-us/dotnet/articles/standard/library>`_ documentation may prove helpful when working out if Ocelot would be suitable for you.
.NET Core 2.1 .NET Core 2.1
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
@ -32,8 +32,8 @@ The following is a very basic ocelot.json. It won't do anything but should get O
The most important thing to note here is BaseUrl. Ocelot needs to know the URL it is running under The most important thing to note here is BaseUrl. Ocelot needs to know the URL it is running under
in order to do Header find & replace and for certain administration configurations. When setting this URL it should be the external URL that clients will see Ocelot running on e.g. If you are running containers Ocelot might run on the url http://123.12.1.1:6543 but has something like nginx in front of it responding on https://api.mybusiness.com. In this case the Ocelot base url should be https://api.mybusiness.com. in order to do Header find & replace and for certain administration configurations. When setting this URL it should be the external URL that clients will see Ocelot running on e.g. If you are running containers Ocelot might run on the url http://123.12.1.1:6543 but has something like nginx in front of it responding on https://api.mybusiness.com. In this case the Ocelot base url should be https://api.mybusiness.com.
If for some reason you are using containers and do want Ocelot to respond to client on http://123.12.1.1:6543 If you are using containers and require Ocelot to respond to clients on http://123.12.1.1:6543
then you can do this but if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script. Hopefully whatever scheduler you are using can pass the IP. then you can do this, however if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script. Hopefully whatever scheduler you are using can pass the IP.
**Program** **Program**
@ -75,6 +75,8 @@ AddOcelot() (adds ocelot services), UseOcelot().Wait() (sets up all the Ocelot m
} }
} }
**Note:** When using ASP.NET Core 2.2 and you want to use In-Process hosting, replace **.UseIISIntegration()** with **.UseIIS()**, otherwise you'll get startup errors.
.NET Core 1.0 .NET Core 1.0
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -32,7 +32,7 @@ The main reasons why I don't think Swagger makes sense is we already hand roll o
If we want people developing against Ocelot to be able to see what routes are available then either share the ocelot.json If we want people developing against Ocelot to be able to see what routes are available then either share the ocelot.json
with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration. with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration.
In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to there product service In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to their product service
and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has
no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return
multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle

View File

@ -0,0 +1,9 @@
.dockerignore
.env
.git
.gitignore
.vs
.vscode
*/bin
*/obj
**/.toolstarget

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj" />
<ProjectReference Include="..\..\..\src\Ocelot\Ocelot.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY ["ApiGateway/ApiGateway.csproj", "ApiGateway/"]
COPY ["../../src/Ocelot/Ocelot.csproj", "../../src/Ocelot/"]
COPY ["../../src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj", "../../src/Ocelot.Provider.Kubernetes/"]
RUN dotnet restore "ApiGateway/ApiGateway.csproj"
COPY . .
WORKDIR "/src/ApiGateway"
RUN dotnet build "ApiGateway.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "ApiGateway.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "ApiGateway.dll"]

View File

@ -0,0 +1,32 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Kubernetes;
namespace ApiGateway
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
.AddJsonFile("ocelot.json", false, false)
.AddEnvironmentVariables();
})
.UseStartup<Startup>()
.Build();
}
}

View File

@ -0,0 +1,32 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52363",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ApiGateway": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://localhost:{ServicePort}"
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Kubernetes;
namespace ApiGateway
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddOcelot()
.AddKubernetes();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseOcelot().Wait();
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,20 @@
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/values",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/values",
"ServiceName": "downstreamservice",
"UpstreamHttpMethod": [ "Get" ]
}
],
"GlobalConfiguration": {
"ServiceDiscoveryProvider": {
"Host": "192.168.0.13",
"Port": 443,
"Token": "txpc696iUhbVoudg164r93CxDTrKRVWG",
"Namespace": "dev",
"Type": "kube"
}
}
}

View File

@ -0,0 +1,22 @@
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY ["ApiGateway/ApiGateway.csproj", "ApiGateway/"]
COPY ["../../src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj", "../../src/Ocelot.Provider.Polly/"]
COPY ["../../src/Ocelot/Ocelot.csproj", "../../src/Ocelot/"]
COPY ["../../src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj", "../../src/Ocelot.Provider.Kubernetes/"]
RUN dotnet restore "ApiGateway/ApiGateway.csproj"
COPY . .
WORKDIR "/src/ApiGateway"
RUN dotnet build "ApiGateway.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "ApiGateway.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "ApiGateway.dll"]

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace DownstreamService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

View File

@ -0,0 +1,19 @@
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY ["DownstreamService/DownstreamService.csproj", "DownstreamService/"]
RUN dotnet restore "DownstreamService/DownstreamService.csproj"
COPY . .
WORKDIR "/src/DownstreamService"
RUN dotnet build "DownstreamService.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "DownstreamService.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "DownstreamService.dll"]

View File

@ -0,0 +1,15 @@
FROM microsoft/dotnet:2.1-sdk
ARG BUILD_CONFIGURATION=Debug
ENV ASPNETCORE_ENVIRONMENT=Development
ENV DOTNET_USE_POLLING_FILE_WATCHER=true
EXPOSE 80
WORKDIR /src
COPY ["DownstreamService/DownstreamService.csproj", "DownstreamService/"]
RUN dotnet restore "DownstreamService/DownstreamService.csproj"
COPY . .
WORKDIR "/src/DownstreamService"
RUN dotnet build --no-restore "DownstreamService.csproj" -c $BUILD_CONFIGURATION
ENTRYPOINT ["dotnet", "run", "--no-build", "--no-launch-profile", "-c", "$BUILD_CONFIGURATION", "--"]

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.1.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace DownstreamService
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

View File

@ -0,0 +1,35 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56411",
"sslPort": 0
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"DownstreamService": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/values"
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DownstreamService
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2048
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "..\..\src\Ocelot\Ocelot.csproj", "{E8551073-622E-45FA-AD09-038EB8AAFFBC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "..\..\src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{EF973868-98A6-4864-BF66-65B5A8C123FE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "DownstreamService\DownstreamService.csproj", "{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.Build.0 = Release|Any CPU
{E8551073-622E-45FA-AD09-038EB8AAFFBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8551073-622E-45FA-AD09-038EB8AAFFBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8551073-622E-45FA-AD09-038EB8AAFFBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8551073-622E-45FA-AD09-038EB8AAFFBC}.Release|Any CPU.Build.0 = Release|Any CPU
{EF973868-98A6-4864-BF66-65B5A8C123FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF973868-98A6-4864-BF66-65B5A8C123FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF973868-98A6-4864-BF66-65B5A8C123FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF973868-98A6-4864-BF66-65B5A8C123FE}.Release|Any CPU.Build.0 = Release|Any CPU
{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D29790E8-4BA9-4E60-8D7D-327E21320CC9}
EndGlobalSection
EndGlobal

View File

@ -27,7 +27,7 @@
public async Task<List<Service>> Get() public async Task<List<Service>> Get()
{ {
var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, false);
var services = new List<Service>(); var services = new List<Service>();

View File

@ -0,0 +1,12 @@
using KubeClient;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ocelot.Provider.Kubernetes
{
public interface IKubeApiClientFactory
{
IKubeApiClient Get(KubeRegistryConfiguration config);
}
}

View File

@ -0,0 +1,22 @@
using KubeClient;
namespace Ocelot.Provider.Kubernetes
{
public class KubeApiClientFactory : IKubeApiClientFactory
{
public IKubeApiClient Get(KubeRegistryConfiguration config)
{
var option = new KubeClientOptions
{
ApiEndPoint = config.ApiEndPoint
};
if(!string.IsNullOrEmpty(config?.AccessToken))
{
option.AccessToken = config.AccessToken;
option.AuthStrategy = config.AuthStrategy;
option.AllowInsecure = config.AllowInsecure;
}
return KubeApiClient.Create(option);
}
}
}

View File

@ -0,0 +1,63 @@
using KubeClient;
using KubeClient.Models;
using Ocelot.Logging;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Provider.Kubernetes
{
public class Kube : IServiceDiscoveryProvider
{
private KubeRegistryConfiguration kubeRegistryConfiguration;
private IOcelotLogger logger;
private IKubeApiClient kubeApi;
public Kube(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClientFactory kubeClientFactory)
{
this.kubeRegistryConfiguration = kubeRegistryConfiguration;
this.logger = factory.CreateLogger<Kube>();
this.kubeApi = kubeClientFactory.Get(kubeRegistryConfiguration);
}
public async Task<List<Service>> Get()
{
var service = await kubeApi.ServicesV1()
.Get(kubeRegistryConfiguration.KeyOfServiceInK8s, kubeRegistryConfiguration.KubeNamespace);
var services = new List<Service>();
if (IsValid(service))
{
services.Add(BuildService(service));
}
else
{
logger.LogWarning($"namespace:{kubeRegistryConfiguration.KubeNamespace }service:{kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0");
}
return services;
}
private bool IsValid(ServiceV1 service)
{
if (string.IsNullOrEmpty(service.Spec.ClusterIP) || service.Spec.Ports.Count <= 0)
{
return false;
}
return true;
}
private Service BuildService(ServiceV1 serviceEntry)
{
var servicePort = serviceEntry.Spec.Ports.FirstOrDefault();
return new Service(
serviceEntry.Metadata.Name,
new ServiceHostAndPort(serviceEntry.Spec.ClusterIP, servicePort.Port),
serviceEntry.Metadata.Uid,
string.Empty,
Enumerable.Empty<string>());
}
}
}

View File

@ -0,0 +1,22 @@
using KubeClient;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ocelot.Provider.Kubernetes
{
public class KubeRegistryConfiguration
{
public Uri ApiEndPoint { get; set; }
public string KubeNamespace { get; set; }
public string KeyOfServiceInK8s { get; set; }
public KubeAuthStrategy AuthStrategy { get; set; }
public string AccessToken { get; set; }
public bool AllowInsecure { get; set; }
}
}

View File

@ -0,0 +1,38 @@
using KubeClient;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Logging;
using Ocelot.ServiceDiscovery;
using System;
namespace Ocelot.Provider.Kubernetes
{
public static class KubernetesProviderFactory
{
public static ServiceDiscoveryFinderDelegate Get = (provider, config, name) =>
{
var factory = provider.GetService<IOcelotLoggerFactory>();
return GetkubeProvider(provider, config, name, factory);
};
private static ServiceDiscovery.Providers.IServiceDiscoveryProvider GetkubeProvider(IServiceProvider provider, Configuration.ServiceProviderConfiguration config, string name, IOcelotLoggerFactory factory)
{
var kubeClientFactory = provider.GetService<IKubeApiClientFactory>();
var k8sRegistryConfiguration = new KubeRegistryConfiguration()
{
ApiEndPoint = new Uri($"https://{config.Host}:{config.Port}"),
KeyOfServiceInK8s = name,
KubeNamespace = config.Namespace,
AuthStrategy = KubeAuthStrategy.BearerToken,
AccessToken = config.Token,
AllowInsecure = true // Don't validate server certificate
};
var k8sServiceDiscoveryProvider = new Kube(k8sRegistryConfiguration, factory, kubeClientFactory);
if (config.Type?.ToLower() == "pollkube")
{
return new PollKube(config.PollingInterval, factory, k8sServiceDiscoveryProvider);
}
return k8sServiceDiscoveryProvider;
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
<NoPackageAnalysis>true</NoPackageAnalysis>
<Product>Ocelot</Product>
<Description>Provides Ocelot extensions to use kubernetes</Description>
<PackageProjectUrl>https://github.com/ThreeMammals/Ocelot</PackageProjectUrl>
<PackageIconUrl>http://threemammals.com/images/ocelot_logo.png</PackageIconUrl>
<PackageReleaseNotes></PackageReleaseNotes>
<AssemblyName>Ocelot.Provider.Kubernetes</AssemblyName>
<PackageId>Ocelot.Provider.Kubernetes</PackageId>
<PackageTags>API Gateway;.NET core</PackageTags>
<RuntimeIdentifiers>win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64</RuntimeIdentifiers>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Version>0.0.0-dev</Version>
<Authors>geffzhang</Authors>
<Company />
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubeClient" Version="2.2.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
namespace Ocelot.Provider.Kubernetes
{
public static class OcelotBuilderExtensions
{
public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder)
{
builder.Services.AddSingleton(KubernetesProviderFactory.Get);
builder.Services.AddSingleton<IKubeApiClientFactory, KubeApiClientFactory>();
return builder;
}
}
}

View File

@ -0,0 +1,50 @@
using Ocelot.Logging;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Ocelot.Provider.Kubernetes
{
public class PollKube : IServiceDiscoveryProvider
{
private readonly IOcelotLogger _logger;
private readonly IServiceDiscoveryProvider _kubeServiceDiscoveryProvider;
private readonly Timer _timer;
private bool _polling;
private List<Service> _services;
public PollKube(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeServiceDiscoveryProvider)
{
_logger = factory.CreateLogger<PollKube>();
_kubeServiceDiscoveryProvider = kubeServiceDiscoveryProvider;
_services = new List<Service>();
_timer = new Timer(async x =>
{
if (_polling)
{
return;
}
_polling = true;
await Poll();
_polling = false;
}, null, pollingInterval, pollingInterval);
}
public Task<List<Service>> Get()
{
return Task.FromResult(_services);
}
private async Task Poll()
{
_services = await _kubeServiceDiscoveryProvider.Get();
}
}
}

View File

@ -4,6 +4,7 @@
using System.Net.Http; using System.Net.Http;
using Ocelot.Values; using Ocelot.Values;
using System.Linq; using System.Linq;
using Ocelot.Configuration.File;
public class ReRouteBuilder public class ReRouteBuilder
{ {
@ -11,11 +12,13 @@
private List<HttpMethod> _upstreamHttpMethod; private List<HttpMethod> _upstreamHttpMethod;
private string _upstreamHost; private string _upstreamHost;
private List<DownstreamReRoute> _downstreamReRoutes; private List<DownstreamReRoute> _downstreamReRoutes;
private List<AggregateReRouteConfig> _downstreamReRoutesConfig;
private string _aggregator; private string _aggregator;
public ReRouteBuilder() public ReRouteBuilder()
{ {
_downstreamReRoutes = new List<DownstreamReRoute>(); _downstreamReRoutes = new List<DownstreamReRoute>();
_downstreamReRoutesConfig = new List<AggregateReRouteConfig>();
} }
public ReRouteBuilder WithDownstreamReRoute(DownstreamReRoute value) public ReRouteBuilder WithDownstreamReRoute(DownstreamReRoute value)
@ -48,6 +51,12 @@
return this; return this;
} }
public ReRouteBuilder WithAggregateReRouteConfig(List<AggregateReRouteConfig> aggregateReRouteConfigs)
{
_downstreamReRoutesConfig = aggregateReRouteConfigs;
return this;
}
public ReRouteBuilder WithAggregator(string aggregator) public ReRouteBuilder WithAggregator(string aggregator)
{ {
_aggregator = aggregator; _aggregator = aggregator;
@ -58,6 +67,7 @@
{ {
return new ReRoute( return new ReRoute(
_downstreamReRoutes, _downstreamReRoutes,
_downstreamReRoutesConfig,
_upstreamHttpMethod, _upstreamHttpMethod,
_upstreamTemplatePattern, _upstreamTemplatePattern,
_upstreamHost, _upstreamHost,

View File

@ -8,6 +8,7 @@ namespace Ocelot.Configuration.Builder
private string _token; private string _token;
private string _configurationKey; private string _configurationKey;
private int _pollingInterval; private int _pollingInterval;
private string _namespace;
public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost)
{ {
@ -45,9 +46,15 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ServiceProviderConfigurationBuilder WithNamespace(string @namespace)
{
_namespace = @namespace;
return this;
}
public ServiceProviderConfiguration Build() public ServiceProviderConfiguration Build()
{ {
return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval); return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval, _namespace);
} }
} }
} }

View File

@ -24,14 +24,18 @@ namespace Ocelot.Configuration.Creator
private ReRoute SetUpAggregateReRoute(IEnumerable<ReRoute> reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration) private ReRoute SetUpAggregateReRoute(IEnumerable<ReRoute> reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration)
{ {
var applicableReRoutes = reRoutes var applicableReRoutes = new List<DownstreamReRoute>();
.SelectMany(x => x.DownstreamReRoute) var allReRoutes = reRoutes.SelectMany(x => x.DownstreamReRoute);
.Where(r => aggregateReRoute.ReRouteKeys.Contains(r.Key))
.ToList();
if (applicableReRoutes.Count != aggregateReRoute.ReRouteKeys.Count) foreach (var reRouteKey in aggregateReRoute.ReRouteKeys)
{ {
return null; var selec = allReRoutes.FirstOrDefault(q => q.Key == reRouteKey);
if (selec == null)
{
return null;
}
applicableReRoutes.Add(selec);
} }
var upstreamTemplatePattern = _creator.Create(aggregateReRoute); var upstreamTemplatePattern = _creator.Create(aggregateReRoute);
@ -40,6 +44,7 @@ namespace Ocelot.Configuration.Creator
.WithUpstreamHttpMethod(aggregateReRoute.UpstreamHttpMethod) .WithUpstreamHttpMethod(aggregateReRoute.UpstreamHttpMethod)
.WithUpstreamPathTemplate(upstreamTemplatePattern) .WithUpstreamPathTemplate(upstreamTemplatePattern)
.WithDownstreamReRoutes(applicableReRoutes) .WithDownstreamReRoutes(applicableReRoutes)
.WithAggregateReRouteConfig(aggregateReRoute.ReRouteKeysConfig)
.WithUpstreamHost(aggregateReRoute.UpstreamHost) .WithUpstreamHost(aggregateReRoute.UpstreamHost)
.WithAggregator(aggregateReRoute.Aggregator) .WithAggregator(aggregateReRoute.Aggregator)
.Build(); .Build();

View File

@ -13,6 +13,7 @@ namespace Ocelot.Configuration.Creator
? globalConfiguration?.ServiceDiscoveryProvider?.Type ? globalConfiguration?.ServiceDiscoveryProvider?.Type
: "consul"; : "consul";
var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0; var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0;
var k8snamespace = globalConfiguration?.ServiceDiscoveryProvider?.Namespace ?? string.Empty;
return new ServiceProviderConfigurationBuilder() return new ServiceProviderConfigurationBuilder()
.WithHost(host) .WithHost(host)
@ -21,6 +22,7 @@ namespace Ocelot.Configuration.Creator
.WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token)
.WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey) .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey)
.WithPollingInterval(pollingInterval) .WithPollingInterval(pollingInterval)
.WithNamespace(k8snamespace)
.Build(); .Build();
} }
} }

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ocelot.Configuration.File
{
public class AggregateReRouteConfig
{
public string ReRouteKey { get; set; }
public string Parameter { get; set; }
public string JsonPath { get; set; }
}
}

View File

@ -5,6 +5,7 @@ namespace Ocelot.Configuration.File
public class FileAggregateReRoute : IReRoute public class FileAggregateReRoute : IReRoute
{ {
public List<string> ReRouteKeys { get;set; } public List<string> ReRouteKeys { get;set; }
public List<AggregateReRouteConfig> ReRouteKeysConfig { get;set; }
public string UpstreamPathTemplate { get;set; } public string UpstreamPathTemplate { get;set; }
public string UpstreamHost { get; set; } public string UpstreamHost { get; set; }
public bool ReRouteIsCaseSensitive { get; set; } public bool ReRouteIsCaseSensitive { get; set; }

View File

@ -8,5 +8,6 @@ namespace Ocelot.Configuration.File
public string Token { get; set; } public string Token { get; set; }
public string ConfigurationKey { get; set; } public string ConfigurationKey { get; set; }
public int PollingInterval { get; set; } public int PollingInterval { get; set; }
public string Namespace { get; set; }
} }
} }

View File

@ -6,7 +6,7 @@ namespace Ocelot.Configuration
{ {
public LoadBalancerOptions(string type, string key, int expiryInMs) public LoadBalancerOptions(string type, string key, int expiryInMs)
{ {
Type = type ?? nameof(NoLoadBalancer); Type = string.IsNullOrWhiteSpace(type) ? nameof(NoLoadBalancer) : type;
Key = key; Key = key;
ExpiryInMs = expiryInMs; ExpiryInMs = expiryInMs;
} }

View File

@ -2,11 +2,13 @@
{ {
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using Ocelot.Configuration.File;
using Ocelot.Values; using Ocelot.Values;
public class ReRoute public class ReRoute
{ {
public ReRoute(List<DownstreamReRoute> downstreamReRoute, public ReRoute(List<DownstreamReRoute> downstreamReRoute,
List<AggregateReRouteConfig> downstreamReRouteConfig,
List<HttpMethod> upstreamHttpMethod, List<HttpMethod> upstreamHttpMethod,
UpstreamPathTemplate upstreamTemplatePattern, UpstreamPathTemplate upstreamTemplatePattern,
string upstreamHost, string upstreamHost,
@ -14,6 +16,7 @@
{ {
UpstreamHost = upstreamHost; UpstreamHost = upstreamHost;
DownstreamReRoute = downstreamReRoute; DownstreamReRoute = downstreamReRoute;
DownstreamReRouteConfig = downstreamReRouteConfig;
UpstreamHttpMethod = upstreamHttpMethod; UpstreamHttpMethod = upstreamHttpMethod;
UpstreamTemplatePattern = upstreamTemplatePattern; UpstreamTemplatePattern = upstreamTemplatePattern;
Aggregator = aggregator; Aggregator = aggregator;
@ -23,6 +26,7 @@
public List<HttpMethod> UpstreamHttpMethod { get; private set; } public List<HttpMethod> UpstreamHttpMethod { get; private set; }
public string UpstreamHost { get; private set; } public string UpstreamHost { get; private set; }
public List<DownstreamReRoute> DownstreamReRoute { get; private set; } public List<DownstreamReRoute> DownstreamReRoute { get; private set; }
public List<AggregateReRouteConfig> DownstreamReRouteConfig { get; private set; }
public string Aggregator {get; private set;} public string Aggregator {get; private set;}
} }
} }

View File

@ -2,7 +2,7 @@
{ {
public class ServiceProviderConfiguration public class ServiceProviderConfiguration
{ {
public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey, int pollingInterval) public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey, int pollingInterval, string @namespace = "")
{ {
ConfigurationKey = configurationKey; ConfigurationKey = configurationKey;
Host = host; Host = host;
@ -10,13 +10,21 @@
Token = token; Token = token;
Type = type; Type = type;
PollingInterval = pollingInterval; PollingInterval = pollingInterval;
Namespace = @namespace;
} }
public string Host { get; } public string Host { get; }
public int Port { get; } public int Port { get; }
public string Type { get; } public string Type { get; }
public string Token { get; } public string Token { get; }
public string ConfigurationKey { get; } public string ConfigurationKey { get; }
public int PollingInterval { get; } public int PollingInterval { get; }
public string Namespace { get; }
} }
} }

View File

@ -12,6 +12,8 @@ namespace Ocelot.DependencyInjection
IConfiguration Configuration { get; } IConfiguration Configuration { get; }
IMvcCoreBuilder MvcCoreBuilder { get; }
IOcelotBuilder AddDelegatingHandler<T>(bool global = false) IOcelotBuilder AddDelegatingHandler<T>(bool global = false)
where T : DelegatingHandler; where T : DelegatingHandler;

View File

@ -42,6 +42,7 @@ namespace Ocelot.DependencyInjection
{ {
public IServiceCollection Services { get; } public IServiceCollection Services { get; }
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
public IMvcCoreBuilder MvcCoreBuilder { get; }
public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot) public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot)
{ {
@ -106,7 +107,6 @@ namespace Ocelot.DependencyInjection
Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>(); Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>();
Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>(); Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>(); Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
Services.TryAddSingleton<IHttpRequester, HttpClientHttpRequester>();
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
// could maybe use a scoped data repository // could maybe use a scoped data repository
@ -133,17 +133,18 @@ namespace Ocelot.DependencyInjection
//add asp.net services.. //add asp.net services..
var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly;
Services.AddMvcCore() this.MvcCoreBuilder = Services.AddMvcCore()
.AddApplicationPart(assembly) .AddApplicationPart(assembly)
.AddControllersAsServices() .AddControllersAsServices()
.AddAuthorization() .AddAuthorization()
.AddJsonFormatters(); .AddJsonFormatters();
Services.AddLogging(); Services.AddLogging();
Services.AddMiddlewareAnalysis(); Services.AddMiddlewareAnalysis();
Services.AddWebEncoders(); Services.AddWebEncoders();
} }
public IOcelotBuilder AddSingletonDefinedAggregator<T>() public IOcelotBuilder AddSingletonDefinedAggregator<T>()
where T : class, IDefinedAggregator where T : class, IDefinedAggregator
{ {
@ -170,7 +171,7 @@ namespace Ocelot.DependencyInjection
if(global) if(global)
{ {
Services.AddTransient<THandler>(); Services.AddTransient<THandler>();
Services.AddTransient<GlobalDelegatingHandler>(s => { Services.AddTransient<GlobalDelegatingHandler>(s =>{
var service = s.GetService<THandler>(); var service = s.GetService<THandler>();
return new GlobalDelegatingHandler(service); return new GlobalDelegatingHandler(service);
}); });

View File

@ -37,6 +37,7 @@
CouldNotFindPlaceholderError, CouldNotFindPlaceholderError,
CouldNotFindAggregatorError, CouldNotFindAggregatorError,
CannotAddPlaceholderError, CannotAddPlaceholderError,
CannotRemovePlaceholderError CannotRemovePlaceholderError,
QuotaExceededError
} }
} }

View File

@ -1,6 +1,6 @@
namespace Ocelot.Infrastructure.Claims.Parser namespace Ocelot.Infrastructure.Claims.Parser
{ {
using Errors; using Microsoft.Extensions.Primitives;
using Responses; using Responses;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -45,11 +45,11 @@
private Response<string> GetValue(IEnumerable<Claim> claims, string key) private Response<string> GetValue(IEnumerable<Claim> claims, string key)
{ {
var claim = claims.FirstOrDefault(c => c.Type == key); var claimValues = claims.Where(c => c.Type == key).Select(c => c.Value).ToArray();
if (claim != null) if (claimValues.Length > 0)
{ {
return new OkResponse<string>(claim.Value); return new OkResponse<string>(new StringValues(claimValues).ToString());
} }
return new ErrorResponse<string>(new CannotFindClaimError($"Cannot find claim for key: {key}")); return new ErrorResponse<string>(new CannotFindClaimError($"Cannot find claim for key: {key}"));

View File

@ -5,12 +5,12 @@ namespace Ocelot.Middleware.Multiplexer
public class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory public class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory
{ {
private readonly UserDefinedResponseAggregator _userDefined; private readonly UserDefinedResponseAggregator _userDefined;
private readonly SimpleJsonResponseAggregator _simple; private readonly IResponseAggregator _simple;
public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider) public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider, IResponseAggregator responseAggregator)
{ {
_userDefined = new UserDefinedResponseAggregator(provider); _userDefined = new UserDefinedResponseAggregator(provider);
_simple = new SimpleJsonResponseAggregator(); _simple = responseAggregator;
} }
public IResponseAggregator Get(ReRoute reRoute) public IResponseAggregator Get(ReRoute reRoute)

View File

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
namespace Ocelot.Middleware.Multiplexer namespace Ocelot.Middleware.Multiplexer
{ {
@ -16,31 +18,105 @@ namespace Ocelot.Middleware.Multiplexer
public async Task Multiplex(DownstreamContext context, ReRoute reRoute, OcelotRequestDelegate next) public async Task Multiplex(DownstreamContext context, ReRoute reRoute, OcelotRequestDelegate next)
{ {
var tasks = new Task<DownstreamContext>[reRoute.DownstreamReRoute.Count]; var reRouteKeysConfigs = reRoute.DownstreamReRouteConfig;
if (reRouteKeysConfigs == null || !reRouteKeysConfigs.Any())
for (var i = 0; i < reRoute.DownstreamReRoute.Count; i++)
{ {
var downstreamContext = new DownstreamContext(context.HttpContext) var tasks = new Task<DownstreamContext>[reRoute.DownstreamReRoute.Count];
for (var i = 0; i < reRoute.DownstreamReRoute.Count; i++)
{
var downstreamContext = new DownstreamContext(context.HttpContext)
{
TemplatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues,
Configuration = context.Configuration,
DownstreamReRoute = reRoute.DownstreamReRoute[i],
};
tasks[i] = Fire(downstreamContext, next);
}
await Task.WhenAll(tasks);
var contexts = new List<DownstreamContext>();
foreach (var task in tasks)
{
var finished = await task;
contexts.Add(finished);
}
await Map(reRoute, context, contexts);
}
else
{
var downstreamContextMain = new DownstreamContext(context.HttpContext)
{ {
TemplatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues, TemplatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues,
Configuration = context.Configuration, Configuration = context.Configuration,
DownstreamReRoute = reRoute.DownstreamReRoute[i], DownstreamReRoute = reRoute.DownstreamReRoute[0],
}; };
var mainResponse = await Fire(downstreamContextMain, next);
tasks[i] = Fire(downstreamContext, next); if (reRoute.DownstreamReRoute.Count == 1)
{
MapNotAggregate(context, new List<DownstreamContext>() { mainResponse });
return;
}
var tasks = new List<Task<DownstreamContext>>();
if (mainResponse.DownstreamResponse == null)
{
return;
}
var content = await mainResponse.DownstreamResponse.Content.ReadAsStringAsync();
var jObject = Newtonsoft.Json.Linq.JToken.Parse(content);
for (var i = 1; i < reRoute.DownstreamReRoute.Count; i++)
{
var templatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues;
var downstreamReRoute = reRoute.DownstreamReRoute[i];
var matchAdvancedAgg = reRouteKeysConfigs.FirstOrDefault(q => q.ReRouteKey == downstreamReRoute.Key);
if (matchAdvancedAgg != null)
{
var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct().ToList();
foreach (var value in values)
{
var downstreamContext = new DownstreamContext(context.HttpContext)
{
TemplatePlaceholderNameAndValues = new List<PlaceholderNameAndValue>(templatePlaceholderNameAndValues),
Configuration = context.Configuration,
DownstreamReRoute = downstreamReRoute,
};
downstreamContext.TemplatePlaceholderNameAndValues.Add(new PlaceholderNameAndValue("{" + matchAdvancedAgg.Parameter + "}", value.ToString()));
tasks.Add(Fire(downstreamContext, next));
}
}
else
{
var downstreamContext = new DownstreamContext(context.HttpContext)
{
TemplatePlaceholderNameAndValues = new List<PlaceholderNameAndValue>(templatePlaceholderNameAndValues),
Configuration = context.Configuration,
DownstreamReRoute = downstreamReRoute,
};
tasks.Add(Fire(downstreamContext, next));
}
}
await Task.WhenAll(tasks);
var contexts = new List<DownstreamContext>() { mainResponse };
foreach (var task in tasks)
{
var finished = await task;
contexts.Add(finished);
}
await Map(reRoute, context, contexts);
} }
await Task.WhenAll(tasks);
var contexts = new List<DownstreamContext>();
foreach (var task in tasks)
{
var finished = await task;
contexts.Add(finished);
}
await Map(reRoute, context, contexts);
} }
private async Task Map(ReRoute reRoute, DownstreamContext context, List<DownstreamContext> contexts) private async Task Map(ReRoute reRoute, DownstreamContext context, List<DownstreamContext> contexts)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -21,19 +22,54 @@ namespace Ocelot.Middleware.Multiplexer
contentBuilder.Append("{"); contentBuilder.Append("{");
for (var i = 0; i < downstreamContexts.Count; i++) var responseKeys = downstreamContexts.Select(s => s.DownstreamReRoute.Key).Distinct().ToList();
for (var k = 0; k < responseKeys.Count; k++)
{ {
if (downstreamContexts[i].IsError) var contexts = downstreamContexts.Where(w => w.DownstreamReRoute.Key == responseKeys[k]).ToList();
if (contexts.Count == 1)
{ {
MapAggregateError(originalContext, downstreamContexts, i); if (contexts[0].IsError)
return; {
MapAggregateError(originalContext, contexts[0]);
return;
}
var content = await contexts[0].DownstreamResponse.Content.ReadAsStringAsync();
contentBuilder.Append($"\"{responseKeys[k]}\":{content}");
}
else
{
contentBuilder.Append($"\"{responseKeys[k]}\":");
contentBuilder.Append("[");
for (var i = 0; i < contexts.Count; i++)
{
if (contexts[i].IsError)
{
MapAggregateError(originalContext, contexts[i]);
return;
}
var content = await contexts[i].DownstreamResponse.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(content))
{
continue;
}
contentBuilder.Append($"{content}");
if (i + 1 < contexts.Count)
{
contentBuilder.Append(",");
}
}
contentBuilder.Append("]");
} }
var content = await downstreamContexts[i].DownstreamResponse.Content.ReadAsStringAsync(); if (k + 1 < responseKeys.Count)
contentBuilder.Append($"\"{downstreamContexts[i].DownstreamReRoute.Key}\":{content}");
if (i + 1 < downstreamContexts.Count)
{ {
contentBuilder.Append(","); contentBuilder.Append(",");
} }
@ -43,16 +79,16 @@ namespace Ocelot.Middleware.Multiplexer
var stringContent = new StringContent(contentBuilder.ToString()) var stringContent = new StringContent(contentBuilder.ToString())
{ {
Headers = {ContentType = new MediaTypeHeaderValue("application/json")} Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
}; };
originalContext.DownstreamResponse = new DownstreamResponse(stringContent, HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), "cannot return from aggregate..which reason phrase would you use?"); originalContext.DownstreamResponse = new DownstreamResponse(stringContent, HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), "cannot return from aggregate..which reason phrase would you use?");
} }
private static void MapAggregateError(DownstreamContext originalContext, List<DownstreamContext> downstreamContexts, int i) private static void MapAggregateError(DownstreamContext originalContext, DownstreamContext downstreamContext)
{ {
originalContext.Errors.AddRange(downstreamContexts[i].Errors); originalContext.Errors.AddRange(downstreamContext.Errors);
originalContext.DownstreamResponse = downstreamContexts[i].DownstreamResponse; originalContext.DownstreamResponse = downstreamContext.DownstreamResponse;
} }
} }
} }

View File

@ -26,7 +26,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="7.6.104" /> <PackageReference Include="FluentValidation" Version="8.1.3" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.MiddlewareAnalysis" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.MiddlewareAnalysis" Version="2.1.1" />

View File

@ -70,6 +70,9 @@ namespace Ocelot.RateLimit.Middleware
// break execution // break execution
await ReturnQuotaExceededResponse(context.HttpContext, options, retrystring); await ReturnQuotaExceededResponse(context.HttpContext, options, retrystring);
// Set Error
context.Errors.Add(new QuotaExceededError(this.GetResponseMessage(options)));
return; return;
} }
} }
@ -117,7 +120,7 @@ namespace Ocelot.RateLimit.Middleware
public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter)
{ {
var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage; var message = this.GetResponseMessage(option);
if (!option.DisableRateLimitHeaders) if (!option.DisableRateLimitHeaders)
{ {
@ -128,6 +131,14 @@ namespace Ocelot.RateLimit.Middleware
return httpContext.Response.WriteAsync(message); return httpContext.Response.WriteAsync(message);
} }
private string GetResponseMessage(RateLimitOptions option)
{
var message = string.IsNullOrEmpty(option.QuotaExceededMessage)
? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}."
: option.QuotaExceededMessage;
return message;
}
private Task SetRateLimitHeaders(object rateLimitHeaders) private Task SetRateLimitHeaders(object rateLimitHeaders)
{ {
var headers = (RateLimitHeaders)rateLimitHeaders; var headers = (RateLimitHeaders)rateLimitHeaders;

View File

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

View File

@ -44,7 +44,7 @@ namespace Ocelot.Request.Middleware
Port = Port, Port = Port,
Host = Host, Host = Host,
Path = AbsolutePath, Path = AbsolutePath,
Query = Query, Query = RemoveLeadingQuestionMark(Query),
Scheme = Scheme Scheme = Scheme
}; };
@ -59,7 +59,7 @@ namespace Ocelot.Request.Middleware
Port = Port, Port = Port,
Host = Host, Host = Host,
Path = AbsolutePath, Path = AbsolutePath,
Query = Query, Query = RemoveLeadingQuestionMark(Query),
Scheme = Scheme Scheme = Scheme
}; };
@ -70,5 +70,15 @@ namespace Ocelot.Request.Middleware
{ {
return ToUri(); return ToUri();
} }
private string RemoveLeadingQuestionMark(string query)
{
if (!string.IsNullOrEmpty(query) && query.StartsWith("?"))
{
return query.Substring(1);
}
return query;
}
} }
} }

View File

@ -33,6 +33,8 @@ namespace Ocelot.Requester.Middleware
Logger.LogDebug("setting http response message"); Logger.LogDebug("setting http response message");
context.DownstreamResponse = new DownstreamResponse(response.Data); context.DownstreamResponse = new DownstreamResponse(response.Data);
await _next.Invoke(context);
} }
} }
} }

View File

@ -32,6 +32,15 @@ namespace Ocelot.Responder
AddHeaderIfDoesntExist(context, httpResponseHeader); AddHeaderIfDoesntExist(context, httpResponseHeader);
} }
SetStatusCode(context, (int)response.StatusCode);
context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = response.ReasonPhrase;
if (response.Content is null)
{
return;
}
foreach (var httpResponseHeader in response.Content.Headers) foreach (var httpResponseHeader in response.Content.Headers)
{ {
AddHeaderIfDoesntExist(context, new Header(httpResponseHeader.Key, httpResponseHeader.Value)); AddHeaderIfDoesntExist(context, new Header(httpResponseHeader.Key, httpResponseHeader.Value));
@ -44,10 +53,6 @@ namespace Ocelot.Responder
AddHeaderIfDoesntExist(context, new Header("Content-Length", new []{ response.Content.Headers.ContentLength.ToString() }) ); AddHeaderIfDoesntExist(context, new Header("Content-Length", new []{ response.Content.Headers.ContentLength.ToString() }) );
} }
SetStatusCode(context, (int)response.StatusCode);
context.Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = response.ReasonPhrase;
using(content) using(content)
{ {
if (response.StatusCode != HttpStatusCode.NotModified && context.Response.ContentLength != 0) if (response.StatusCode != HttpStatusCode.NotModified && context.Response.ContentLength != 0)

View File

@ -141,6 +141,100 @@ namespace Ocelot.AcceptanceTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_return_response_200_with_advanced_aggregate_configs()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51889,
}
},
UpstreamPathTemplate = "/Comments",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Comments"
},
new FileReRoute
{
DownstreamPathTemplate = "/users/{userId}",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51890,
}
},
UpstreamPathTemplate = "/UserDetails",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "UserDetails"
},
new FileReRoute
{
DownstreamPathTemplate = "/posts/{postId}",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51887,
}
},
UpstreamPathTemplate = "/PostDetails",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "PostDetails"
}
},
Aggregates = new List<FileAggregateReRoute>
{
new FileAggregateReRoute
{
UpstreamPathTemplate = "/",
UpstreamHost = "localhost",
ReRouteKeys = new List<string>
{
"Comments",
"UserDetails",
"PostDetails"
},
ReRouteKeysConfig = new List<AggregateReRouteConfig>()
{
new AggregateReRouteConfig(){ReRouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"},
new AggregateReRouteConfig(){ReRouteKey = "PostDetails",JsonPath = "$[*].postId",Parameter = "postId"}
},
}
}
};
var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}";
var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}";
var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]";
var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}";
this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51889", "/", 200, commentsResponseContent))
.Given(x => x.GivenServiceTwoIsRunning("http://localhost:51890", "/users/1", 200, userDetailsResponseContent))
.Given(x => x.GivenServiceTwoIsRunning("http://localhost:51887", "/posts/2", 200, postDetailsResponseContent))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}
[Fact] [Fact]
public void should_return_response_200_with_simple_url_user_defined_aggregate() public void should_return_response_200_with_simple_url_user_defined_aggregate()
{ {
@ -189,8 +283,8 @@ namespace Ocelot.AcceptanceTests
UpstreamHost = "localhost", UpstreamHost = "localhost",
ReRouteKeys = new List<string> ReRouteKeys = new List<string>
{ {
"Tom", "Laura",
"Laura" "Tom"
}, },
Aggregator = "FakeDefinedAggregator" Aggregator = "FakeDefinedAggregator"
} }
@ -258,8 +352,8 @@ namespace Ocelot.AcceptanceTests
UpstreamHost = "localhost", UpstreamHost = "localhost",
ReRouteKeys = new List<string> ReRouteKeys = new List<string>
{ {
"Tom", "Laura",
"Laura" "Tom"
} }
} }
} }
@ -326,8 +420,9 @@ namespace Ocelot.AcceptanceTests
UpstreamHost = "localhost", UpstreamHost = "localhost",
ReRouteKeys = new List<string> ReRouteKeys = new List<string>
{ {
"Tom", "Laura",
"Laura" "Tom"
} }
} }
} }
@ -394,8 +489,8 @@ namespace Ocelot.AcceptanceTests
UpstreamHost = "localhost", UpstreamHost = "localhost",
ReRouteKeys = new List<string> ReRouteKeys = new List<string>
{ {
"Tom", "Laura",
"Laura" "Tom"
} }
} }
} }
@ -462,8 +557,8 @@ namespace Ocelot.AcceptanceTests
UpstreamHost = "localhost", UpstreamHost = "localhost",
ReRouteKeys = new List<string> ReRouteKeys = new List<string>
{ {
"Tom", "Laura",
"Laura" "Tom"
} }
} }
} }

View File

@ -434,7 +434,8 @@
Host = "localhost", Host = "localhost",
Port = consulPort, Port = consulPort,
Type = "PollConsul", Type = "PollConsul",
PollingInterval = 0 PollingInterval = 0,
Namespace = string.Empty
} }
} }
}; };

View File

@ -30,7 +30,8 @@ namespace Ocelot.UnitTests.Configuration
Port = 1234, Port = 1234,
Type = "ServiceFabric", Type = "ServiceFabric",
Token = "testtoken", Token = "testtoken",
ConfigurationKey = "woo" ConfigurationKey = "woo",
Namespace ="default"
} }
}; };
@ -40,6 +41,7 @@ namespace Ocelot.UnitTests.Configuration
.WithType("ServiceFabric") .WithType("ServiceFabric")
.WithToken("testtoken") .WithToken("testtoken")
.WithConfigurationKey("woo") .WithConfigurationKey("woo")
.WithNamespace("default")
.Build(); .Build();
this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig)) this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig))
@ -64,6 +66,7 @@ namespace Ocelot.UnitTests.Configuration
_result.Port.ShouldBe(expected.Port); _result.Port.ShouldBe(expected.Port);
_result.Token.ShouldBe(expected.Token); _result.Token.ShouldBe(expected.Token);
_result.Type.ShouldBe(expected.Type); _result.Type.ShouldBe(expected.Type);
_result.Namespace.ShouldBe(expected.Namespace);
_result.ConfigurationKey.ShouldBe(expected.ConfigurationKey); _result.ConfigurationKey.ShouldBe(expected.ConfigurationKey);
} }
} }

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Configuration;
using Ocelot.Logging;
using Ocelot.Provider.Kubernetes;
using Shouldly;
using System;
using Xunit;
namespace Ocelot.UnitTests.Kubernetes
{
public class KubeProviderFactoryTests
{
private readonly IServiceProvider _provider;
public KubeProviderFactoryTests()
{
var services = new ServiceCollection();
var loggerFactory = new Mock<IOcelotLoggerFactory>();
var logger = new Mock<IOcelotLogger>();
loggerFactory.Setup(x => x.CreateLogger<Kube>()).Returns(logger.Object);
var kubeFactory = new Mock<IKubeApiClientFactory>();
services.AddSingleton(kubeFactory.Object);
services.AddSingleton(loggerFactory.Object);
_provider = services.BuildServiceProvider();
}
[Fact]
public void should_return_KubeServiceDiscoveryProvider()
{
var provider = KubernetesProviderFactory.Get(_provider, new ServiceProviderConfiguration("kube", "localhost", 443, "", "", 1,"dev"), "");
provider.ShouldBeOfType<Kube>();
}
}
}

View File

@ -0,0 +1,147 @@
using KubeClient.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Moq;
using Newtonsoft.Json;
using Ocelot.Logging;
using Ocelot.Provider.Kubernetes;
using Ocelot.Values;
using Shouldly;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Kubernetes
{
public class KubeServiceDiscoveryProviderTests : IDisposable
{
private IWebHost _fakeKubeBuilder;
private ServiceV1 _serviceEntries;
private Kube _provider;
private readonly string _serviceName;
private readonly string _namespaces;
private readonly int _port;
private readonly string _kubeHost;
private readonly string _fakekubeServiceDiscoveryUrl;
private List<Service> _services;
private readonly Mock<IOcelotLoggerFactory> _factory;
private readonly Mock<IOcelotLogger> _logger;
private string _receivedToken;
private readonly IKubeApiClientFactory _clientFactory;
public KubeServiceDiscoveryProviderTests()
{
_serviceName = "test";
_namespaces = "dev";
_port = 8001;
_kubeHost = "localhost";
_fakekubeServiceDiscoveryUrl = $"http://{_kubeHost}:{_port}";
_serviceEntries = new ServiceV1();
_factory = new Mock<IOcelotLoggerFactory>();
_clientFactory = new KubeApiClientFactory();
_logger = new Mock<IOcelotLogger>();
_factory.Setup(x => x.CreateLogger<Kube>()).Returns(_logger.Object);
var config = new KubeRegistryConfiguration()
{
ApiEndPoint = new Uri(_fakekubeServiceDiscoveryUrl),
AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG",
AllowInsecure = true,
AuthStrategy = KubeClient.KubeAuthStrategy.BearerToken,
KeyOfServiceInK8s = _serviceName,
KubeNamespace = _namespaces
};
_provider = new Kube(config, _factory.Object, _clientFactory);
}
[Fact]
public void should_return_service_from_k8s()
{
var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG";
var serviceEntryOne = new ServiceV1()
{
Kind = "service",
ApiVersion = "1.0",
Metadata = new ObjectMetaV1()
{
Namespace = "dev"
},
Spec = new ServiceSpecV1()
{
ClusterIP = "localhost"
},
Status = new ServiceStatusV1() {
LoadBalancer = new LoadBalancerStatusV1()
}
};
serviceEntryOne.Spec.Ports.Add(
new ServicePortV1()
{
Port = 80
}
);
this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces))
.And(x => GivenTheServicesAreRegisteredWithKube(serviceEntryOne))
.When(x => WhenIGetTheServices())
.Then(x => ThenTheCountIs(1))
.And(_ => _receivedToken.ShouldBe(token))
.BDDfy();
}
private void ThenTheCountIs(int count)
{
_services.Count.ShouldBe(count);
}
private void WhenIGetTheServices()
{
_services = _provider.Get().GetAwaiter().GetResult();
}
private void GivenTheServicesAreRegisteredWithKube(ServiceV1 serviceEntries)
{
_serviceEntries = serviceEntries;
}
private void GivenThereIsAFakeKubeServiceDiscoveryProvider(string url, string serviceName, string namespaces)
{
_fakeKubeBuilder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(async context =>
{
if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/services/{serviceName}")
{
if (context.Request.Headers.TryGetValue("Authorization", 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();
_fakeKubeBuilder.Start();
}
public void Dispose()
{
_fakeKubeBuilder?.Dispose();
}
}
}

View File

@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Provider.Kubernetes;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Text;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Kubernetes
{
public class OcelotBuilderExtensionsTests
{
private readonly IServiceCollection _services;
private IServiceProvider _serviceProvider;
private readonly IConfiguration _configRoot;
private IOcelotBuilder _ocelotBuilder;
private Exception _ex;
public OcelotBuilderExtensionsTests()
{
_configRoot = new ConfigurationRoot(new List<IConfigurationProvider>());
_services = new ServiceCollection();
_services.AddSingleton<IHostingEnvironment, HostingEnvironment>();
_services.AddSingleton(_configRoot);
}
[Fact]
public void should_set_up_kubernetes()
{
this.Given(x => WhenISetUpOcelotServices())
.When(x => WhenISetUpKubernetes())
.Then(x => ThenAnExceptionIsntThrown())
.BDDfy();
}
private void WhenISetUpOcelotServices()
{
try
{
_ocelotBuilder = _services.AddOcelot(_configRoot);
}
catch (Exception e)
{
_ex = e;
}
}
private void WhenISetUpKubernetes()
{
try
{
_ocelotBuilder.AddKubernetes();
}
catch (Exception e)
{
_ex = e;
}
}
private void ThenAnExceptionIsntThrown()
{
_ex.ShouldBeNull();
}
}
}

View File

@ -0,0 +1,82 @@
using Moq;
using Ocelot.Infrastructure;
using Ocelot.Logging;
using Ocelot.Provider.Kubernetes;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Text;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Kubernetes
{
public class PollingKubeServiceDiscoveryProviderTests
{
private readonly int _delay;
private PollKube _provider;
private readonly List<Service> _services;
private readonly Mock<IOcelotLoggerFactory> _factory;
private readonly Mock<IOcelotLogger> _logger;
private readonly Mock<IServiceDiscoveryProvider> _kubeServiceDiscoveryProvider;
private List<Service> _result;
public PollingKubeServiceDiscoveryProviderTests()
{
_services = new List<Service>();
_delay = 1;
_factory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>();
_factory.Setup(x => x.CreateLogger<PollKube>()).Returns(_logger.Object);
_kubeServiceDiscoveryProvider = new Mock<IServiceDiscoveryProvider>();
}
[Fact]
public void should_return_service_from_kube()
{
var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List<string>());
this.Given(x => GivenKubeReturns(service))
.When(x => WhenIGetTheServices(1))
.Then(x => ThenTheCountIs(1))
.BDDfy();
}
private void GivenKubeReturns(Service service)
{
_services.Add(service);
_kubeServiceDiscoveryProvider.Setup(x => x.Get()).ReturnsAsync(_services);
}
private void ThenTheCountIs(int count)
{
_result.Count.ShouldBe(count);
}
private void WhenIGetTheServices(int expected)
{
_provider = new PollKube(_delay, _factory.Object, _kubeServiceDiscoveryProvider.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();
}
}
}

View File

@ -18,7 +18,8 @@ namespace Ocelot.UnitTests.Middleware
public ResponseAggregatorFactoryTests() public ResponseAggregatorFactoryTests()
{ {
_provider = new Mock<IDefinedAggregatorProvider>(); _provider = new Mock<IDefinedAggregatorProvider>();
_factory = new InMemoryResponseAggregatorFactory(_provider.Object); _aggregator = new SimpleJsonResponseAggregator();
_factory = new InMemoryResponseAggregatorFactory(_provider.Object, _aggregator);
} }
[Fact] [Fact]

View File

@ -7,11 +7,11 @@ using Castle.Components.DictionaryAdapter;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Configuration.Builder; using Ocelot.Configuration.Builder;
using Ocelot.Errors; using Ocelot.Configuration.File;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Middleware.Multiplexer; using Ocelot.Middleware.Multiplexer;
using Ocelot.Request.Middleware;
using Ocelot.UnitTests.Responder; using Ocelot.UnitTests.Responder;
using Ocelot.Values;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -30,6 +30,58 @@ namespace Ocelot.UnitTests.Middleware
_aggregator = new SimpleJsonResponseAggregator(); _aggregator = new SimpleJsonResponseAggregator();
} }
[Fact]
public void should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig()
{
var commentsDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("Comments").Build();
var userDetailsDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("UserDetails")
.WithUpstreamPathTemplate(new UpstreamPathTemplate("", 0, false, "/v1/users/{userId}"))
.Build();
var downstreamReRoutes = new List<DownstreamReRoute>
{
commentsDownstreamReRoute,
userDetailsDownstreamReRoute
};
var reRoute = new ReRouteBuilder()
.WithDownstreamReRoutes(downstreamReRoutes)
.WithAggregateReRouteConfig(new List<AggregateReRouteConfig>()
{
new AggregateReRouteConfig(){ReRouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}
})
.Build();
var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":1,""text"":""text1""},{""id"":2,""writerId"":2,""postId"":2,""text"":""text2""},{""id"":3,""writerId"":2,""postId"":1,""text"":""text21""}]";
var commentsDownstreamContext = new DownstreamContext(new DefaultHttpContext())
{
DownstreamResponse = new DownstreamResponse(new StringContent(commentsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new EditableList<KeyValuePair<string, IEnumerable<string>>>(), "some reason"),
DownstreamReRoute = commentsDownstreamReRoute
};
var userDetailsResponseContent = @"[{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""},{""id"":2,""firstName"":""reza"",""lastName"":""rezaei""}]";
var userDetailsDownstreamContext = new DownstreamContext(new DefaultHttpContext())
{
DownstreamResponse = new DownstreamResponse(new StringContent(userDetailsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List<KeyValuePair<string, IEnumerable<string>>>(), "some reason"),
DownstreamReRoute = userDetailsDownstreamReRoute
};
var downstreamContexts = new List<DownstreamContext> { commentsDownstreamContext, userDetailsDownstreamContext };
var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + "}";
this.Given(x => GivenTheUpstreamContext(new DownstreamContext(new DefaultHttpContext())))
.And(x => GivenTheReRoute(reRoute))
.And(x => GivenTheDownstreamContext(downstreamContexts))
.When(x => WhenIAggregate())
.Then(x => ThenTheContentIs(expected))
.And(x => ThenTheContentTypeIs("application/json"))
.And(x => ThenTheReasonPhraseIs("cannot return from aggregate..which reason phrase would you use?"))
.BDDfy();
}
[Fact] [Fact]
public void should_aggregate_n_responses_and_set_response_content_on_upstream_context() public void should_aggregate_n_responses_and_set_response_content_on_upstream_context()
{ {

View File

@ -20,6 +20,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj" />
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" /> <ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Administration\Ocelot.Administration.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Administration\Ocelot.Administration.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" />
@ -72,7 +73,7 @@
<PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.1.2" /> <PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.1.2" />
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.2" /> <PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.2" />
<PackageReference Include="Polly" Version="6.0.1" /> <PackageReference Include="Polly" Version="6.0.1" />
<PackageReference Include="Rafty" Version="0.4.4"/> <PackageReference Include="Rafty" Version="0.4.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -125,7 +125,7 @@ namespace Ocelot.UnitTests.Responder
// If this test fails then it's because the number of error codes has changed. // If this test fails then it's because the number of error codes has changed.
// You should make the appropriate changes to the test cases here to ensure // You should make the appropriate changes to the test cases here to ensure
// they cover all the error codes, and then modify this assertion. // they cover all the error codes, and then modify this assertion.
Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(36, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(37, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?");
} }
private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)

View File

@ -39,6 +39,23 @@ namespace Ocelot.UnitTests.Responder
header.ShouldBeEmpty(); header.ShouldBeEmpty();
} }
[Fact]
public void should_ignore_content_if_null()
{
var httpContext = new DefaultHttpContext();
var response = new DownstreamResponse(null, HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(), "some reason");
Should.NotThrow(() =>
{
_responder
.SetResponseOnHttpContext(httpContext, response)
.GetAwaiter()
.GetResult()
;
});
}
[Fact] [Fact]
public void should_have_content_length() public void should_have_content_length()
{ {