Open P9avel opened 2 years ago
There is not a swagger endpoint for the proxy - the proxy itself does not directly expose services that would have a swagger definition - it's the destinations of the proxy that might. Depending on the routes the proxy is configured with, it could expose multiple services that each has an OpenAPI definition. In theory you could create a service that would enumerate the routes the proxy supports, takes a OpenAPI definition and does a transform based on the route definition, but that has not been automated.
Depending on how the swagger UI is generated, it may work correctly through the proxy, but it will depend on how it generates links and request URLs for testing services.
We do this by probing routes for well known swagger.json endpoints, then expose those via an enumerator like https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1093#issuecomment-964622538
This allows us to have a single swagger UI served up by the proxy which has a drop down to select one of the routes that exposes a swagger.json file.
Authentication is a bit hit or miss. For example, some backends use PKCE and others don't but the swagger.json doesn't have a way to convey that so it has to be hard coded one way or the other. Haven't found a way to support both yet.
Also need to set up CORS to allow whatever host you're exposing the SwaggerUI as to call in to the actual routes (assuming they're not the same origin).
Other than the mentioned authentication issues, it "works" to select one of the routes on the Swagger UI and Try It! which calls to the backend as expected.
We discussed this in triage. Its not a feature that we are likely to build for YARP, however if the community implements it, we'll seriously consider a PR.
+1
This is how I do with Swashbuckle. This is a work in progress, there is a lot of missing part (matching, cache...)
Create new specs list based on your services:
app.UseSwaggerUI(opt =>
{
opt.SwaggerEndpoint("/swagger/1.0/swagger.json", "Gateway service by itself- 1.0");
opt.SwaggerEndpoint("/swagger/serviceA/1.0/swagger.json", "ServiceA - 1.0");
opt.SwaggerEndpoint("/swagger/serviceB/1.0/swagger.json", "ServiceB - 1.0");
});
Add endpoint to handle specs request
app.UseEndpoints(endpoints =>
{
// map yarp swagger
endpoints.MapGetSwaggerForYarp(_configuration);
// All controller
endpoints.MapReverseProxy();
endpoints.MapControllers().RequireAuthorization();
});
Create swagger options class
internal class GatewaySwaggerSpec
{
public string Endpoint { get; set; }
public string Spec { get; set; }
public string OriginPath { get; set; }
public string TargetPath { get; set; }
}
Config exemple, the main point is to provide ClusterId inside cluster configuration object to be able to match routes and cluster
{
"Gateway": {
"Routes": {
"serviceA": {
"ClusterId": "ServiceA",
"AuthorizationPolicy": "Validated",
"Match": {
"Path": "/api/advertising/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "/api/{**catch-all}" }
]
}
},
"Clusters": {
"ServiceA": {
"ClusterId": "ServiceA",
"Swagger": {
"Endpoint": "/swagger/serviceA/1.0/swagger.json",
"Spec": "http://localhost:5003/swagger/serviceA/1.0/swagger.json",
"OriginPath": "/api/serviceA",
"TargetPath": "/api"
},
"Destinations": {
"serviceA": {
"Address": ""
}
}
}
}
}
}
Get all swagger to implements from the configuration
public static void MapGetSwaggerForYarp(this IEndpointRouteBuilder endpoints,IConfiguration configuration)
{
var clusters = configuration.GetSection("Gateway:Clusters");
var routes = configuration.GetSection("Gateway:Routes").Get<List<RouteConfig>>();
if (clusters != null)
{
foreach (var child in clusters.GetChildren())
{
if (child.GetSection("Swagger").Exists())
{
var cluster = child.Get<ClusterConfig>();
var swagger = child.GetSection("Swagger").Get<GatewaySwaggerSpec>();
// Map swagger endpoint if we find a cluster with swagger configuration
endpoints.MapSwaggerSpecs(routes, cluster, swagger);
}
}
}
}
And finaly, this is how I get the swagger spec and filter routes and operations that does not match the proxy configuration
public static void MapSwaggerSpecs(this IEndpointRouteBuilder endpoints, List<RouteConfig> config, ClusterConfig cluster, GatewaySwaggerSpec swagger)
{
endpoints.Map(swagger.Endpoint, async context => {
var client = new HttpClient();
var stream = await client.GetStreamAsync(swagger.Spec);
var document = new OpenApiStreamReader().Read(stream, out var diagnostic);
var rewrite = new OpenApiPaths();
// map existing path
var routes = config.Where(p => p.ClusterId == cluster.ClusterId);
var hasCatchAll = routes != null && routes.Any(p => p.Match.Path.Contains("**catch-all")) ;
foreach (var path in document.Paths) {
var rewritedPath = path.Key.Replace(swagger.TargetPath, swagger.OriginPath);
// there is a catch all, all route are elligible
// or there is a route that match without any operation methods filtering
if (hasCatchAll || routes.Any(p => p.Match.Path.Equals(rewritedPath) && p.Match.Methods == null ))
{
rewrite.Add(rewritedPath, path.Value);
}
else
{
// there is a route that match
var routeThatMatchPath = routes.Any( p => p.Match.Path.Equals(rewritedPath) );
if(routeThatMatchPath)
{
// filter operation based on yarp config
var operationToRemoves = new List<OperationType>();
foreach (var operation in path.Value.Operations)
{
// match route and method
var hasRoute = routes.Any(
p => p.Match.Path.Equals(rewritedPath) && p.Match.Methods.Contains(operation.Key.ToString().ToUpperInvariant())
);
if (!hasRoute)
{
operationToRemoves.Add(operation.Key);
}
}
// remove operation routes
foreach (var operationToRemove in operationToRemoves)
{
path.Value.Operations.Remove(operationToRemove);
}
// add path if there is any operation
if (path.Value.Operations.Any())
{
rewrite.Add(rewritedPath, path.Value);
}
}
}
}
document.Paths = rewrite;
var result = document.Serialize(OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json);
await context.Response.WriteAsync(
result
);
});
Btw do we have a way to retrieve the yarp object configuration directly instead of reading the config files manually?
Thank you. I will try to write the code above with ProxyState function. I will share when it will be done
Many thx, sorceb
Is it possible to get the list of RouteModel (from IProxyStateLookup.GetRoutes()) or RouteConfig objects with all the transformations already applied? Or do I need to read all of the transforms from the config/proxy state as well and apply them manually?
I'm trying to configure swagger for YARP gateway using code similiar to the above and when I'm filtering the OpenApiPaths I would like to be able to compare them with transformed routes. What would be the best approach to do this?
No, transforms aren't applied to routes, they're applied to outgoing requests on a per-request basis.
Why would you include transformed routes in the public swagger?
Why would you include transformed routes in the public swagger?
I don't want to include them in swagger, I just need them to compare with paths that are generated in swagger by default.
I think I can help a bit with this part. But I'm not sure if this swagger addition should be put in this repository, or a separate one There are quite a few constraints, depending on the integration of spec files. In particular the whole cluster part, but also the "catch-all" part on the routes.
@psavoldelli I'd expect a YARP swagger generator to only cover the routes? The clusters should be opaque to the caller.
You can outline a design proposal here, or send a draft PR for discussion.
Here is the main idea (I will push also a PR soon!!)
Find a way with Yarp to expose open API specifications based on underlying services
We use yarp as a gateway for microservices exposition. The frontend automaticaly builds its data access layer directly from the open API specs for obvious reasons.
It is therefore necessary to correctly expose the routes via Yarp, and that they are aligned with what is really made available by the different microservices, as we do not allow manual changes to the data access generation on frontend side.
This implementation is not complete and has its small drawbacks. I assume that there is only one destination per cluster, or at least that each destination respects the same interface contract. There is no hot reload configuration consideration also...
The proxy must provide the routes for the spec files it makes available.
// Configure app services
var builder = WebApplication.CreateBuilder(args);
// Http stack
var app = builder.Build();
app.MapOpenApiSpecs(app.Configuration)
app.MapReverseProxy();
app.Run();
It provide specs based on cluster configuration as all clusters do not provide open api specs
Sample cluster configuration
"Clusters": {
"<clusterName>": {
"ClusterId": "<clusterId>",
"OpenApiSpec": {
"Endpoint": "/swagger/content/1.0/swagger.json",
"Spec": "swagger/1.0/swagger.json"
},
"Destinations": {
"<destinationName>": {
"Address": "http://localhost:5000"
}
}
}
}
/// <summary>
/// open api configuration model
/// </summary>
/// <param name="Endpoint">Endpoint provided to get the specs</param>
/// <param name="Spec">Specification route based on cluster first destination base address</param>
internal record OpenApiSpecOptions(string Endpoint, string Spec);
public static void MapOpenApiSpecs(this IEndpointRouteBuilder endpoints, IConfiguration configuration)
{
var clusters = configuration.GetSection("Gateway:Clusters");
if (clusters != null)
{
foreach (var child in clusters.GetChildren())
{
if (child.GetSection("OpenApiSpec").Exists())
{
// get raw cluster config only to get id
var cluster = child.Get<ClusterConfig>();
var config = child.GetSection("OpenApiSpec").Get<OpenApiSpecOptions>();
endpoints.MapOpenApiSpecs(config, cluster.ClusterId);
}
}
}
}
And finally map the endpoint for that cluser.
This is an overview of the road I took. I use Microsoft.OpenApi
to parse the open spec file.
/// <summary>
/// Map open api spec based on cluster config
/// </summary>
public static void MapOpenApiSpec(this IEndpointRouteBuilder endpoints, OpenApiSpecOptions options, string clusterId)
{
endpoints.Map(options.Endpoint, async context =>
{
// get cluster configuration
var configuration = endpoints.ServiceProvider.GetService<IProxyStateLookup>();
configuration.TryGetCluster(clusterId, out var cluster);
// get first destination open api specs
var client = new HttpClient();
var root = cluster.Destinations.First().Value.Model.Config.Address;
var stream = await client.GetStreamAsync($"{root.TrimEnd('/')}/{options.Spec.TrimStart('/')}");
// get open api documents
var document = new OpenApiStreamReader().Read(stream, out var diagnostic);
// get an api document rewritten and filtered
// according to the route configuration that matches the cluster
var specifications = FilterOpenApiDocument(configuration, cluster, document);
// write the new specifications as a response
var result = specifications.Serialize(OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json);
await context.Response.WriteAsync(
result
);
});
}
The main idea...
private static OpenApiDocument FilterOpenApiDocument(IProxyStateLookup configuration, ClusterState cluster, OpenApiDocument document)
{
// create new paths
var rewrite = new OpenApiPaths();
// map existing path
var routes = configuration.GetRoutes().Where(p => p.Cluster.ClusterId == cluster.ClusterId);
var hasCatchAll = routes != null && routes.Any(p => p.Config.Match.Path.Contains("**catch-all"));
// filter routes and apply transformation.
foreach (var path in document.Paths)
{
// the main work :) ...
}
// apply specific tagging for each route (optional!)
foreach (var path in rewrite)
{
foreach (var operation in path.Value.Operations)
{
operation.Value.Tags = new List<OpenApiTag> {
new OpenApiTag() { Name = cluster.ClusterId }
};
}
}
// return new document
return new OpenApiDocument(document) {
Paths = rewrite
};
}
Here is the main idea (I will push also a PR soon!!)
Any progress on this ?
I have created a NuGet package to fix this issue: https://github.com/ctyar/Swashbuckle.Yarp
Hi there π
I have created a NuGet package too, but with generation of Swagger files by clusters. Repo is in link.
Welcome contributors and issuers π
@andreytreyt , thank you for the nuget it was very much needed. I tried that, and seems all working but it exposes all the endpoints. Do you have something similar to Ocelet to expose only methods that are mentioned in the routing?
For example, the downstream API has 10 endpoints and we want to expose only 2 out of them and the gateway has mapping of only 2 endpoints.
@andreytreyt , thank you for the nuget it was very much needed. I tried that, and seems all working but it exposes all the endpoints. Do you have something similar to Ocelet to expose only methods that are mentioned in the routing?
For example, the downstream API has 10 endpoints and we want to expose only 2 out of them and the gateway has mapping of only 2 endpoints.
Welcome! It doesn't have a similar function. But if you create an issue in the repository of my package I'll resolve it.
Hi there many thanks to @andreytreyt! π Waiting for when this will be included in the main library wondering how YARP can be used without swagger UI, it helps a lot when you have different endpoints.
I'm eagerly awaiting a resolution to this issue as well. Integrating Swagger with YARP would be a significant enhancement for my projects. Hope to see this feature soon!
Also a thanks to @andreytreyt! Using that lib and it works great. Adapted a middleware for our reverse-proxy from a blog post so its protected by oAuth, gist and links referencing the post are here: https://gist.github.com/dbeattie71/390da65f45b22c9b284555b96ddc055c
Hello,
andreytreyt, just curious why not use OpenApiFilterService.CreateFilteredDocument to filter the openApiSpec. We combined it with TemplateMatcher to filter the operations from the route template in the Proxy Config.
psavoldelli any progress on the PR? We are doing something similar, but it would be great if there is an 'official' solution for the problem.
@agerchev hi there! It's better to see an example of your implementation π
Hello,
These are the fragments we use last two years in our projects. They are working for us while waiting for something more 'official' from the YARP team.
Sample configuration:
"MetaData": {
"backendOpenApiUrl": "/openapi.json",
"PathPrefix": "/api/sse"
}
We register the swagger endpoints for the clusters that have open api spec configured:
var proxyConfigProvider = endpoints.ServiceProvider.GetRequiredService<IProxyConfigProvider>();
var proxyConfig = proxyConfigProvider.GetConfig();
var clusters = proxyConfig.Clusters.Where(cc =>
{
if (cc == null || cc.Metadata == null) return false;
if (!cc.Metadata.TryGetValue("backendOpenApiUrl", out string clusterOpenApiDocumentUrl)) return false;
return !string.IsNullOrEmpty(clusterOpenApiDocumentUrl);
});
foreach (var cluster in clusters)
{
c.SwaggerEndpoint($"{cluster.ClusterId}/swagger.json", $"{ cluster.ClusterId}");
}
Additionally we register a swaggerProvider:
public class YarpSwaggerProvider : ISwaggerProvider, IAsyncSwaggerProvider
{
private static readonly string cachePrefix = Guid.NewGuid().ToString();
private SwaggerGenerator _generator;
private IProxyConfig _proxyConfig;
private IMemoryCache _memoryCache;
#region Types
internal class OperationSearchFixer : OperationSearch
{
private int _mode;
private class FixerExtension : IOpenApiExtension
{
public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
{
throw new NotImplementedException();
}
}
public OperationSearchFixer(int mode) : base((url, operationType, operation) => true)
{
_mode = mode;
}
public override void Visit(IList<OpenApiParameter> parameters)
{
if (_mode == 0)
{
/* Π² Π±Π°Π·ΠΎΠ²Π°ΡΠ° ΠΈΠΌΠΏΠ»Π΅ΠΌΠ΅Π½ΡΠ°ΡΠΈΡ, ΡΠ΅ ΡΠ΅ΡΠ²Π° parameter.Explode.
* https://github.com/microsoft/OpenAPI.NET/blob/0ec11c156cfd2169f7c0ccdf9720240ba97816dd/src/Microsoft.OpenApi/Services/OperationSearch.cs#L62
*
*/
foreach (var parameter in parameters.Where(x => x.Style == ParameterStyle.Form))
{
if (parameter.Explode)
parameter.AddExtension("x-set-explode-true", new FixerExtension());
}
base.Visit(parameters);
}
else if (_mode == 1)
{
base.Visit(parameters);
/* Π² Π±Π°Π·ΠΎΠ²Π°ΡΠ° ΠΈΠΌΠΏΠ»Π΅ΠΌΠ΅Π½ΡΠ°ΡΠΈΡ, ΡΠ΅ ΡΠ΅ΡΠ²Π° parameter.Explode.
* https://github.com/microsoft/OpenAPI.NET/blob/0ec11c156cfd2169f7c0ccdf9720240ba97816dd/src/Microsoft.OpenApi/Services/OperationSearch.cs#L62
*
*/
foreach (var parameter in parameters.Where(x => x.Style == ParameterStyle.Form))
{
if (parameter.Extensions.ContainsKey("x-set-explode-true"))
{
parameter.Explode = true;
parameter.Extensions.Remove("x-set-explode-true");
}
}
}
else
base.Visit(parameters);
}
}
#endregion
#region Constructors
public YarpSwaggerProvider(SwaggerGenerator generator, IProxyConfigProvider proxyConfigProvider, IMemoryCache memoryCache)
{
_generator = generator;
_proxyConfig = proxyConfigProvider.GetConfig();
_memoryCache = memoryCache;
}
#endregion
public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
{
return GetSwaggerAsync(documentName, host, basePath).Result;
}
public Task<OpenApiDocument> GetSwaggerAsync(string documentName, string host = null, string basePath = null)
{
return _memoryCache.GetOrCreate(cachePrefix + documentName, async entry =>
{
OpenApiDocument document = null;
//ΠΏΡΠΎΠ±Π²Π°ΠΌΠ΅ Π΄Π° Π³Π΅Π½Π΅ΡΠΈΡΠ°ΠΌΠ΅ Π΄ΠΎΠΊΡΠΌΠ΅Π½Ρ ΠΎΡ ΠΊΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΡΡΠ° Π½Π° yarp. ΠΠΊΠΎ Π½ΡΠΌΠ° ΡΡΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅, ΡΠ΅ ΠΈΠ·ΠΏΠΎΠ»Π·Π²Π° SwaggerGenerator - Π°
document = await CreateDocumentFromProxyConfig(documentName, host, basePath);
if (document != null) return document;
document = await _generator.GetSwaggerAsync(documentName, host, basePath);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(3);
return document;
});
}
private async Task<OpenApiDocument> CreateDocumentFromProxyConfig(string documentName, string host = null, string basePath = null)
{
ClusterConfig cConfig = _proxyConfig.Clusters.SingleOrDefault((cluster) => { return string.Compare(cluster.ClusterId, documentName, true) == 0; });
if (cConfig == null || cConfig.Metadata == null) return null;
if (!cConfig.Metadata.TryGetValue("backendOpenApiUrl", out string clusterOpenApiDocumentUrl)) return null;
var routesToProcess = _proxyConfig.Routes.Where((routeConfig) => { return string.Compare(routeConfig.ClusterId, cConfig.ClusterId, true) == 0; }).ToList();
if (routesToProcess.Count == 0) return null;
OpenApiDocument openApiDocument;
#region Read serviceOpenApiDocument
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(cConfig.Destinations["default"].Address);
OpenApiStreamReader openApiStreamReader = new OpenApiStreamReader();
openApiDocument = openApiStreamReader.Read(await client.GetStreamAsync(clusterOpenApiDocumentUrl), out OpenApiDiagnostic diagnostic);
}
#endregion
new OpenApiWalker(new OperationSearchFixer(0)).Walk(openApiDocument);
var openApiResultDocument = OpenApiFilterService.CreateFilteredDocument(openApiDocument, (url, operationType, operation) =>
{
foreach (var route in routesToProcess)
{
if (route.Match.Methods != null &&
route.Match.Methods.Count > 0)
{
if (!operationType.HasValue) continue;
else if (!route.Match.Methods.Contains(Enum.GetName(operationType.Value), StringComparer.OrdinalIgnoreCase)) continue;
}
string template = route.Match.Path;
if (cConfig.Metadata.TryGetValue("PathPrefix", out string pathPrefix))
template = template.Replace(pathPrefix, "");
TemplateMatcher matcher = new TemplateMatcher(TemplateParser.Parse(template), new RouteValueDictionary());
var routeValues = new RouteValueDictionary();
if (matcher.TryMatch(url, routeValues))
return true;
}
return false;
});
new OpenApiWalker(new OperationSearchFixer(1)).Walk(openApiDocument);
openApiResultDocument.Components.SecuritySchemes.Clear();
openApiResultDocument.SecurityRequirements.Clear();
openApiResultDocument.Info.Title = documentName;
openApiResultDocument.Servers.Add(new OpenApiServer()
{
Url = cConfig.Metadata["PathPrefix"]
});
return openApiResultDocument;
}
}
@agerchev can you please make a sample of it? I cant get it working?
@andreytreyt seems not able to use JwtBearer
@moedeveloper hello! Which tool? If you found any error you can create an issue in yarp swagger repo.
Hello, @moedeveloper these days i'm out of time, and right now, i cannot create a full repo, sorry for that. Where do you have trouble ?
PS: You have to register the Swagger provider like this.
services.AddTransient<ISwaggerProvider, YarpSwaggerProvider>(); //use SwaggerGenerator in YarpSwaggerProvider implementation services.AddTransient<SwaggerGenerator, SwaggerGenerator>();
@andreytreyt thanks very much of your library its very helpful is it possible to also the yarp itself controllers in the swagger (finally to see also the proxy swagger and the yarp controllers)
Can you provide example using Swagger with proxy?