Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1225 Update ServiceDiscovery documentation and samples to include Custom Providers #1656

Merged
merged 28 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8f5ab8a
Update servicediscovery documentation to include custom provider
leonluc-dev May 22, 2023
2467d5a
Update servicediscovery.rst
raman-m May 24, 2023
182d9ac
Update servicediscovery.rst
raman-m May 24, 2023
9e07104
Added custom service provider sample
leonluc-dev May 25, 2023
a0d865a
Minor clarification to custom service discovery provider docs
leonluc-dev May 25, 2023
b4bf163
Move usings to the top. Use file-scoped namespace declaration
raman-m May 26, 2023
c4d2f08
Moved custom service discovery sample
leonluc-dev May 31, 2023
5d1c479
Added custom service provider sample
leonluc-dev May 25, 2023
f7e9ad9
Move usings to the top. Use file-scoped namespace declaration
raman-m May 26, 2023
805b92b
Moved custom service discovery sample
leonluc-dev May 31, 2023
2bc5863
Add 2 options/ways of solution development via ConfigureServices
raman-m Jun 9, 2023
9446149
Upgrade DownstreamService to ASP.NET 7
raman-m Jun 10, 2023
ebf69f2
Upgrade ApiGateway to ASP.NET 7
raman-m Jun 10, 2023
38d8b91
Update README.md
raman-m Jun 10, 2023
6f51bda
Removed redundant spring section in config
leonluc-dev Jun 20, 2023
0a34cb5
Workaround for the Categories route
raman-m Jun 21, 2023
36d5c5c
Rename controller: class name should be the same as file name
raman-m Jun 21, 2023
4e56f42
Upgrade to Web API app: multiple startup profiles, add Docker profile…
raman-m Jun 21, 2023
a5aa5d2
Correct registration of IServiceDiscoveryProviderFactory interface
raman-m Jun 21, 2023
909a124
Update README.md: Fix upstream path because of case sensitivity
raman-m Jun 21, 2023
5624ed5
Update servicediscovery.rst: Update Custom Providers section. Add sam…
raman-m Jun 21, 2023
93a8cd3
Update servicediscovery.rst: Update actual code from the sample
raman-m Jun 21, 2023
0848927
Remove obsolete code
raman-m Jun 21, 2023
0091857
CS8632 The annotation for nullable reference types should only be use…
raman-m Sep 8, 2023
afd495f
Revert to previous state
raman-m Sep 8, 2023
2127294
Revert back to the version from ThreeMammals:develop
raman-m Sep 8, 2023
d14543b
Update servicediscovery.rst: English checking
raman-m Sep 21, 2023
0cb1958
Merge branch 'develop' into develop
raman-m Sep 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Ocelot.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
VisualStudioVersion = 17.6.33723.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}"
EndProject
Expand Down Expand Up @@ -86,6 +86,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-discovery", "service-discovery", "{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\OcelotServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{D37209EA-C13E-42AE-B851-A8604F1FCD0E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -188,6 +194,14 @@ Global
{C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU
{D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.Build.0 = Release|Any CPU
{E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -224,6 +238,9 @@ Global
{11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702}
{731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C}
{C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7}
{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C}
{D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}
{E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48}
Expand Down
133 changes: 127 additions & 6 deletions docs/features/servicediscovery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ Ocelot allows you to specify a service discovery provider and will use this to f
GlobalConfiguration section which means the same service discovery provider will be used for all Routes you specify a ServiceName for at Route level.

Consul
^^^^^^
------

The first thing you need to do is install the NuGet package that provides Consul support in Ocelot.

``Install-Package Ocelot.Provider.Consul``
.. code-block:: powershell

Install-Package Ocelot.Provider.Consul

Then add the following to your ConfigureServices method.

Expand Down Expand Up @@ -92,7 +94,7 @@ Or
}

ACL Token
---------
^^^^^^^^^

If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below.

Expand All @@ -108,13 +110,15 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade
Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request.

Eureka
^^^^^^
------

This feature was requested as part of `Issue 262 <https://github.com/ThreeMammals/Ocelot/issues/262>`_ . to add support for Netflix's Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something to do with `Pivotal <https://pivotal.io/platform>`_! Anyway enough of the background.

The first thing you need to do is install the NuGet package that provides Eureka support in Ocelot.

``Install-Package Ocelot.Provider.Eureka``
.. code-block:: powershell

Install-Package Ocelot.Provider.Eureka

Then add the following to your ConfigureServices method.

Expand Down Expand Up @@ -150,7 +154,7 @@ Ocelot will now register all the necessary services when it starts up and if you
Ocelot will use the scheme (http/https) set in Eureka if these values are not provided in ocelot.json

Dynamic Routing
^^^^^^^^^^^^^^^
---------------

This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issues/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.

Expand Down Expand Up @@ -242,3 +246,120 @@ Ocelot also allows you to set DynamicRoutes which lets you set rate limiting rul
This configuration means that if you have a request come into Ocelot on /product/* then dynamic routing will kick in and ocelot will use the rate limiting set against the product service in the DynamicRoutes section.

Please take a look through all of the docs to understand these options.

Custom Providers
----------------------------------

Ocelot also allows you to create a custom ServiceDiscovery implementation.
This is done by implementing the ``IServiceDiscoveryProvider`` interface like in the following example:

.. code-block:: csharp

public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider
{
private readonly DownstreamRoute _downstreamRoute;

public MyServiceDiscoveryProvider(DownstreamRoute downstreamRoute)
{
_downstreamRoute = downstreamRoute;
}

public async Task<List<Service>> Get()
{
var services = new List<Service>();
//...
//Add service(s) to the list matching the _downstreamRoute
return services;
}
}

And set its class name as the provider type in **ocelot.json**:

.. code-block:: json

"GlobalConfiguration": {
"ServiceDiscoveryProvider": {
"Type": "MyServiceDiscoveryProvider"
}
}

Finally, in the application's **ConfigureServices** method register a ``ServiceDiscoveryFinderDelegate`` to initialize and return the provider:

.. code-block:: csharp

ServiceDiscoveryFinderDelegate serviceDiscoveryFinder = (provider, config, route) =>
{
return new MyServiceDiscoveryProvider(route);
};
services.AddSingleton(serviceDiscoveryFinder);
services.AddOcelot();

Custom provider sample
^^^^^^^^^^^^^^^^^^^^^^

In order to introduce the basic template of custom Service Discovery provider we've prepared nice Web API sample:

| **Link**: `samples <../../samples>`_ / `OcelotServiceDiscovery <../../samples/OcelotServiceDiscovery>`_
| **Solution**: `Ocelot.Samples.ServiceDiscovery.sln <../../samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln>`_

This solution contains the following projects:

- `ApiGateway <#apigateway>`_
- `DownstreamService <#downstreamservice>`_

This solution is ready for any kind of deployment. All services are linked which means all ports and hosts are prepared for immediate usage (running in Visual Studio).

All instructions for running this solution are in `README.md <../../samples/OcelotServiceDiscovery/README.md>`_.

DownstreamService
"""""""""""""""""

This project is a single downstream service to be reused in `ApiGateway <#apigateway>`_ routes.
It has multiple **launchSettings.json** profiles for your favorite startup and hosting scenarios: in Visual Studio running sessions, console Kestrel hosting and Docker deployment for sure.

ApiGateway
""""""""""

This project includes custom Service Discovery provider, and it has a route(s) to `DownstreamService <#downstreamservice>`_ services only in **ocelot.json** file.
You are welcome to add more routes!

The main source code for the custom provider is in the `ServiceDiscovery <../../samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery>`_ folder:
the ``MyServiceDiscoveryProvider`` and ``MyServiceDiscoveryProviderFactory`` classes. You are welcome to design and develop them!

Also, the keystone of this custom provider locates in ``ConfigureServices`` method where you could choose a design & implementation options: easy or more complex:

.. code-block:: csharp

builder.ConfigureServices(s =>
{
// Initialize from app configuration or hardcode/choose the best option.
bool easyWay = true;

if (easyWay)
{
// Option #1. Define custom finder delegate to instantiate custom provider
// by default factory which is ServiceDiscoveryProviderFactory
s.AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)
=> new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute));
}
else
{
// Option #2. Abstract from default factory (ServiceDiscoveryProviderFactory) and from FinderDelegate,
// and build custom factory by implementation of the IServiceDiscoveryProviderFactory interface.
s.RemoveAll<IServiceDiscoveryProviderFactory>();
s.AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();

// Will not be called, but it is required for internal validators, aka life hack
s.AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute) => null);
}

s.AddOcelot();
});

Easy way, simple design means you develop provider class only and specify ``ServiceDiscoveryFinderDelegate`` object for default ``ServiceDiscoveryProviderFactory`` in Ocelot core.

More complex design means you develop both, provider and provider's factory classes. After that you need to add the ``IServiceDiscoveryProviderFactory``
interface to DI-container with removal of registered default ``ServiceDiscoveryProviderFactory`` class.
Please note, in this case default ``ServiceDiscoveryProviderFactory`` in Ocelot core will not be used.
Additionally you don't have to specify ``"Type": "MyServiceDiscoveryProvider"`` in the **ServiceDiscoveryProvider** options of the GlobalConfiguration settings.
But you can leave this ``Type`` option for compatibility between both designs.
25 changes: 25 additions & 0 deletions samples/OcelotServiceDiscovery/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Ocelot\Ocelot.csproj" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions samples/OcelotServiceDiscovery/ApiGateway/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.ServiceDiscovery;

namespace Ocelot.Samples.ServiceDiscovery.ApiGateway;

using ServiceDiscovery;

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();
})
.ConfigureServices(s =>
{
// Initialize from app configuration or hardcode/choose the best option.
bool easyWay = true;

if (easyWay)
{
// Option #1. Define custom finder delegate to instantiate custom provider
// by default factory which is ServiceDiscoveryProviderFactory
s.AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)
=> new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute));
}
else
{
// Option #2. Abstract from default factory (ServiceDiscoveryProviderFactory) and from FinderDelegate,
// and build custom factory by implementation of the IServiceDiscoveryProviderFactory interface.
s.RemoveAll<IServiceDiscoveryProviderFactory>();
s.AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();

// Will not be called, but it is required for internal validators, aka life hack
s.AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)
=> null);
}

s.AddOcelot();
})
.Configure(a =>
{
a.UseOcelot().Wait();
})
.Build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54060/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ApiGateway": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "categories",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000/"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Ocelot.Configuration;
using Ocelot.ServiceDiscovery.Providers;
using Ocelot.Values;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery;

public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly ServiceProviderConfiguration _config;
private readonly DownstreamRoute _downstreamRoute;

public MyServiceDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute)
{
_serviceProvider = serviceProvider;
_config = config;
_downstreamRoute = downstreamRoute;
}

public Task<List<Service>> Get()
{

// Returns a list of service(s) that match the downstream route passed to the provider
var services = new List<Service>();

// Apply configuration checks
// ... if (_config.Host)
if (_downstreamRoute.ServiceName.Equals("downstream-service"))
{
//For this example we simply do a manual match to a single service
var service = new Service(
name: "downstream-service",
hostAndPort: new ServiceHostAndPort("localhost", 5001),
id: "downstream-service-1",
version: "1.0",
tags: new string[] { "downstream", "hardcoded" }
);

services.Add(service);
}

return Task.FromResult(services);
}
}
Loading