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
This commit is contained in:
geffzhang 2019-01-31 18:19:32 +08:00 committed by Marcelo Castagna
parent 05ede70e62
commit 44dccf1fce
42 changed files with 1135 additions and 7 deletions

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

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

@ -23,7 +23,8 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n
features/requestaggregation features/requestaggregation
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

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

@ -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.Namesapce,
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,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</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

@ -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 WithNamesapce(string @namesapce)
{
_namespace = @namesapce;
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

@ -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 k8snamesapce = 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)
.WithNamesapce(k8snamesapce)
.Build(); .Build();
} }
} }

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

@ -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;
Namesapce = @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 Namesapce { get; }
} }
} }

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,13 +41,14 @@ namespace Ocelot.UnitTests.Configuration
.WithType("ServiceFabric") .WithType("ServiceFabric")
.WithToken("testtoken") .WithToken("testtoken")
.WithConfigurationKey("woo") .WithConfigurationKey("woo")
.WithNamesapce("default")
.Build(); .Build();
this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig)) this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig))
.When(x => x.WhenICreate()) .When(x => x.WhenICreate())
.Then(x => x.ThenTheConfigIs(expected)) .Then(x => x.ThenTheConfigIs(expected))
.BDDfy(); .BDDfy();
} }
private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration fileGlobalConfig) private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration fileGlobalConfig)
{ {
@ -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.Namesapce.ShouldBe(expected.Namesapce);
_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

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