================================================
FILE: samples/ReverseProxy.Auth.Sample/Views/Account/LoggedOut.cshtml
================================================
@{
ViewData["Title"] = "Logged Out";
}
You have been logged out.
================================================
FILE: samples/ReverseProxy.Auth.Sample/Views/Account/Login.cshtml
================================================
@{
ViewData["Title"] = "Login";
}
Login
================================================
FILE: samples/ReverseProxy.Auth.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Auth.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"https": {
"Url": "https://localhost:5001"
},
"http": {
"Url": "http://localhost:5000"
}
}
},
"ReverseProxy": {
"Clusters": {
"cluster1": {
"Destinations": {
"cluster1/destination1": {
"Address": "https://example.com/"
}
}
}
},
"Routes": {
"DefaultAuthRoute": {
"ClusterId": "cluster1",
// This route uses the built-in default authorization policy which is to require authenticated users
"AuthorizationPolicy": "Default",
"Match": {
"Path": "/default"
}
},
"ClaimsAuthRoute": {
"ClusterId": "cluster1",
// This route requires the "myPolicy" authorization policy which is defined in Startup.cs
"AuthorizationPolicy": "myPolicy",
"Match": {
"Path": "/custom/{*any}"
}
},
"AnonymousRoute": {
"ClusterId": "cluster1",
// This route uses the explicit name Anonymous to not require authentication
"AuthorizationPolicy": "Anonymous",
"Match": {
"Path": "/open/{*any}"
}
},
"Other": {
// As the following route does not define an authorization policy, it uses the fallback policy
// which is set in Startup.cs to be null, and so not require authentication or claims.
"ClusterId": "cluster1",
"Match": {
"Path": "{**catchall}"
}
}
}
}
}
================================================
FILE: samples/ReverseProxy.Code.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Configuration;
using Microsoft.AspNetCore.Http;
using Yarp.ReverseProxy.Model;
const string DEBUG_HEADER = "Debug";
const string DEBUG_METADATA_KEY = "debug";
const string DEBUG_VALUE = "true";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddReverseProxy()
.LoadFromMemory(GetRoutes(), GetClusters());
var app = builder.Build();
app.Map("/update", context =>
{
context.RequestServices.GetRequiredService().Update(GetRoutes(), GetClusters());
return Task.CompletedTask;
});
// We can customize the proxy pipeline and add/remove/replace steps
app.MapReverseProxy(proxyPipeline =>
{
// Use a custom proxy middleware, defined below
proxyPipeline.Use(MyCustomProxyStep);
// Don't forget to include these two middleware when you make a custom proxy pipeline (if you need them).
proxyPipeline.UseSessionAffinity();
proxyPipeline.UseLoadBalancing();
});
app.Run();
RouteConfig[] GetRoutes()
{
return
[
new RouteConfig()
{
RouteId = "route" + Random.Shared.Next(), // Forces a new route id each time GetRoutes is called.
ClusterId = "cluster1",
Match = new RouteMatch
{
// Path or Hosts are required for each route. This catch-all pattern matches all request paths.
Path = "{**catch-all}"
}
}
];
}
ClusterConfig[] GetClusters()
{
var debugMetadata = new Dictionary
{
{ DEBUG_METADATA_KEY, DEBUG_VALUE }
};
return
[
new ClusterConfig()
{
ClusterId = "cluster1",
SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" },
Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "destination1", new DestinationConfig() { Address = "https://example.com" } },
{ "debugdestination1", new DestinationConfig() {
Address = "https://bing.com",
Metadata = debugMetadata }
},
}
}
];
}
///
/// Custom proxy step that filters destinations based on a header in the inbound request
/// Looks at each destination metadata, and filters in/out based on their debug flag and the inbound header
///
Task MyCustomProxyStep(HttpContext context, Func next)
{
// Can read data from the request via the context
var useDebugDestinations = context.Request.Headers.TryGetValue(DEBUG_HEADER, out var headerValues) && headerValues.Count == 1 && headerValues[0] == DEBUG_VALUE;
// The context also stores a ReverseProxyFeature which holds proxy specific data such as the cluster, route and destinations
var availableDestinationsFeature = context.Features.Get();
var filteredDestinations = new List();
// Filter destinations based on criteria
foreach (var d in availableDestinationsFeature.AvailableDestinations)
{
//Todo: Replace with a lookup of metadata - but not currently exposed correctly here
if (d.DestinationId.Contains("debug") == useDebugDestinations) { filteredDestinations.Add(d); }
}
availableDestinationsFeature.AvailableDestinations = filteredDestinations;
// Important - required to move to the next step in the proxy pipeline
return next();
}
================================================
FILE: samples/ReverseProxy.Code.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.Code.Sample": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.Code.Sample/README.md
================================================
# YARP Code Extensibility Sample
This sample shows two common customizations via code of the YARP reverse proxy:
- ## Dynamic configuration from code
YARP supports pulling configuration from a config file, but in many scenarios the configuration of the routes to use, and which destinations the requests should be sent to need to be programmatically fetched from another source. The extensibility of YARP makes it easy for you to fetch that data from where you need to, and then pass to the proxy as lists of objects.
This sample shows the routes and destinations being created in code, and then passed to an in-memory provider. The role of the in-memory provider is to give change notifications to YARP for when the config has been changed and needs to be updated. YARP uses a snapshot model for its configuration, so that changes are applied as an atomic action, that will apply to subsequent requests after the change is applied. Existing requests that are already being processed will be completed using the configuration snapshot from the time that they were received.
The ```IProxyConfig``` interface implemented in InMemoryConfigProvider includes a change token which is used to signal when a batch of changes to the configuration is complete, and the proxy should take a snapshot and update its internal configuration. Part of the snapshot processing is to create an optimized route table in ASP.NET, which can be a CPU intensive operation, for that reason we don't recommend signaling for updates more than once per 15 seconds.
- ## Custom pipeline step
YARP uses a pipeline model for the stages involved in processing each request:
- Mapping the request path to a route and cluster of destinations
- Pre-assigning servers based on existing session affinity headers in the request
- Filtering the destination list for servers that are not healthy
- Load balancing between the remaining servers based on load etc
- Storing session affinity if applicable
- Transforming headers if required
- Proxying the request/response to/from the destination server
You can insert additional custom stages into the pipeline, or replace built-in steps with your own implementations.
This sample adds an additional stage that will filter the destinations from a cluster based on a "debug" metadata attribute being included in the config data based. If a custom header "Debug:true" is present in the request, then destinations with the debug metadata will be retained, and others will be filtered out, or vice-versa.
## Key Files
The following files are key to implementing the features described above:
- ### [Program.cs](Program.cs)
Provides the initialization routines for ASP.NET and the reverse proxy. It:
- sets up the proxy passing in the InMemoryConfigProvider instance. The sample routes and clusters definitions are created as part of this initialization. The config provider instance is used for the lifetime of the proxy.
- sets up the request pipeline. As an additional step is added, the proxy pipeline is configured here.
- ```MyCustomProxyStep``` is the implementation of the additional step. It finds the proxy functionality via features added to the HttpContext, and then filters the destinations based on the presence of a "Debug" header in the request.
================================================
FILE: samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.Code.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Code.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
================================================
FILE: samples/ReverseProxy.Config.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.UseCors();
app.MapReverseProxy();
app.Run();
================================================
FILE: samples/ReverseProxy.Config.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.Config.Sample": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.Config.Sample/README.md
================================================
# Configuration Sample
This sample shows off the properties that can be supplied to YARP via configuration. In this case its using appsettings.json, but the same configuration properties could be supplied through code instead. See [ReverseProxy.Code.Sample](../ReverseProxy.Code.Sample) for an example
The [configuration file](appsettings.json) includes all the settings that are currently supported by YARP.
The configuration shows two routes and two clusters:
- minimalRoute which will map to any URL
- Routes to cluster "minimalCluster" which has one destination "www.example.com"
- allRouteProps route
- Which includes further restrictions:
- Path must be /download/*
- Host must be localhost, www.aaaaa.com or www.bbbbb.com
- Http Method must be GET or POST
- Must have a header "MyCustomHeader" with a value of "value1", "value2" or "another value"
- A "MyHeader" header will be added with the value "MyValue"
- Must have a query parameter "MyQueryParameter" with a value of "value1", "value2" or "another value"
- This will route to cluster "allClusterProps" which has 2 destinations - https://dotnet.microsoft.com and https://10.20.30.40
- Requests will be [load balanced](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/load-balancing) between destinations using a "PowerOfTwoChoices" algorithm, which picks two destinations at random, then uses the least loaded of the two.
- It includes [session affinity](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity) using a cookie which will ensure subsequent requests from the same client go to the same host.
- It is configured to have both active and passive [health checks](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/dests-health-checks) - note the second destination will timeout for active checks (unless you have a host with that IP on your network)
- It includes [HttpClient configuration](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config) setting outbound connection properties
- HttpRequest properties defaulting to HTTP/2 with a 2min timeout
The other files in the sample are the same as the getting started instructions.
To make a request that would be successful against the second route, you will need a client request similar to:
```bash
curl -v -k -X GET -H "MyCustomHeader: value1" https://localhost:5001/download?MyQueryParameter=value1
```
================================================
FILE: samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.Config.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Config.Sample/appsettings.json
================================================
{
// Base URLs the server listens on, must be configured independently of the routes below.
// Can also be configured via Kestrel/Endpoints, see https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints
"Urls": "http://localhost:5000;https://localhost:5001",
//Sets the Logging level for ASP.NET
"Logging": {
"LogLevel": {
"Default": "Information",
// Uncomment to hide diagnostic messages from runtime and proxy
// "Microsoft": "Warning",
// "Yarp" : "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
// Configuration for YARP
"ReverseProxy": {
// Routes tell the proxy which requests to forward
"Routes": {
"minimalRoute": {
// Matches anything and routes it to www.example.com
"ClusterId": "minimalCluster",
"Match": {
"Path": "{**catch-all}"
}
},
"allRouteProps": {
// matches /download/* and routes to "allClusterProps"
"ClusterId": "allClusterProps", // Name of one of the clusters
"Order": 0, // Lower numbers have higher precedence, default is 0
"AuthorizationPolicy": "Anonymous", // Name of the policy or "Default", "Anonymous"
"CorsPolicy": "disable", // Name of the CorsPolicy to apply to this route or "default", "disable"
"Match": { // Rules that have to be met for the route to match the request
"Path": "/download/{**remainder}", // The path to match using ASP.NET syntax.
"Hosts": [ "localhost", "www.aaaaa.com", "www.bbbbb.com" ], // The host names to match, unspecified is any
"Methods": [ "GET", "PUT" ], // The HTTP methods that match, unspecified is all
"Headers": [ // The headers to match, unspecified is any
{
"Name": "MyCustomHeader", // Name of the header
"Values": [ "value1", "value2", "another value" ], // Matches are against any of these values
"Mode": "ExactHeader", // or "HeaderPrefix", "Exists" , "Contains", "NotContains"
"IsCaseSensitive": true
}
],
"QueryParameters": [ // The query parameters to match, unspecified is any
{
"Name": "MyQueryParameter", // Name of the query parameter
"Values": [ "value1", "value2", "another value" ], // Matches are against any of these values
"Mode": "Exact", // or "Prefix", "Exists" , "Contains", "NotContains"
"IsCaseSensitive": true
}
]
},
"Metadata": { // List of key value pairs that can be used by custom extensions
"MyName": "MyValue"
},
"Transforms": [ // List of transforms. See ReverseProxy.Transforms.Sample for more details
{
"RequestHeader": "MyHeader",
"Set": "MyValue"
}
]
}
},
// Clusters tell the proxy where and how to forward requests
"Clusters": { // Cluster with the minimum information
"minimalCluster": {
"Destinations": { // Specifies which back end servers requests should be routed to.
"example.com": { // name is used for logging and via extensibility
"Address": "http://www.example.com" // Should specify Protocol, Address/IP & Port, but not path
}
}
},
"allClusterProps": { // Cluster with all properties
"Destinations": { // Specifies which back end servers requests should be routed to.
"first_destination": { // name is used for logging and via extensibility
"Address": "https://dotnet.microsoft.com" // Should specify Protocol, Address/IP & Port, but not path
},
"another_destination": {
"Address": "https://10.20.30.40",
"Health": "https://10.20.30.40:12345", // override for active health checks
"Host": "contoso",
"Metadata": {
"SomeKey": "SomeValue"
}
}
},
"LoadBalancingPolicy": "PowerOfTwoChoices", // Alternatively "First", "Random", "RoundRobin", "LeastRequests"
"SessionAffinity": { // Ensures subsequent requests from a client go to the same destination server
"Enabled": true, // Defaults to 'false'
"Policy": "HashCookie", // Default, alternatively "Cookie" or "CustomHeader"
"FailurePolicy": "Redistribute", // default, alternatively "Return503Error"
"AffinityKeyName": "MySessionCookieName", // Required, no default
"Cookie": { // Options for cookie based session affinity
"Path": "/",
"SameSite": "None",
"HttpOnly": true,
"Expiration": "00:30:00",
"Domain": "example.com",
"MaxAge": "08:00:00",
"SecurePolicy": "Always",
"IsEssential": true
}
},
"HealthCheck": { // Ways to determine which destinations should be filtered out due to unhealthy state
"Active": { // Makes API calls to validate the health of each destination
"Enabled": true,
"Interval": "00:00:10", // How often to query for health data
"Timeout": "00:00:10", // Timeout for the health check request/response
"Policy": "ConsecutiveFailures", // Or other custom policy that has been registered
"Path": "/favicon.ico", // API endpoint to query for health state. Looks for 2XX response codes to indicate healthy state
// Typically something like "/api/health" but used favicon to enable sample to run
"Query": "?healthCheck=true" // Query string to append to the health check request
},
"Passive": { // Disables destinations based on HTTP response codes for proxy requests
"Enabled": true, // Defaults to false
"Policy": "TransportFailureRate", // Or other custom policy that has been registered
"ReactivationPeriod": "00:00:10" // how long before the destination is re-enabled
},
"AvailableDestinationsPolicy": "HealthyOrPanic" // Policy for which destinations can be used when sending requests
},
"HttpClient": { // Configuration of HttpClient instance used to contact destinations
"SslProtocols": [ "Tls13" ],
"DangerousAcceptAnyServerCertificate": true, // Disables destination cert validation
"MaxConnectionsPerServer": 1024, // Destination server can further limit this number
"EnableMultipleHttp2Connections": true,
"RequestHeaderEncoding": "Latin1", // How to interpret non ASCII characters in proxied request's header values
"ResponseHeaderEncoding": "Latin1", // How to interpret non ASCII characters in proxied request's response header values
"WebProxy": { // Optional proxy configuration for outgoing requests
"Address": "http://127.0.0.1",
"BypassOnLocal": true,
"UseDefaultCredentials": false
}
},
"HttpRequest": { // Options for sending request to destination
"ActivityTimeout": "00:02:00", // Activity timeout for the request
"Version": "2", // Http Version that should be tried first
"VersionPolicy": "RequestVersionOrLower", // Policy for which other versions can be be used
"AllowResponseBuffering": false
},
"Metadata": { // Custom Key/value pairs for extensibility
"TransportFailureRateHealthPolicy.RateLimit": "0.5", // Used by Passive health policy
"MyKey": "MyValue"
}
}
}
}
}
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Configuration;
namespace Yarp.Sample
{
public class CustomConfigFilter : IProxyConfigFilter
{
// Matches {{env_var_name}}
private readonly Regex _exp = new("\\{\\{(\\w+)\\}\\}");
// Configuration filter for clusters, will be passed each cluster in turn, which it should either return as-is or
// clone and create a new version of with updated changes
//
// This sample looks at the destination addresses and any of the form {{key}} will be modified, looking up the key
// as an environment variable. This is useful when hosted in Azure etc, as it enables a simple way to replace
// destination addresses via the management console
public ValueTask ConfigureClusterAsync(ClusterConfig origCluster, CancellationToken cancel)
{
// Each cluster has a dictionary of destinations, which is read-only, so we'll create a new one with our updates
var newDests = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var d in origCluster.Destinations)
{
var origAddress = d.Value.Address;
if (_exp.IsMatch(origAddress))
{
// Get the name of the env variable from the destination and lookup value
var lookup = _exp.Matches(origAddress)[0].Groups[1].Value;
var newAddress = System.Environment.GetEnvironmentVariable(lookup);
if (string.IsNullOrWhiteSpace(newAddress))
{
throw new System.ArgumentException($"Configuration Filter Error: Substitution for '{lookup}' in cluster '{d.Key}' not found as an environment variable.");
}
// using c# 9 "with" to clone and initialize a new record
var modifiedDest = d.Value with { Address = newAddress };
newDests.Add(d.Key, modifiedDest);
}
else
{
newDests.Add(d.Key, d.Value);
}
}
return new ValueTask(origCluster with { Destinations = newDests });
}
public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)
{
// Example: do not let config based routes take priority over code based routes.
// Lower numbers are higher priority. Code routes default to 0.
if (route.Order.HasValue && route.Order.Value < 1)
{
return new ValueTask(route with { Order = 1 });
}
return new ValueTask(route);
}
}
}
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Yarp.Sample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddConfigFilter();
var app = builder.Build();
app.MapReverseProxy();
app.Run();
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.ConfigFilter.Sample": {
"commandName": "Project",
"environmentVariables": {
"Key": "Value",
"contoso": "https://contoso.com",
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/README.md
================================================
# Configuration Filter Sample
This sample shows an example of a configuration filter. A configuration filter enables a callback as part of the configuration load where custom code can modify the configuration values for the proxy as they are loaded. This is valuable when the configuration file provides most of what you need, but you want to be able to tweak some values, but don't want to have to write a custom config provider.
## IProxyConfigFilter
The bulk of the code is the CustomConfigFilter class which implements the IProxyConfigFilter interface. The interface has two methods which act as callbacks when Clusters and Routes are loaded from config. The methods will be called for each Route and Cluster, and as both are defined as Records, they are immutable so the method should return the same object as-is or a replacement.
## CustomConfigFilter Class
### ConfigureClusterAsync
This looks at the value of each destination and sees whether it matches the pattern {{env_var_name}}, and if so it treats it as an indirection to an environment variable, and replaces the destination address with the value of the named variable (if it exists).
**Note:** AppSettings.json includes a destination of {{contoso}} which will be matched. The Properties/launchSettings.json file includes a definition of the environment variable, which will be used by Visual Studio and other tools when debugging with "F5".
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.ConfigFilter.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"https": {
"Url": "https://localhost:5001"
},
"http": {
"Url": "http://localhost:5000"
}
}
},
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Methods": [ "GET", "POST" ],
"Hosts": [ "localhost" ],
"Path": "/api/{**catch-all}"
}
},
"route2": {
"ClusterId": "cluster2",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"cluster1/destination1": {
// Following value will be found by regex and looked up as an environment variable
"Address": "{{contoso}}"
},
"cluster1/destination2": {
"Address": "https://bing.com/"
}
}
},
"cluster2": {
"Destinations": {
"cluster2/destination1": {
"Address": "https://example.com/"
}
}
}
}
}
}
================================================
FILE: samples/ReverseProxy.Direct.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Forwarder;
using Yarp.ReverseProxy.Transforms;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder();
var app = builder.Build();
// Configure our own HttpMessageInvoker for outbound calls for proxy operations
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false,
EnableMultipleHttp2Connections = true,
ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
ConnectTimeout = TimeSpan.FromSeconds(15),
});
// Setup our own request transform class
var transformer = new CustomTransformer(); // or HttpTransformer.Default;
var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) };
app.UseRouting();
// When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc..
// For an alternate example that includes those features see BasicYarpSample.
app.Map("/test/{**catch-all}", async (HttpContext httpContext, IHttpForwarder forwarder) =>
{
var error = await forwarder.SendAsync(httpContext, "https://example.com", httpClient, requestOptions,
static (context, proxyRequest) =>
{
// Customize the query string:
var queryContext = new QueryTransformContext(context.Request);
queryContext.Collection.Remove("param1");
queryContext.Collection["area"] = "xx2";
// Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default.
proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", context.Request.Path, queryContext.QueryString);
// Suppress the original request header, use the one from the destination Uri.
proxyRequest.Headers.Host = null;
return default;
});
// Check if the proxy operation was successful
if (error != ForwarderError.None)
{
var errorFeature = httpContext.Features.Get();
var exception = errorFeature.Exception;
}
});
app.MapForwarder("/sample/{id}", "https://httpbin.org", "/anything/{id}");
app.MapForwarder("/sample/anything/{id}", "https://httpbin.org", b => b.AddPathRemovePrefix("/sample"));
// When using extension methods for registering IHttpForwarder providing configuration, transforms, and HttpMessageInvoker is optional (defaults will be used).
app.MapForwarder("/{**catch-all}", "https://example.com", requestOptions, transformer, httpClient);
app.Run();
///
/// Custom request transformation
///
internal sealed class CustomTransformer : HttpTransformer
{
///
/// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage
/// fields are initialized except RequestUri, which will be initialized after the
/// callback if no value is provided. The string parameter represents the destination
/// URI prefix that should be used when constructing the RequestUri. The headers
/// are copied by the base implementation, excluding some protocol headers like HTTP/2
/// pseudo headers (":authority").
///
/// The incoming request.
/// The outgoing proxy request.
/// The uri prefix for the selected destination server which can be used to create
/// the RequestUri.
public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)
{
// Copy all request headers
await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);
// Customize the query string:
var queryContext = new QueryTransformContext(httpContext.Request);
queryContext.Collection.Remove("param1");
queryContext.Collection["area"] = "xx2";
// Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default.
proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", httpContext.Request.Path, queryContext.QueryString);
// Suppress the original request header, use the one from the destination Uri.
proxyRequest.Headers.Host = null;
}
}
================================================
FILE: samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.Direct.Sample": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.Direct.Sample/README.md
================================================
# YARP Direct Proxy Example
Some customers who have an existing custom proxy for HTTP/1.1 are looking at YARP for a solution to handle more complex requests, such as HTTP/2, gRPC, WebSockets in future QUIC and HTTP/3. These applications have their own means of routing, load balancing, affinity, etc. and only need to forward a specific request to a specific destination. To make it easier to integrate YARP into these scenarios, the component that proxies requests is exposed via IHttpForwarder which can be called directly, and has few dependencies on the rest of YARP's infrastructure.
This example shows how to use IHttpForwarder to proxy a request to/from a specified destination.
The operation of the proxy can be thought of as:
```text
+-------------------+ +-------------------+ +-------------------+
| Client | ──(a)──► | Proxy | ──(b)──► | Destination |
| | ◄──(d)── | | ◄──(c)── | |
+-------------------+ +-------------------+ +-------------------+
```
(a) and (b) show the *request* path, going from the client to the destination.
(c) and (d) show the *response* path, going from the destination back to the client.
Normal proxying comprises the following steps:
| \# | Step | Direction |
| -- | ---- | --------- |
| 1 | Disable ASP .NET Core limits for streaming requests | |
| 2 | Create outgoing HttpRequestMessage | |
| 3 | Setup copy of request body (background) | Client --► Proxy --► Destination |
| 4 | Copy request headers | Client --► Proxy --► Destination |
| 5 | Send the outgoing request using HttpMessageInvoker | Client --► Proxy --► Destination |
| 6 | Copy response status line | Client ◄-- Proxy ◄-- Destination |
| 7 | Copy response headers | Client ◄-- Proxy ◄-- Destination |
| 8.1 | Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. | |
| 8.1.1 | Upgrade client channel | Client ◄--- Proxy ◄--- Destination |
| 8.1.2 | Copy duplex streams and return | Client ◄--► Proxy ◄--► Destination |
| 8.2 | Copy (normal) response body | Client ◄-- Proxy ◄-- Destination |
| 9 | Copy response trailer headers and finish response | Client ◄-- Proxy ◄-- Destination |
| 10 | Wait for completion of step 2: copying request body | Client --► Proxy --► Destination |
To enable control over mapping request and response fields and headers between the client and destination (steps 4 and 7 above), the HttpForwarder.ProxyAsync method takes a HttpTransformer. Your implementation can modify the request url, method, protocol version, response status code, or decide which headers are copied, modify them, or insert additional headers as required.
**Note:** When using the HttpForwarder class directly there are some [header transforms](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/transforms) included by default, such as adding ```X-Forwarded-For``` and removing the original Host header. This is the same set of transforms included by default in the YARP pipeline model (see BasicYarpSample).
## Files
The key functionality for this sample is all included in [Program.cs](Program.cs).
================================================
FILE: samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.Direct.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Direct.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"https": {
"Url": "https://localhost:5001"
},
"http": {
"Url": "http://localhost:5000"
}
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/README.md
================================================
# Http.sys Delegation Sample
This sample shows how to use YARP to delegate requests to other Http.sys request queues instead of or in addition to proxying requests. Using Http.sys delegation requires hosting YARP on [ASP.NET Core's Http.sys server](https://docs.microsoft.com/aspnet/core/fundamentals/servers/httpsys) and requests can only be delegated to other processes which use Http.sys for request processing (e.g. ASP.NET Core using Http.sys server or IIS).
**Note:** delegation only works for ASP.NET Core 6+ running on new versions of Windows
## Sample Projects
There are two projects as part of this sample. A sample Http.sys server where traffic will be delegated to and a YARP example which both proxies and delegates request depending on the route. Both projects use the minimal API style but this isn't a requirement.
### ReverseProxy Delegation
There are four parts to enable YARP delegation support:
- Use the ASP.NET Core Http.sys server
```c#
builder.WebHost.UseHttpSys();
```
- Add YARP services
```c#
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
```
- Add YARP to the request pipeline.
You need to use the overload that allows you to define the middleware used in the YARP pipeline.
```c#
app.MapReverseProxy(proxyPipeline =>
{
// Add the three middleware YARP adds by default plus the Http.sys delegation middleware
proxyPipeline.UseSessionAffinity(); // Has no affect on delegation destinations because the response doesn't go through YARP
proxyPipeline.UseLoadBalancing();
proxyPipeline.UsePassiveHealthChecks();
proxyPipeline.UseHttpSysDelegation();
});
```
- Add a ReverseProxy section to appsettings.json.
Configuration is almost identical to how YARP in typically configured. The only difference is, for destinations which should use delegation, they have metadata which indicates the Http.sys queue name to delegate the request to.
```json
"Destinations": {
"SampleHttpSysServer": {
"Address": "http://localhost:5600/",
"Metadata": {
"HttpSysDelegationQueue": "SampleHttpSysServerQueue"
}
}
}
```
## Usage
To run the sample:
1. Start the SampleHttpSysServer project `dotnet run --project SampleHttpSysServer\SampleHttpSysServer.csproj`
2. Start the ReverseProxy.HttpSysDelegation.Sample project `dotnet run --project ReverseProxy\ReverseProxy.HttpSysDelegation.Sample.csproj`
By default, the SampleHttpSysServer will listen to http://localhost:5600 and the ReverseProxy will listen to http://localhost:5500. The ReverseProxy will delegate any requests under the path http://localhost:5500/delegate to the SampleHttpSysServer. Any other path will be proxied to https://httpbin.org/.
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
Debug.Assert(OperatingSystem.IsWindows());
builder.WebHost.UseHttpSys();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.UseSessionAffinity(); // Has no effect on delegation destinations because the response doesn't go through YARP
proxyPipeline.UseLoadBalancing();
proxyPipeline.UsePassiveHealthChecks();
proxyPipeline.UseHttpSysDelegation();
});
app.Run();
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Properties/launchSettings.json
================================================
{
"profiles": {
"ReverseProxy.HttpSysDelegation": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5500",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/ReverseProxy.HttpSysDelegation.Sample.csproj
================================================
$(ReleaseTFMs)enableenable
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"delegateroute": {
"ClusterId": "delegatecluster",
"Match": {
"Path": "/delegate/{**catch-all}"
}
},
"proxyroute": {
"ClusterId": "proxycluster",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"delegatecluster": {
"Destinations": {
"SampleHttpSysServer": {
"Address": "http://localhost:5600/",
"Metadata": {
"HttpSysDelegationQueue": "SampleHttpSysServerQueue"
}
}
}
},
"proxycluster": {
"Destinations": {
"httpbin.org": {
"Address": "https://httpbin.org/"
}
}
}
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);
Debug.Assert(OperatingSystem.IsWindows());
builder.WebHost.UseHttpSys(options =>
{
options.RequestQueueName = "SampleHttpSysServerQueue";
options.RequestQueueMode = RequestQueueMode.Create;
});
var app = builder.Build();
app.Run(async context =>
{
await context.Response.WriteAsync($"Hello World! (PID: {Environment.ProcessId})");
});
app.Run();
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Properties/launchSettings.json
================================================
{
"profiles": {
"SampleHttpSysServer": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5600",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/SampleHttpSysServer.csproj
================================================
$(ReleaseTFMs)enableenable
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
================================================
FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
================================================
FILE: samples/ReverseProxy.LetsEncrypt.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLettuceEncrypt();
builder.Services.AddControllers();
// Add the reverse proxy capability to the server
builder.Services.AddReverseProxy()
// Initialize the reverse proxy from the "ReverseProxy" section of configuration
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// Register the reverse proxy routes
app.MapReverseProxy();
app.Run();
================================================
FILE: samples/ReverseProxy.LetsEncrypt.Sample/Properties/launchSettings.json
================================================
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"ReverseProxy.LetsEncrypt.Sample": {
"commandName": "Project",
"launchBrowser": false,
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.LetsEncrypt.Sample/README.md
================================================
# Lets Encrypt Sample
[Lets Encrypt](https://letsencrypt.org/) is a certificate authority (CA) that provides HTTPS (SSL/TLS) certificates for free. This sample shows how to add Lets Encrypt for TLS termination in YARP by integrating with [LettuceEncrypt](https://github.com/natemcmaster/LettuceEncrypt). It allows to set up TLS between the client and YARP with minimal configuration.
The sample includes the following parts:
- **[Program.cs](Program.cs)**
It calls `IServiceCollection.AddLettuceEncrypt` in the `ConfigureServices` method.
- **[appsettings.json](appsettings.json)**
Sets up the required options for LettuceEncrypt including:
- "DomainNames" - at least one domain name is required
- "EmailAddress" - email address must be specified to register with the certificate authority
================================================
FILE: samples/ReverseProxy.LetsEncrypt.Sample/ReverseProxy.LetsEncrypt.Sample.csproj
================================================
$(ReleaseTFMs)latest
================================================
FILE: samples/ReverseProxy.LetsEncrypt.Sample/appsettings.json
================================================
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:80"
},
"Https": {
"Url": "https://*:443"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"destination1": {
"Address": "https://example.com/"
}
}
}
}
},
"LettuceEncrypt": {
// Set this to automatically accept the terms of service of your certificate authority.
// If you don't set this in config, you will need to press "y" whenever the application starts
"AcceptTermsOfService": true,
// You must specify at least one domain name
"DomainNames": [ "example.com" ],
// You must specify an email address to register with the certificate authority
"EmailAddress": "it-admin@example.com"
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/ForwarderMetricsConsumer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Yarp.Telemetry.Consumption;
namespace Yarp.Sample
{
public sealed class ForwarderMetricsConsumer : IMetricsConsumer
{
public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current)
{
var elapsed = current.Timestamp - previous.Timestamp;
var newRequests = current.RequestsStarted - previous.RequestsStarted;
Console.Title = $"Forwarded {current.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)";
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Yarp.ReverseProxy.Forwarder;
using Yarp.Telemetry.Consumption;
namespace Yarp.Sample
{
public sealed class ForwarderTelemetryConsumer : IForwarderTelemetryConsumer
{
public void OnForwarderStart(DateTime timestamp, string destinationPrefix)
{
var metrics = PerRequestMetrics.Current;
metrics.ProxyStartOffset = metrics.CalcOffset(timestamp);
}
public void OnForwarderStop(DateTime timestamp, int statusCode)
{
var metrics = PerRequestMetrics.Current;
metrics.ProxyStopOffset = metrics.CalcOffset(timestamp);
}
public void OnForwarderFailed(DateTime timestamp, ForwarderError error)
{
var metrics = PerRequestMetrics.Current;
metrics.Error = error;
}
public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime)
{
var metrics = PerRequestMetrics.Current;
if (isRequest)
{
metrics.RequestBodyLength = contentLength;
metrics.RequestContentIops = iops;
}
else
{
// We don't get a content stop from http as it is returning a stream that is up to the consumer to
// read, but we know its ended here.
metrics.HttpResponseContentStopOffset = metrics.CalcOffset(timestamp);
metrics.ResponseBodyLength = contentLength;
metrics.ResponseContentIops = iops;
}
}
public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId)
{
var metrics = PerRequestMetrics.Current;
metrics.RouteInvokeOffset = metrics.CalcOffset(timestamp);
metrics.RouteId = routeId;
metrics.ClusterId = clusterId;
metrics.DestinationId = destinationId;
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Net.Http;
using Yarp.Telemetry.Consumption;
namespace Yarp.Sample
{
public sealed class HttpClientTelemetryConsumer : IHttpTelemetryConsumer
{
public void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestStartOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestStop(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestStopOffset = metrics.CalcOffset(timestamp);
}
public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpConnectionEstablishedOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestLeftQueueOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestHeadersStart(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestHeadersStartOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestHeadersStop(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestHeadersStopOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestContentStart(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestContentStartOffset = metrics.CalcOffset(timestamp);
}
public void OnRequestContentStop(DateTime timestamp, long contentLength)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpRequestContentStopOffset = metrics.CalcOffset(timestamp);
}
public void OnResponseHeadersStart(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpResponseHeadersStartOffset = metrics.CalcOffset(timestamp);
}
public void OnResponseHeadersStop(DateTime timestamp)
{
var metrics = PerRequestMetrics.Current;
metrics.HttpResponseHeadersStopOffset = metrics.CalcOffset(timestamp);
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Threading;
using Yarp.ReverseProxy.Forwarder;
using System.Text.Json;
namespace Yarp.Sample
{
public class PerRequestMetrics
{
private static readonly AsyncLocal _local = new AsyncLocal();
private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true };
// Ensure we are only fetched via the factory
private PerRequestMetrics() { }
///
/// Factory to instantiate or restore the metrics from AsyncLocal storage
///
public static PerRequestMetrics Current => _local.Value ??= new PerRequestMetrics();
// Time the request was started via the pipeline
public DateTime StartTime { get; set; }
// Offset Tics for each part of the proxy operation
public float RouteInvokeOffset { get; set; }
public float ProxyStartOffset { get; set; }
public float HttpRequestStartOffset { get; set; }
public float HttpConnectionEstablishedOffset { get; set; }
public float HttpRequestLeftQueueOffset { get; set; }
public float HttpRequestHeadersStartOffset { get; set; }
public float HttpRequestHeadersStopOffset { get; set; }
public float HttpRequestContentStartOffset { get; set; }
public float HttpRequestContentStopOffset { get; set; }
public float HttpResponseHeadersStartOffset { get; set; }
public float HttpResponseHeadersStopOffset { get; set; }
public float HttpResponseContentStopOffset { get; set; }
public float HttpRequestStopOffset { get; set; }
public float ProxyStopOffset { get; set; }
// Info about the request
public ForwarderError Error { get; set; }
public long RequestBodyLength { get; set; }
public long ResponseBodyLength { get; set; }
public long RequestContentIops { get; set; }
public long ResponseContentIops { get; set; }
public string DestinationId { get; set; }
public string ClusterId { get; set; }
public string RouteId { get; set; }
public string ToJson()
{
return JsonSerializer.Serialize(this, _jsonOptions);
}
public float CalcOffset(DateTime timestamp)
{
return (float)(timestamp - StartTime).TotalMilliseconds;
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/PerRequestYarpMetricCollectionMiddleware.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Yarp.Sample
{
///
/// Middleware that collects YARP metrics and logs them at the end of each request
///
public class PerRequestYarpMetricCollectionMiddleware
{
// Required for middleware
private readonly RequestDelegate _next;
// Supplied via DI
private readonly ILogger _logger;
public PerRequestYarpMetricCollectionMiddleware(RequestDelegate next, ILogger logger)
{
_logger = logger;
_next = next;
}
///
/// Entrypoint for being called as part of the request pipeline
///
public async Task InvokeAsync(HttpContext context)
{
var metrics = PerRequestMetrics.Current;
metrics.StartTime = DateTime.UtcNow;
// Call the next steps in the middleware, including the proxy
await _next(context);
// Called after the other middleware steps have completed
// Write the info to the console via ILogger. In a production scenario you probably want
// to write the results to your telemetry systems directly.
_logger.LogInformation("PerRequestMetrics: " + metrics.ToJson());
}
}
///
/// Helper to aid with registration of the middleware
///
public static class YarpMetricCollectionMiddlewareHelper
{
public static IApplicationBuilder UsePerRequestMetricCollection(
this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Yarp.Sample;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddControllers();
services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
services.AddHttpContextAccessor();
// Interface that collects general metrics about the proxy forwarder
services.AddMetricsConsumer();
// Registration of a consumer to events for proxy forwarder telemetry
services.AddTelemetryConsumer();
// Registration of a consumer to events for HttpClient telemetry
services.AddTelemetryConsumer();
services.AddTelemetryConsumer();
var app = builder.Build();
// Custom middleware that collects and reports the proxy metrics
// Placed at the beginning so that it is the first and last thing to run for each request
app.UsePerRequestMetricCollection();
// Middleware used to intercept the WebSocket connection and collect telemetry exposed to WebSocketsTelemetryConsumer
app.UseWebSocketsTelemetry();
app.MapReverseProxy();
app.Run();
================================================
FILE: samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.Metrics.Sample": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/README.md
================================================
# ReverseProxy.Metrics.Sample
This sample demonstrates how to use the ReverseProxy.Telemetry.Consumption library to listen to telemetry data from YARP.
In this case it uses the events to create a per-request data structure with detailed timings for each operation that takes place as part of the proxy operation.
Internally YARP uses EventSource to collect telemetry events and metrics from a number of subsystems that are used to process the requests.
The YARP telemetry library provides wrapper classes that collect these events metrics and make them available for Consumption.
To listen for the metrics you register classes with DI that implement an interface for each subsystem.
The subsystems are:
- **Proxy** which represents the overall proxy operation, and success or failure.
Events include:
- When proxy requests are started and stopped
- When request/response bodies are processed
Metrics include:
- Number of requests started
- Number of request in flight
- Number of requests that have failed
- **Kestrel** which is the web server that handles incoming requests.
Events include:
- When requests are started/stopped or fail
Metrics include:
- Connection Rate - how many connections are opened a second
- Total number of connections
- Number of TLS handshakes
- Incoming queue length
- **Http** which is the HttpClient which makes outgoing requests to the destination servers.
Events include:
- When connections are created
- When requests are started/stopped or fail
- When headers/contents are sent/received
- When requests are dequeued as connections become available
Metrics include:
- Number of outgoing requests started
- Number of requests failed
- Number of active requests
- Number of outbound connections
- **Sockets** which includes events around connection attempts & metrics about the amount of data sent and received
- **NameResolution** which includes events around name resolution attempts & metrics about DNS lookups of destinations
- **NetSecurity** which includes events around SslStream handshakes & metrics about the number and latency of handshakes per protocol
## Key Files
The following files are key to implementing the features described above:
### Program.cs
Performs registrtion of the proxy, the listener classes and a custom ASP.NET middleware step that starts per-request telemetry and reports the results when complete
### ProxyTelemetryConsumer.cs
Listens to events from the proxy telemetry and records timings and info about the high level processing involved in proxying a request.
### HttpTelemetryConsumer.cs
Listens to events from the HttpClient telemetry and records timings and info about the outbound request and response from the destination server.
### PerRequestMetrics.cs
Class to store the metrics on a per request basis. Instances are stored in AsyncLocal storage for the duration of the request.
### PerRequestYarpMetricCollectionMiddleware.cs
ASP.NET Core middleware that is the first and last thing called as part of the ASP.NET handling of the request. It initializes the per-request metrics and logs the results at the end of the request.
================================================
FILE: samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.Metrics.Sample/WebSocketsTelemetryConsumer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using Microsoft.Extensions.Logging;
using Yarp.Telemetry.Consumption;
namespace Yarp.Sample
{
public sealed class WebSocketsTelemetryConsumer : IWebSocketsTelemetryConsumer
{
private readonly ILogger _logger;
public WebSocketsTelemetryConsumer(ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger;
}
public void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten)
{
_logger.LogInformation($"WebSocket connection closed ({closeReason}) after reading {messagesRead} and writing {messagesWritten} messages over {(timestamp - establishedTime).TotalSeconds:N2} seconds.");
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Metrics.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
// "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"http": {
"Url": "http://localhost:5000"
},
"https": {
"Url": "https://localhost:5001"
}
}
},
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"cluster1/destination1": {
"Address": "https://example.com/"
}
}
}
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Net.Http;
using Yarp.ReverseProxy.Transforms;
using Yarp.ReverseProxy.Transforms.Builder;
namespace Yarp.Sample
{
internal sealed class MyTransformFactory : ITransformFactory
{
public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues)
{
if (transformValues.TryGetValue("CustomTransform", out var value))
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException("A non-empty CustomTransform value is required"));
}
return true; // Matched
}
return false;
}
public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues)
{
if (transformValues.TryGetValue("CustomTransform", out var value))
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("A non-empty CustomTransform value is required");
}
context.AddRequestTransform(transformContext =>
{
transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomTransform"), value);
return default;
});
return true;
}
return false;
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Net.Http;
using Yarp.ReverseProxy.Transforms;
using Yarp.ReverseProxy.Transforms.Builder;
namespace Yarp.Sample
{
internal sealed class MyTransformProvider : ITransformProvider
{
public void ValidateRoute(TransformRouteValidationContext context)
{
// Check all routes for a custom property and validate the associated transform data.
if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required"));
}
}
}
public void ValidateCluster(TransformClusterValidationContext context)
{
// Check all clusters for a custom property and validate the associated transform data.
if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required"));
}
}
}
public void Apply(TransformBuilderContext transformBuildContext)
{
// Check all routes for a custom property and add the associated transform.
if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false)
|| (transformBuildContext.Cluster?.Metadata?.TryGetValue("CustomMetadata", out value) ?? false))
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("A non-empty CustomMetadata value is required");
}
transformBuildContext.AddRequestTransform(transformContext =>
{
transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomMetadata"), value);
return default;
});
}
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Transforms;
using Yarp.Sample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms() // Adds custom transforms via code.
.AddTransformFactory() // Adds custom transforms via config.
.AddTransforms(transformBuilderContext => // Add transforms inline
{
// For each route+cluster pair decide if we want to add transforms, and if so, which?
// This logic is re-run each time a route is rebuilt.
transformBuilderContext.AddPathPrefix("/prefix");
// Only do this for routes that require auth.
if (string.Equals("token", transformBuilderContext.Route.AuthorizationPolicy))
{
transformBuilderContext.AddRequestTransform(async transformContext =>
{
// AuthN and AuthZ will have already been completed after request routing.
var ticket = await transformContext.HttpContext.AuthenticateAsync("token");
var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService();
var token = await tokenService.GetAuthTokenAsync(ticket.Principal);
transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
});
}
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Register the reverse proxy routes
app.MapReverseProxy();
app.Run();
================================================
FILE: samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json
================================================
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:44356/",
"sslPort": 44356
}
},
"profiles": {
"ReverseProxy.Transforms.Sample": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj
================================================
$(ReleaseTFMs)ExeYarp.Samplelatest
================================================
FILE: samples/ReverseProxy.Transforms.Sample/TokenService.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Claims;
using System.Threading.Tasks;
namespace Yarp.Sample
{
internal sealed class TokenService
{
internal Task GetAuthTokenAsync(ClaimsPrincipal user)
{
return Task.FromResult(user.Identity.Name);
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/ReverseProxy.Transforms.Sample/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"https": {
"Url": "https://localhost:5001"
},
"http": {
"Url": "http://localhost:5000"
}
}
},
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "{**catch-all}"
},
"Transforms": [
{ "PathPrefix": "/prefix" },
{ "RequestHeadersCopy": true },
{ "RequestHeaderOriginalHost": false },
{
"RequestHeader": "foo0",
"Append": "bar"
},
{
"RequestHeader": "foo1",
"Set": "bar, baz"
},
{
"RequestHeader": "clearMe",
"Set": ""
},
{
"ResponseHeader": "foo",
"Append": "bar",
"When": "Always"
},
{
"ResponseTrailer": "foo",
"Append": "trailer",
"When": "Always"
},
{
"CustomTransform": "custom value"
}
]
}
},
"Clusters": {
"cluster1": {
"Metadata": {
"CustomMetadata": "custom value"
},
"Destinations": {
"cluster1/destination1": {
"Address": "https://example.com"
}
}
}
}
}
}
================================================
FILE: samples/SampleServer/Controllers/HealthController.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Mvc;
namespace SampleServer.Controllers
{
///
/// Controller for active health check probes.
///
[ApiController]
public class HealthController : ControllerBase
{
private static volatile int _count;
///
/// Returns 200 if server is healthy.
///
[HttpGet]
[Route("/api/health")]
public IActionResult CheckHealth()
{
_count++;
// Simulate temporary health degradation.
return _count % 10 < 4 ? Ok() : StatusCode(500);
}
}
}
================================================
FILE: samples/SampleServer/Controllers/HttpController.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace SampleServer.Controllers
{
///
/// Sample controller.
///
[ApiController]
public class HttpController : ControllerBase
{
///
/// Returns a 200 response.
///
[HttpGet]
[Route("/api/noop")]
public void NoOp()
{
}
///
/// Returns a 200 response dumping all info from the incoming request.
///
[HttpGet, HttpPost]
[Route("/api/dump")]
[Route("/{**catchall}", Order = int.MaxValue)] // Make this the default route if nothing matches
public async Task Dump()
{
var result = new {
Request.Protocol,
Request.Method,
Request.Scheme,
Host = Request.Host.Value,
PathBase = Request.PathBase.Value,
Path = Request.Path.Value,
Query = Request.QueryString.Value,
Headers = Request.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),
Time = DateTimeOffset.UtcNow,
Body = await new StreamReader(Request.Body).ReadToEndAsync(),
};
return Ok(result);
}
///
/// Returns a 200 response dumping all info from the incoming request.
///
[HttpGet]
[Route("/api/statuscode")]
public void Status(int statusCode)
{
Response.StatusCode = statusCode;
}
///
/// Returns a 200 response dumping all info from the incoming request.
///
[HttpGet]
[Route("/api/headers")]
public void Headers([FromBody] Dictionary headers)
{
foreach (var (key, value) in headers)
{
Response.Headers[key] = value;
}
}
}
}
================================================
FILE: samples/SampleServer/Controllers/WebSocketsController.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace SampleServer.Controllers
{
///
/// Sample controller.
///
[ApiController]
public class WebSocketsController : ControllerBase
{
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
public WebSocketsController(ILogger logger)
{
_logger = logger;
}
///
/// Returns a 200 response.
///
[HttpGet]
[Route("/api/websockets")]
public async Task WebSockets()
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
{
HttpContext.Response.ContentType = "text/html";
await HttpContext.Response.SendFileAsync("./wwwroot/index.html");
return;
}
using (var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync())
{
_logger.LogInformation("WebSockets established.");
await RunPingPongAsync(webSocket, HttpContext.RequestAborted);
}
_logger.LogInformation("WebSockets finished.");
}
private static async Task RunPingPongAsync(WebSocket webSocket, CancellationToken cancellation)
{
var buffer = new byte[1024];
while (true)
{
var message = await webSocket.ReceiveAsync(buffer, cancellation);
if (message.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye", cancellation);
return;
}
await webSocket.SendAsync(new ArraySegment(buffer, 0, message.Count),
message.MessageType,
message.EndOfMessage,
cancellation);
}
}
}
}
================================================
FILE: samples/SampleServer/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
var app = builder.Build();
app.UseWebSockets();
app.MapControllers();
app.Run();
================================================
FILE: samples/SampleServer/Properties/launchSettings.json
================================================
{
"profiles": {
"SampleServer": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
================================================
FILE: samples/SampleServer/README.md
================================================
# Sample Server
This is a simple web server implementation that can be used to test YARP proxy, by using it as the destination.
Functionality in this sample server includes:
## Echoing of Request Headers
Provided that the request URI path doesn't match other endpoints described below, then the request headers will be reported back as text in the response body. This enables you to quickly see what headers were sent, for example to analyze header transforms made by the reverse proxy.
## Healthcheck status endpoint
[HealthController](Controllers/HealthController.cs) implements an API endpoint for /api/health that will randomly return bad health status.
## WebSockets endpoint
[WebSocketsController](Controllers/WebSocketsController.cs) implements an endpoint for testing web sockets at /api/websockets.
## Usage
To run the sample server use:
- ```dotnet run``` from the sample folder
- ```dotnet run SampleServer/SampleServer.csproj``` passing in the path to the .csproj file
- Build an executable using ```dotnet build SampleServer.csproj``` and then run the executable directly
The server will listen to http://localhost:5000 and https://localhost:5001 by default. The ports and interface can be changed using the urls option on the cmd line. For example ```dotnet run SampleServer/SampleServer.csproj --urls "https://localhost:10000;http://localhost:10010"```
================================================
FILE: samples/SampleServer/SampleServer.csproj
================================================
$(ReleaseTFMs)ExeSampleServer
================================================
FILE: samples/SampleServer/appsettings.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
================================================
FILE: samples/SampleServer/appsettings.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
================================================
FILE: samples/SampleServer/wwwroot/index.html
================================================
WebSocket Test Page
Ready to connect...
Note: When connected to the default server (i.e. the server in the address bar ;)), the message "ServerClose" will cause the server to close the connection. Similarly, the message "ServerAbort" will cause the server to forcibly terminate the connection without a closing handshake
Communication Log
From
To
Data
================================================
FILE: src/Application/Extensions.cs
================================================
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using HealthChecks.ApplicationStatus.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using static System.Net.WebRequestMethods;
namespace Microsoft.Extensions.Hosting;
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions
{
public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
return builder;
}
public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
var telemetryBuilder = builder.Services.AddOpenTelemetry();
telemetryBuilder
.WithLogging(logging => logging.AddOtlpExporter())
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.SetExemplarFilter(ExemplarFilterType.TraceBased)
.AddOtlpExporter();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter();
});
if (string.Equals(Environment.GetEnvironmentVariable("YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE"), "true", StringComparison.InvariantCultureIgnoreCase))
{
// We cannot use UseOtlpExporter() since it doesn't support configuration via OtlpExporterOptions
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/5802
builder.Services.Configure(ConfigureOtlpExporterOptions);
}
}
static void ConfigureOtlpExporterOptions(OtlpExporterOptions options)
{
options.HttpClientFactory = () =>
{
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var httpClient = new HttpClient(handler);
return httpClient;
};
}
return builder;
}
public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
// Add a default liveness check on application status.
.AddApplicationStatus(tags: ["live"]);
return builder;
}
}
================================================
FILE: src/Application/Program.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder();
// Load configuration from file if passed
if (args.Length == 1)
{
var configFile = args[0];
var fileInfo = new FileInfo(configFile);
if (!fileInfo.Exists)
{
Console.Error.WriteLine($"Could not find '{configFile}'.");
return 2;
}
builder.Configuration.AddJsonFile(fileInfo.FullName, optional: false, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
}
// Configure YARP
builder.AddServiceDefaults();
builder.Services.AddServiceDiscovery();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddServiceDiscoveryDestinationResolver();
var app = builder.Build();
var isEnabledStaticFiles = Environment.GetEnvironmentVariable("YARP_ENABLE_STATIC_FILES");
if (string.Equals(isEnabledStaticFiles, "true", StringComparison.OrdinalIgnoreCase))
{
app.UseFileServer();
}
app.UseRouting();
app.MapReverseProxy();
await app.RunAsync();
return 0;
================================================
FILE: src/Application/Yarp.Application.csproj
================================================
Exewin-x64;win-arm64;linux-x64;linux-arm64;net9.0enableenableyarpfalsefalse
================================================
FILE: src/Common/Package.targets
================================================
================================================
FILE: src/Directory.Build.props
================================================
$(MSBuildProjectDirectory)\ConfigurationSchema.jsontrue$(TargetsForTfmSpecificContentInPackage);AddPackageTargetsInPackagetruetruetrueicon.png
================================================
FILE: src/Kubernetes.Controller/Caching/Endpoints.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s.Models;
using System;
using System.Collections.Generic;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// Holds data needed from a resource.
///
public struct Endpoints
{
public Endpoints(V1Endpoints endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
Name = endpoints.Name();
Subsets = endpoints.Subsets;
}
public string Name { get; set; }
public IList Subsets { get; }
}
================================================
FILE: src/Kubernetes.Controller/Caching/ICache.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Models;
using System.Collections.Generic;
using System.Collections.Immutable;
using Yarp.Kubernetes.Controller.Services;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// ICache service interface holds onto the least amount of data necessary
/// for to process work.
///
public interface ICache
{
void Update(WatchEventType eventType, V1IngressClass ingressClass);
bool Update(WatchEventType eventType, V1Ingress ingress);
ImmutableList Update(WatchEventType eventType, V1Service service);
ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints);
void Update(WatchEventType eventType, V1Secret secret);
bool TryGetReconcileData(NamespacedName key, out ReconcileData data);
void GetKeys(List keys);
IEnumerable GetIngresses();
}
================================================
FILE: src/Kubernetes.Controller/Caching/IngressCache.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Yarp.Kubernetes.Controller.Certificates;
using Yarp.Kubernetes.Controller.Services;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// ICache service interface holds onto the least amount of data necessary
/// for to process work.
///
public class IngressCache : ICache
{
private readonly object _sync = new object();
private readonly Dictionary _ingressClassData = new Dictionary();
private readonly Dictionary _namespaceCaches = new Dictionary();
private readonly YarpOptions _options;
private readonly IServerCertificateSelector _certificateSelector;
private readonly ICertificateHelper _certificateHelper;
private readonly ILogger _logger;
private bool _isDefaultController;
public IngressCache(IOptions options, IServerCertificateSelector certificateSelector, ICertificateHelper certificateHelper, ILogger logger)
{
ArgumentNullException.ThrowIfNull(options?.Value);
ArgumentNullException.ThrowIfNull(certificateSelector);
ArgumentNullException.ThrowIfNull(certificateHelper);
ArgumentNullException.ThrowIfNull(logger);
_options = options.Value;
_certificateSelector = certificateSelector;
_certificateHelper = certificateHelper;
_logger = logger;
}
public void Update(WatchEventType eventType, V1IngressClass ingressClass)
{
ArgumentNullException.ThrowIfNull(ingressClass);
if (!string.Equals(_options.ControllerClass, ingressClass.Spec.Controller, StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation(
"Ignoring {IngressClassNamespace}/{IngressClassName} as the spec.controller is not the same as this ingress",
ingressClass.Metadata.NamespaceProperty,
ingressClass.Metadata.Name);
return;
}
var ingressClassName = ingressClass.Name();
lock (_sync)
{
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
_ingressClassData[ingressClassName] = new IngressClassData(ingressClass);
}
else if (eventType == WatchEventType.Deleted)
{
_ingressClassData.Remove(ingressClassName);
}
_isDefaultController = _ingressClassData.Values.Any(ic => ic.IsDefault);
}
}
public bool Update(WatchEventType eventType, V1Ingress ingress)
{
ArgumentNullException.ThrowIfNull(ingress);
Namespace(ingress.Namespace()).Update(eventType, ingress);
return true;
}
public ImmutableList Update(WatchEventType eventType, V1Service service)
{
ArgumentNullException.ThrowIfNull(service);
return Namespace(service.Namespace()).Update(eventType, service);
}
public ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints)
{
return Namespace(endpoints.Namespace()).Update(eventType, endpoints);
}
public void Update(WatchEventType eventType, V1Secret secret)
{
var namespacedName = NamespacedName.From(secret);
_logger.LogDebug("Found secret '{NamespacedName}'. Checking against default {CertificateSecretName}", namespacedName, _options.DefaultSslCertificate);
if (!string.Equals(namespacedName.ToString(), _options.DefaultSslCertificate, StringComparison.OrdinalIgnoreCase))
{
return;
}
_logger.LogInformation("Found secret `{NamespacedName}` to use as default certificate for HTTPS traffic", namespacedName);
var certificate = _certificateHelper.ConvertCertificate(namespacedName, secret);
if (certificate is null)
{
return;
}
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
_certificateSelector.AddCertificate(namespacedName, certificate);
}
else if (eventType == WatchEventType.Deleted)
{
_certificateSelector.RemoveCertificate(namespacedName);
}
}
public bool TryGetReconcileData(NamespacedName key, out ReconcileData data)
{
return Namespace(key.Namespace).TryLookup(key, out data);
}
public void GetKeys(List keys)
{
lock (_sync)
{
foreach (var (ns, cache) in _namespaceCaches)
{
cache.GetKeys(ns, keys);
}
}
}
public IEnumerable GetIngresses()
{
var ingresses = new List();
lock (_sync)
{
foreach (var ns in _namespaceCaches)
{
ingresses.AddRange(ns.Value.GetIngresses().Where(IsYarpIngress));
}
}
return ingresses;
}
private bool IsYarpIngress(IngressData ingress)
{
if (ingress.Spec.IngressClassName is null)
{
return _isDefaultController;
}
lock (_sync)
{
return _ingressClassData.ContainsKey(ingress.Spec.IngressClassName);
}
}
private NamespaceCache Namespace(string key)
{
lock (_sync)
{
if (!_namespaceCaches.TryGetValue(key, out var value))
{
value = new NamespaceCache();
_namespaceCaches.Add(key, value);
}
return value;
}
}
}
================================================
FILE: src/Kubernetes.Controller/Caching/IngressClassData.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s.Models;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// Holds data needed from a resource.
///
public struct IngressClassData
{
public IngressClassData(V1IngressClass ingressClass)
{
ArgumentNullException.ThrowIfNull(ingressClass);
IngressClass = ingressClass;
IsDefault = GetDefaultAnnotation(ingressClass);
}
public V1IngressClass IngressClass { get; }
public bool IsDefault { get; }
private static bool GetDefaultAnnotation(V1IngressClass ingressClass)
{
var annotation = ingressClass.GetAnnotation("ingressclass.kubernetes.io/is-default-class");
return string.Equals("true", annotation, StringComparison.OrdinalIgnoreCase);
}
}
================================================
FILE: src/Kubernetes.Controller/Caching/IngressData.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s.Models;
using System;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// Holds data needed from a resource.
///
public struct IngressData
{
public IngressData(V1Ingress ingress)
{
ArgumentNullException.ThrowIfNull(ingress);
Spec = ingress.Spec;
Metadata = ingress.Metadata;
}
public V1IngressSpec Spec { get; set; }
public V1ObjectMeta Metadata { get; set; }
}
================================================
FILE: src/Kubernetes.Controller/Caching/NamespaceCache.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Yarp.Kubernetes.Controller.Services;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// Per-namespace cache data. Implicitly scopes name-based lookups to same namespace. Also
/// intended to make updates faster because cross-reference dictionaries are not cluster-wide.
///
public class NamespaceCache
{
private readonly object _sync = new object();
private readonly Dictionary> _ingressToServiceNames = new Dictionary>();
private readonly Dictionary> _serviceToIngressNames = new Dictionary>();
private readonly Dictionary _ingressData = new Dictionary();
private readonly Dictionary _serviceData = new Dictionary();
private readonly Dictionary _endpointsData = new Dictionary();
public void Update(WatchEventType eventType, V1Ingress ingress)
{
ArgumentNullException.ThrowIfNull(ingress);
var serviceNames = ImmutableList.Empty;
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
// If the ingress exists, list out the related services
var spec = ingress.Spec;
var defaultBackend = spec?.DefaultBackend;
var defaultService = defaultBackend?.Service;
if (!string.IsNullOrEmpty(defaultService?.Name))
{
serviceNames = serviceNames.Add(defaultService.Name);
}
foreach (var rule in spec.Rules ?? Enumerable.Empty())
{
var http = rule.Http;
foreach (var path in http.Paths ?? Enumerable.Empty())
{
var backend = path.Backend;
var service = backend.Service;
if (!serviceNames.Contains(service.Name))
{
serviceNames = serviceNames.Add(service.Name);
}
}
}
}
var ingressName = ingress.Name();
lock (_sync)
{
var serviceNamesPrevious = ImmutableList.Empty;
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
// If the ingress exists then remember details
_ingressData[ingressName] = new IngressData(ingress);
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
{
_ingressToServiceNames[ingressName] = serviceNames;
}
else
{
serviceNamesPrevious = ImmutableList.Empty;
_ingressToServiceNames.Add(ingressName, serviceNames);
}
}
else if (eventType == WatchEventType.Deleted)
{
// otherwise clear out details
_ingressData.Remove(ingressName);
if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))
{
_ingressToServiceNames.Remove(ingressName);
}
}
// update cross-reference for new ingress-to-services linkage not previously known
foreach (var serviceName in serviceNames)
{
if (!serviceNamesPrevious.Contains(serviceName))
{
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNamesPrevious))
{
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Add(ingressName);
}
else
{
_serviceToIngressNames.Add(serviceName, ImmutableList.Empty.Add(ingressName));
}
}
}
// remove cross-reference for previous ingress-to-services linkage no longer present
foreach (var serviceName in serviceNamesPrevious)
{
if (!serviceNames.Contains(serviceName))
{
_serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Remove(ingressName);
}
}
}
}
public ImmutableList Update(WatchEventType eventType, V1Service service)
{
ArgumentNullException.ThrowIfNull(service);
var serviceName = service.Name();
lock (_sync)
{
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
_serviceData[serviceName] = new ServiceData(service);
}
else if (eventType == WatchEventType.Deleted)
{
_serviceData.Remove(serviceName);
}
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))
{
return ingressNames;
}
else
{
return ImmutableList.Empty;
}
}
}
public void GetKeys(string ns, List keys)
{
ArgumentNullException.ThrowIfNull(keys);
lock (_sync)
{
foreach (var name in _ingressData.Keys)
{
keys.Add(new NamespacedName(ns, name));
}
}
}
public ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var serviceName = endpoints.Name();
lock (_sync)
{
if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)
{
_endpointsData[serviceName] = new Endpoints(endpoints);
}
else if (eventType == WatchEventType.Deleted)
{
_endpointsData.Remove(serviceName);
}
if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))
{
return ingressNames;
}
else
{
return ImmutableList.Empty;
}
}
}
public IEnumerable GetIngresses()
{
return _ingressData.Values;
}
public bool IngressExists(V1Ingress ingress)
{
return _ingressData.ContainsKey(ingress.Name());
}
public bool TryLookup(NamespacedName key, out ReconcileData data)
{
var endpointsList = new List();
var servicesList = new List();
lock (_sync)
{
if (!_ingressData.TryGetValue(key.Name, out var ingress))
{
data = default;
return false;
}
if (_ingressToServiceNames.TryGetValue(key.Name, out var serviceNames))
{
foreach (var serviceName in serviceNames)
{
if (_serviceData.TryGetValue(serviceName, out var serviceData))
{
servicesList.Add(serviceData);
}
if (_endpointsData.TryGetValue(serviceName, out var endpoints))
{
endpointsList.Add(endpoints);
}
}
}
if (_serviceData.Count == 0)
{
data = default;
return false;
}
data = new ReconcileData(ingress, servicesList, endpointsList);
return true;
}
}
}
================================================
FILE: src/Kubernetes.Controller/Caching/ServiceData.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s.Models;
using System;
namespace Yarp.Kubernetes.Controller.Caching;
///
/// Holds data needed from a resource.
///
public struct ServiceData
{
public ServiceData(V1Service service)
{
ArgumentNullException.ThrowIfNull(service);
Spec = service.Spec;
Metadata = service.Metadata;
}
public V1ServiceSpec Spec { get; set; }
public V1ObjectMeta Metadata { get; set; }
}
================================================
FILE: src/Kubernetes.Controller/Certificates/CertificateHelper.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using k8s.Models;
using Microsoft.Extensions.Logging;
namespace Yarp.Kubernetes.Controller.Certificates;
public class CertificateHelper : ICertificateHelper
{
private const string TlsCertKey = "tls.crt";
private const string TlsPrivateKeyKey = "tls.key";
private readonly ILogger _logger;
public CertificateHelper(ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger;
}
public X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret)
{
try
{
var cert = secret?.Data[TlsCertKey];
var privateKey = secret?.Data[TlsPrivateKeyKey];
if (cert == null || cert.Length == 0 || privateKey == null || privateKey.Length == 0)
{
_logger.LogWarning("TLS secret '{NamespacedName}' contains invalid data.", namespacedName);
return null;
}
var certString = EnsurePemFormat(cert, "CERTIFICATE");
var privateString = EnsurePemFormat(privateKey, "PRIVATE KEY");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Cert needs converting. Read https://github.com/dotnet/runtime/issues/23749#issuecomment-388231655
using var convertedCertificate = X509Certificate2.CreateFromPem(certString, privateString);
return new X509Certificate2(convertedCertificate.Export(X509ContentType.Pkcs12));
}
return X509Certificate2.CreateFromPem(certString, privateString);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to convert secret '{NamespacedName}'", namespacedName);
}
return null;
}
///
/// Kubernetes Secrets should be stored in base-64 encoded DER format (see https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets)
/// but need can be imported into a object via PEM. Before this type of secret existed, an Opaque secret would be
/// used containing the full PEM format, so it's possible that the incorrect format would be used.
/// Doing it this way means we are more tolerant in handling certs in the wrong format.
///
/// The raw data.
/// The type for the PEM header.
/// The certificate data in PEM format.
private static string EnsurePemFormat(byte[] data, string pemType)
{
var der = Encoding.ASCII.GetString(data);
if (!der.StartsWith("---", StringComparison.Ordinal))
{
// Convert from encoded DER to PEM
return $"-----BEGIN {pemType}-----\n{der}\n-----END {pemType}-----";
}
return der;
}
}
================================================
FILE: src/Kubernetes.Controller/Certificates/ICertificateHelper.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Cryptography.X509Certificates;
using k8s.Models;
namespace Yarp.Kubernetes.Controller.Certificates;
public interface ICertificateHelper
{
X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret);
}
================================================
FILE: src/Kubernetes.Controller/Certificates/IServerCertificateSelector.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
namespace Yarp.Kubernetes.Controller.Certificates;
///
/// A mechanism for obtaining server certificates dynamically based on the SNI domain name.
///
public interface IServerCertificateSelector
{
///
/// Retrieve a certificate using the provided domain name.
///
/// The connection context.
/// The domain name.
/// Either returns the specific certificate for the domain name, a wildcard certificates, or no certificate.
X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName);
///
/// Adds a certificate to the selector.
///
/// An identifier for the certificate that can be used to remove it.
/// The server certificate.
void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate);
///
/// Removes a certificate from the selector.
///
/// An identifier for the certificate that can be used to remove it.
void RemoveCertificate(NamespacedName certificateName);
}
================================================
FILE: src/Kubernetes.Controller/Certificates/ServerCertificateSelector.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
namespace Yarp.Kubernetes.Controller.Certificates;
internal class ServerCertificateSelector : IServerCertificateSelector
{
private X509Certificate2 _defaultCertificate;
public void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate)
{
_defaultCertificate = certificate;
}
public X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName)
{
return _defaultCertificate;
}
public void RemoveCertificate(NamespacedName certificateName)
{
_defaultCertificate = null;
}
}
================================================
FILE: src/Kubernetes.Controller/Client/GroupApiVersionKind.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s.Models;
using System;
using System.Reflection;
namespace Yarp.Kubernetes.Controller.Client;
public struct GroupApiVersionKind : IEquatable
{
public GroupApiVersionKind(string group, string apiVersion, string kind)
{
ApiVersion = apiVersion;
GroupApiVersion = string.IsNullOrEmpty(group) ? apiVersion : $"{group}/{apiVersion}";
Kind = kind;
}
public string ApiVersion { get; }
public string GroupApiVersion { get; }
public string Kind { get; }
public static GroupApiVersionKind From() => From(typeof(TResource));
public static GroupApiVersionKind From(Type resourceType)
{
var entity = resourceType.GetTypeInfo().GetCustomAttribute();
return new GroupApiVersionKind(
group: entity.Group,
apiVersion: entity.ApiVersion,
kind: entity.Kind);
}
public override bool Equals(object obj)
{
return obj is GroupApiVersionKind kind && Equals(kind);
}
public bool Equals(GroupApiVersionKind other)
{
return GroupApiVersion == other.GroupApiVersion
&& Kind == other.Kind;
}
public override int GetHashCode()
{
return HashCode.Combine(GroupApiVersion, Kind);
}
public override string ToString()
{
return $"{Kind}.{GroupApiVersion}";
}
public static bool operator ==(GroupApiVersionKind left, GroupApiVersionKind right)
{
return left.Equals(right);
}
public static bool operator !=(GroupApiVersionKind left, GroupApiVersionKind right)
{
return !(left == right);
}
}
================================================
FILE: src/Kubernetes.Controller/Client/IIngressResourceStatusUpdater.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
public interface IIngressResourceStatusUpdater
{
///
/// Updates the status of cached ingresses.
///
Task UpdateStatusAsync(CancellationToken cancellationToken);
}
================================================
FILE: src/Kubernetes.Controller/Client/IResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
///
/// Callback for resource event notifications.
///
/// The type of being monitored.
/// The type of change event which was received.
/// The instance of the resource which was received.
public delegate void ResourceInformerCallback(WatchEventType eventType, TResource resource) where TResource : class, IKubernetesObject;
///
/// Interface IResourceInformer is a service which generates
/// notifications for a specific type
/// of Kubernetes object. The callback eventType informs if the notification
/// is because it is new, modified, or has been deleted.
/// Implements the .
///
/// The type of the t resource.
///
///
public interface IResourceInformer : IHostedService, IResourceInformer
where TResource : class, IKubernetesObject, new()
{
///
/// Registers a callback for change notification. To ensure no events are missed the registration
/// may be created in the constructor of a dependant . The returned
/// registration should be disposed when the receiver is ending its work.
///
/// The delegate that is invoked with each resource notification.
/// A registration that should be disposed to end the notifications.
IResourceInformerRegistration Register(ResourceInformerCallback callback);
}
public interface IResourceInformer
{
///
/// Instructs the resource informer to being watching resources. Allows the startup of informers to be synchronised.
///
void StartWatching();
///
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
/// Once an await on this method is completed it is safe to assume that all the knowledge of this resource
/// type has been made available. Any new changes will be a result of receiving new updates.
///
/// The cancellation token that can be used by other objects or threads to receive notice of cancellation.
/// Task.
Task ReadyAsync(CancellationToken cancellationToken);
///
/// Registers a callback for change notification. To ensure no events are missed the registration
/// may be created in the constructor of a dependant . The returned
/// registration should be disposed when the receiver is ending its work.
///
/// The delegate that is invoked with each resource notification.
/// A registration that should be disposed to end the notifications.
IResourceInformerRegistration Register(ResourceInformerCallback> callback);
}
================================================
FILE: src/Kubernetes.Controller/Client/IResourceInformerRegistration.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
///
/// Returned by to control the lifetime of an event
/// notification connection. Call when the lifetime of the notification receiver is ending.
///
public interface IResourceInformerRegistration : IDisposable
{
///
/// Returns a task that can be awaited to know when the initial listing of resources is complete.
/// Once an await on this method is completed it is safe to assume that all the knowledge of this resource
/// type has been made available. Any new changes will be a result of receiving new updates.
///
/// The cancellation token that can be used by other objects or threads to receive notice of cancellation.
/// Task.
Task ReadyAsync(CancellationToken cancellationToken);
}
================================================
FILE: src/Kubernetes.Controller/Client/KubernetesClientOptions.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
namespace Yarp.Kubernetes.Controller.Client;
///
/// Class KubernetesClientOptions.
///
public class KubernetesClientOptions
{
///
/// Gets or sets the configuration.
///
/// The configuration.
public KubernetesClientConfiguration Configuration { get; set; }
}
================================================
FILE: src/Kubernetes.Controller/Client/ResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Yarp.Kubernetes.Controller.Hosting;
using Yarp.Kubernetes.Controller.Rate;
namespace Yarp.Kubernetes.Controller.Client;
///
/// Class ResourceInformer.
/// Implements the .
/// Implements the .
///
/// The type of the t resource.
/// The type of the t resource used in lists.
///
///
public abstract class ResourceInformer : BackgroundHostedService, IResourceInformer
where TResource : class, IKubernetesObject, new()
where TListResource : class, IKubernetesObject, IItems, new()
{
private readonly object _sync = new object();
private readonly GroupApiVersionKind _names;
private readonly SemaphoreSlim _ready = new SemaphoreSlim(0);
private readonly SemaphoreSlim _start = new SemaphoreSlim(0);
private readonly ResourceSelector _selector;
private ImmutableList _registrations = ImmutableList.Empty;
private Dictionary> _cache = [];
private string _lastResourceVersion;
///
/// Initializes a new instance of the class.
///
/// The client.
/// A resource selector for (optionally) filtering the list of resources.
/// The host application lifetime.
/// The logger.
public ResourceInformer(
IKubernetes client,
ResourceSelector selector,
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger)
: base(hostApplicationLifetime, logger)
{
ArgumentNullException.ThrowIfNull(client);
ArgumentNullException.ThrowIfNull(selector);
Client = client;
_selector = selector;
_names = GroupApiVersionKind.From();
}
private enum EventType
{
SynchronizeStarted = 101,
SynchronizeComplete = 102,
WatchingResource = 103,
ReceivedError = 104,
WatchingComplete = 105,
InformerWatchEvent = 106,
DisposingToReconnect = 107,
IgnoringError = 108,
}
protected IKubernetes Client { get; init; }
///
protected override void Dispose(bool disposing)
{
if (disposing)
{
try
{
_start.Dispose();
_ready.Dispose();
}
catch (ObjectDisposedException)
{
// ignore redundant exception to allow shutdown sequence to progress uninterrupted
}
}
base.Dispose(disposing);
}
public void StartWatching()
{
_start.Release();
}
public virtual IResourceInformerRegistration Register(ResourceInformerCallback callback)
{
return new Registration(this, callback);
}
public IResourceInformerRegistration Register(ResourceInformerCallback> callback)
{
return new Registration(this, (eventType, resource) => callback(eventType, resource));
}
///
public virtual async Task ReadyAsync(CancellationToken cancellationToken)
{
await _ready.WaitAsync(cancellationToken).ConfigureAwait(false);
// Release is called after each WaitAsync because
// the semaphore is being used as a manual reset event
_ready.Release();
}
///
/// RunAsync starts processing when StartAsync is called, and is terminated when
/// StopAsync is called.
///
/// The cancellation token that can be used by other objects or threads to receive notice of cancellation.
/// A representing the result of the asynchronous operation.
public override async Task RunAsync(CancellationToken cancellationToken)
{
try
{
await _start.WaitAsync(cancellationToken).ConfigureAwait(false);
var limiter = new Limiter(new Limit(0.2), 3);
var shouldSync = true;
var firstSync = true;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
if (shouldSync)
{
await ListAsync(cancellationToken).ConfigureAwait(true);
shouldSync = false;
}
if (firstSync)
{
_ready.Release();
firstSync = false;
}
await WatchAsync(cancellationToken).ConfigureAwait(true);
}
catch (IOException ex) when (ex.InnerException is SocketException)
{
Logger.LogDebug(
EventId(EventType.ReceivedError),
"Received error watching {ResourceType}: {ErrorMessage}",
typeof(TResource).Name,
ex.Message);
}
catch (KubernetesException ex)
{
Logger.LogDebug(
EventId(EventType.ReceivedError),
"Received error watching {ResourceType}: {ErrorMessage}",
typeof(TResource).Name,
ex.Message);
// deal with this non-recoverable condition "too old resource version"
// with a re-sync to listing everything again ensuring no subscribers miss updates
if (ex is KubernetesException kubernetesError)
{
if (string.Equals(kubernetesError.Status.Reason, "Expired", StringComparison.Ordinal))
{
shouldSync = true;
}
}
}
// rate limiting the reconnect loop
await limiter.WaitAsync(cancellationToken).ConfigureAwait(true);
}
}
catch (Exception error)
{
Logger.LogInformation(
EventId(EventType.WatchingComplete),
error,
"No longer watching {ResourceType} resources from API server.",
typeof(TResource).Name);
throw;
}
}
protected abstract Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default);
protected abstract Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null);
private static EventId EventId(EventType eventType) => new EventId((int)eventType, eventType.ToString());
private async Task ListAsync(CancellationToken cancellationToken)
{
var previousCache = _cache;
_cache = new Dictionary>();
if (_selector.FieldSelector is not null)
{
Logger.LogInformation(
EventId(EventType.SynchronizeStarted),
"Started synchronizing {ResourceType} resources from API server with field selector '{FieldSelector}'.",
typeof(TResource).Name,
_selector.FieldSelector);
}
else
{
Logger.LogInformation(
EventId(EventType.SynchronizeStarted),
"Started synchronizing {ResourceType} resources from API server.",
typeof(TResource).Name);
}
string continueParameter = null;
do
{
cancellationToken.ThrowIfCancellationRequested();
// request next page of items
using var listWithHttpMessage = await RetrieveResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector, cancellationToken: cancellationToken);
var list = listWithHttpMessage.Body;
foreach (var item in list.Items)
{
// These properties are not already set on items while listing
// assigned here for consistency
item.ApiVersion = _names.GroupApiVersion;
item.Kind = _names.Kind;
var key = NamespacedName.From(item);
_cache[key] = item?.Metadata?.OwnerReferences;
var watchEventType = WatchEventType.Added;
if (previousCache.Remove(key))
{
// an already-known key is provided as a modification for re-sync purposes
watchEventType = WatchEventType.Modified;
}
InvokeRegistrationCallbacks(watchEventType, item);
}
foreach (var (key, value) in previousCache)
{
// for anything which was previously known but not part of list
// send a deleted notification to clear any observer caches
var item = new TResource
{
ApiVersion = _names.GroupApiVersion,
Kind = _names.Kind,
Metadata = new V1ObjectMeta
{
Name = key.Name,
NamespaceProperty = key.Namespace,
OwnerReferences = value
}
};
InvokeRegistrationCallbacks(WatchEventType.Deleted, item);
}
// keep track of values needed for next page and to start watching
_lastResourceVersion = list.ResourceVersion();
continueParameter = list.Continue();
}
while (!string.IsNullOrEmpty(continueParameter));
Logger.LogInformation(
EventId(EventType.SynchronizeComplete),
"Completed synchronizing {ResourceType} resources from API server.",
typeof(TResource).Name);
}
private async Task WatchAsync(CancellationToken cancellationToken)
{
Logger.LogInformation(
EventId(EventType.WatchingResource),
"Watching {ResourceType} starting from resource version {ResourceVersion}.",
typeof(TResource).Name,
_lastResourceVersion);
// completion source helps turn OnClose callback into something awaitable
var watcherCompletionSource = new TaskCompletionSource();
// begin watching where list left off
var watcher = WatchResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector,
(watchEventType, item) =>
{
if (!watcherCompletionSource.Task.IsCompleted)
{
OnEvent(watchEventType, item);
}
},
error =>
{
if (error is KubernetesException kubernetesError)
{
// deal with this non-recoverable condition "too old resource version"
if (string.Equals(kubernetesError.Status.Reason, "Expired", StringComparison.Ordinal))
{
// cause this error to surface
watcherCompletionSource.TrySetException(error);
throw error;
}
}
Logger.LogDebug(
EventId(EventType.IgnoringError),
"Ignoring error {ErrorType}: {ErrorMessage}",
error.GetType().Name,
error.Message);
},
() =>
{
watcherCompletionSource.TrySetResult(0);
}
);
var lastEventUtc = DateTime.UtcNow;
// reconnect if no events have arrived after a certain time
using var checkLastEventUtcTimer = new Timer(
_ =>
{
var lastEvent = DateTime.UtcNow - lastEventUtc;
if (lastEvent > TimeSpan.FromMinutes(9.5))
{
lastEventUtc = DateTime.MaxValue;
Logger.LogDebug(
EventId(EventType.DisposingToReconnect),
"Disposing watcher for {ResourceType} to cause reconnect.",
typeof(TResource).Name);
watcherCompletionSource.TrySetCanceled();
watcher.Dispose();
}
},
state: null,
dueTime: TimeSpan.FromSeconds(45),
period: TimeSpan.FromSeconds(45));
using var registration = cancellationToken.Register(watcher.Dispose);
try
{
await watcherCompletionSource.Task;
}
catch (TaskCanceledException)
{
}
}
private void OnEvent(WatchEventType watchEventType, TResource item)
{
if (watchEventType != WatchEventType.Modified || item.Kind != "ConfigMap")
{
Logger.LogDebug(
EventId(EventType.InformerWatchEvent),
"Informer {ResourceType} received {WatchEventType} notification for {ItemKind}/{ItemName}.{ItemNamespace} at resource version {ResourceVersion}",
typeof(TResource).Name,
watchEventType,
item.Kind,
item.Name(),
item.Namespace(),
item.ResourceVersion());
}
if (watchEventType == WatchEventType.Added ||
watchEventType == WatchEventType.Modified)
{
// BUGBUG: log warning if cache was not in expected state
_cache[NamespacedName.From(item)] = item.Metadata?.OwnerReferences;
}
if (watchEventType == WatchEventType.Deleted)
{
_cache.Remove(NamespacedName.From(item));
}
if (watchEventType == WatchEventType.Added ||
watchEventType == WatchEventType.Modified ||
watchEventType == WatchEventType.Deleted ||
watchEventType == WatchEventType.Bookmark)
{
_lastResourceVersion = item.ResourceVersion();
}
if (watchEventType == WatchEventType.Added ||
watchEventType == WatchEventType.Modified ||
watchEventType == WatchEventType.Deleted)
{
InvokeRegistrationCallbacks(watchEventType, item);
}
}
private void InvokeRegistrationCallbacks(WatchEventType eventType, TResource resource)
{
List innerExceptions = default;
foreach (var registration in _registrations)
{
try
{
registration.Callback.Invoke(eventType, resource);
}
catch (Exception innerException)
{
innerExceptions ??= new List();
innerExceptions.Add(innerException);
}
}
if (innerExceptions is not null)
{
throw new AggregateException("One or more exceptions thrown by ResourceInformerCallback.", innerExceptions);
}
}
internal class Registration : IResourceInformerRegistration
{
private bool _disposedValue;
public Registration(ResourceInformer resourceInformer, ResourceInformerCallback callback)
{
ResourceInformer = resourceInformer;
Callback = callback;
lock (resourceInformer._sync)
{
resourceInformer._registrations = resourceInformer._registrations.Add(this);
}
}
~Registration()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public ResourceInformer ResourceInformer { get; }
public ResourceInformerCallback Callback { get; }
public Task ReadyAsync(CancellationToken cancellationToken) => ResourceInformer.ReadyAsync(cancellationToken);
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
lock (ResourceInformer._sync)
{
ResourceInformer._registrations = ResourceInformer._registrations.Remove(this);
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
================================================
FILE: src/Kubernetes.Controller/Client/ResourceSelector.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using k8s;
using k8s.Models;
namespace Yarp.Kubernetes.Controller.Client;
///
/// Provides a mechanism for to constrain search based on fields in the resource.
///
public class ResourceSelector
where TResource : class, IKubernetesObject, new()
{
public ResourceSelector(string fieldSelector)
{
FieldSelector = fieldSelector;
}
public string FieldSelector { get; }
}
================================================
FILE: src/Kubernetes.Controller/Client/V1EndpointsResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
internal class V1EndpointsResourceInformer : ResourceInformer
{
public V1EndpointsResourceInformer(
IKubernetes client,
ResourceSelector selector,
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger)
: base(client, selector, hostApplicationLifetime, logger)
{
}
protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default)
{
return Client.CoreV1.ListEndpointsForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);
}
protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null)
{
return Client.CoreV1.WatchListEndpointsForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);
}
}
================================================
FILE: src/Kubernetes.Controller/Client/V1IngressClassResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
internal class V1IngressClassResourceInformer : ResourceInformer
{
public V1IngressClassResourceInformer(
IKubernetes client,
ResourceSelector selector,
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger)
: base(client, selector, hostApplicationLifetime, logger)
{
}
protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default)
{
return Client.NetworkingV1.ListIngressClassWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);
}
protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null)
{
return Client.NetworkingV1.WatchListIngressClass(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);
}
}
================================================
FILE: src/Kubernetes.Controller/Client/V1IngressResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
internal class V1IngressResourceInformer : ResourceInformer
{
public V1IngressResourceInformer(
IKubernetes client,
ResourceSelector selector,
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger)
: base(client, selector, hostApplicationLifetime, logger)
{
}
protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default)
{
return Client.NetworkingV1.ListIngressForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);
}
protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null)
{
return Client.NetworkingV1.WatchListIngressForAllNamespaces(resourceVersion: resourceVersion,
fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);
}
}
================================================
FILE: src/Kubernetes.Controller/Client/V1IngressResourceStatusUpdater.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Linq;
using k8s;
using k8s.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
using Yarp.Kubernetes.Controller.Caching;
using System.Threading;
namespace Yarp.Kubernetes.Controller.Client;
internal sealed class V1IngressResourceStatusUpdater : IIngressResourceStatusUpdater
{
private readonly IKubernetes _client;
private readonly YarpOptions _options;
private readonly ICache _cache;
private readonly ILogger _logger;
public V1IngressResourceStatusUpdater(
IKubernetes client,
IOptions options,
ICache cache,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(options?.Value);
_options = options.Value;
_client = client;
_cache = cache;
_logger = logger;
}
public async Task UpdateStatusAsync(CancellationToken cancellationToken)
{
var service = await _client.CoreV1.ReadNamespacedServiceStatusAsync(_options.ControllerServiceName, _options.ControllerServiceNamespace, cancellationToken: cancellationToken);
if (service.Status?.LoadBalancer?.Ingress is { } loadBalancerIngresses)
{
var status = new V1IngressStatus
{
LoadBalancer = new V1IngressLoadBalancerStatus
{
Ingress = loadBalancerIngresses?.Select(ingress => new V1IngressLoadBalancerIngress
{
Hostname = ingress.Hostname,
Ip = ingress.Ip,
Ports = ingress.Ports?.Select(port => new V1IngressPortStatus
{
Port = port.Port, Protocol = port.Protocol, Error = port.Error
}).ToArray()
}).ToArray()
}
};
var ingresses = _cache.GetIngresses().ToArray();
foreach (var ingress in ingresses)
{
_logger.LogInformation("Updating ingress {IngressClassNamespace}/{IngressClassName} status.", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name);
var ingressStatus = await _client.NetworkingV1.ReadNamespacedIngressStatusAsync(ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken);
ingressStatus.Status = status;
await _client.NetworkingV1.ReplaceNamespacedIngressStatusAsync(ingressStatus, ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken);
_logger.LogInformation("Updated ingress {IngressClassNamespace}/{IngressClassName} status.", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name);
}
}
}
}
================================================
FILE: src/Kubernetes.Controller/Client/V1SecretResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
internal class V1SecretResourceInformer : ResourceInformer
{
public V1SecretResourceInformer(
IKubernetes client,
ResourceSelector selector,
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger)
: base(client, selector, hostApplicationLifetime, logger)
{
}
protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default)
{
return Client.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);
}
protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null)
{
return Client.CoreV1.WatchListSecretForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);
}
}
================================================
FILE: src/Kubernetes.Controller/Client/V1ServiceResourceInformer.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using k8s;
using k8s.Autorest;
using k8s.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace Yarp.Kubernetes.Controller.Client;
internal class V1ServiceResourceInformer : ResourceInformer