diff --git a/.dockerignore b/.dockerignore index 3729ff0cd..eb64b6981 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,10 @@ **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md \ No newline at end of file +README.md +**/ingress.yaml +**/ingress-controller.yaml +**/backend.yaml +**/ingress-sample.yaml +.dotnet +TestResults diff --git a/NuGet.config b/NuGet.config index 3e92cb918..0a0c7d220 100644 --- a/NuGet.config +++ b/NuGet.config @@ -11,6 +11,8 @@ + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index aaac3c5d7..c6c72ee18 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,6 +23,7 @@ trigger: include: - main - release/* + - internal/release/* pr: autoCancel: false diff --git a/docs/docfx/README.md b/docs/docfx/README.md index e9b567354..18644588c 100644 --- a/docs/docfx/README.md +++ b/docs/docfx/README.md @@ -12,8 +12,8 @@ The build will produce a series of HTML files in the `_site` directory. Many of ## Publishing the docs -The docs are automatically built and published by a [GitHub Action](https://github.com/microsoft/reverse-proxy/blob/main/.github/workflows/docfx_build.yml) on every push to `release/docs`. The built `_site` directory is pushed to the `gh-pages` branch and served by [https://microsoft.github.io/reverse-proxy/](https://microsoft.github.io/reverse-proxy/). Maintaining a seperate branch for the released docs allows us to choose when to publish them and with what content, and without modifying the build scripts each release. +The docs are automatically built and published by a [GitHub Action](https://github.com/microsoft/reverse-proxy/blob/main/.github/workflows/docfx_build.yml) on every push to `release/latest`. The built `_site` directory is pushed to the `gh-pages` branch and served by [https://microsoft.github.io/reverse-proxy/](https://microsoft.github.io/reverse-proxy/). Maintaining a seperate branch for the released docs allows us to choose when to publish them and with what content, and without modifying the build scripts each release. -Doc edits for the current public release should go into that release's branch (e.g. `release/1.0.0-preview3`) and merged forward into `main` and `release/docs`. +Doc edits for the current public release should go into that release's branch (e.g. `release/1.0.0-preview3`) and merged forward into `main` and `release/latest`. -When publishing a new product version (e.g. `release/1.0.0-preview4`) `release/latest` should be reset to that position after the docs have been updated. +When publishing a new product version (e.g. `release/1.0.0-preview4`) `release/latest` should be merged to that position after the docs have been updated. diff --git a/docs/docfx/articles/diagnosing-yarp-issues.md b/docs/docfx/articles/diagnosing-yarp-issues.md new file mode 100644 index 000000000..9b09ae5dd --- /dev/null +++ b/docs/docfx/articles/diagnosing-yarp-issues.md @@ -0,0 +1,249 @@ +# Diagnosing YARP-based proxies + +When using a reverse proxy, there is an additional hop from the client to the proxy, and then from the proxy to destination for things to go wrong. This topic should provide some hints and tips for how to debug and diagnose issues when they occur. It assumes that the proxy is already running, and so does not include problems at startup such as configuration errors. + +## Logging + +The first step to being able to tell what is going on with YARP is to turn on [logging](Link to https://docs.microsoft.com/aspnet/core/fundamentals/logging/#configure-logging-1). This is a configuration flag so can be changed on the fly. YARP is implemented as a middleware component for ASP.NET Core, so you need to enable logging for both YARP and ASP.NET to get the complete picture of what is going on. + +By default ASP.NET will log to the console, and the configuration file can be used to control the level of logging. + +```Json + //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" + } + }, +``` + +You want logging infomation from the "Microsoft.AspNetCore.*" and "Yarp.ReverseProxy.*" providers. The example above will emit "Information" level events from both providers to the Console. Changing the level to "Debug" will show additional entries. ASP.NET implements change detection for configuration files, so you can edit the appsettings.json file (or appsettings.development.json if running from Visual Studio) while the project is running and observe changes to the log output. + +### Understanding Log entries + +The logging output is directly tied to the way that ASP.NET Core processes requests. It's important to realize that as middleware, YARP is relying on much of the ASP.NET functionality to process the requests, for example the following is for the processing of a request with "Debug" mode enabled: + +| Level | Log Message | Description | +| ----- | ----------- | ----------- | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Connections[39]
Connection id "0HMCD0JK7K51U" accepted.| Connections are independent of requests, so this is a new connection | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Connections[1]
Connection id "0HMCD0JK7K51U" started. | | +| info | Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/ - - | This is the incomming request to ASP.NET | +| dbug | Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
Wildcard detected, all requests with hosts will be allowed. | My configuation does not tie endpoints to specific hostnames | +| dbug | Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
1 candidate(s) found for the request path '/' | This shows what possible matches there are for the route | +| dbug | Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
Endpoint 'minimumroute' with route pattern '{\*\*catch-all}' is valid for the request path '/' | The mimimum route from YARPs configuration has matched| +| dbug | Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
Request matched endpoint 'minimumroute' | | +| info | Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'minimumroute' | | +| info | Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://www.example.com/ | YARP is proxying the request to example.com | +| info | Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'minimumroute' | | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Connections[9]
Connection id "0HMCD0JK7K51U" completed keep alive response. | The response has finished, but connection can be kept alive. | +| info | Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET http://localhost:5000/ - - - 200 1256 text/html;+charset=utf-8 12.7797ms| The response completed with status code 200, responding with 1256 bytes as text/html in ~13ms. | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[6]
Connection id "0HMCD0JK7K51U" received FIN. | Diagnostic information about the connection to determine who closed it and how cleanly | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Connections[10]
Connection id "0HMCD0JK7K51U" disconnecting. | | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Connections[2]
Connection id "0HMCD0JK7K51U" stopped. | | +| dbug | Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
Connection id "0HMCD0JK7K51U" sending FIN because: "The Socket transport's send loop completed gracefully." | | + +The above gives general information about the request and how it was processed. + +### Using ASP.NET 6 Request Logging + +If running on .NET 6, then ASP.NET includes an additional middleware component that can be used to provide more details about the request and response. The `UseHttpLogging` component can be added to the request pipeline. It will add additional entries to the log detailing the incoming and outgoing request headers. + +``` C# +// This method gets called by the runtime. Use this method to configure the HTTP request +// pipeline that handles requests +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + app.UseHttpLogging(); + // Enable endpoint routing, required for the reverse proxy + app.UseRouting(); + // Register the reverse proxy routes + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); +} +``` + +For example: + +```Console +info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1] + Request: + Protocol: HTTP/1.1 + Method: GET + Scheme: http + PathBase: + Path: / + Accept: */* + Host: localhost:5000 + User-Agent: curl/7.55.1 +info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2] + Response: + StatusCode: 200 + Content-Type: text/html; charset=utf-8 + Date: Tue, 12 Oct 2021 23:29:20 GMT + Server: ECS,(sec/97A5) + Age: 113258 + Cache-Control: [Redacted] + ETag: [Redacted] + Expires: Tue, 19 Oct 2021 23:29:20 GMT + Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT + Vary: [Redacted] + Content-Length: 1256 + X-Cache: [Redacted] +``` + +## Using Telemetry Events + +The [Metrics sample](https://github.com/microsoft/reverse-proxy/tree/main/samples/ReverseProxy.Metrics.Sample) shows how to listen to events from the different providers that collect telemetry as part of YARP. The most important from a diagnostics perspective are: + +* ForwarderTelemetryConsumer +* HttpClientTelemetryConsumer + +To use either of these you create a class implementing an interface, such as IForwarderTelemetryConsumer: + +```C# +public class ForwarderTelemetry : IForwarderTelemetryConsumer +{ + + /// Called before forwarding a request. + public void OnForwarderStart(DateTime timestamp, string destinationPrefix) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnForwarderStart :: Destination prefix: {destinationPrefix}"); + } + + /// Called after forwarding a request. + public void OnForwarderStop(DateTime timestamp, int statusCode) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnForwarderStop :: Status: {statusCode}"); + } + + /// Called before if forwarding the request failed. + public void OnForwarderFailed(DateTime timestamp, ForwarderError error) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnForwarderFailed :: Error: {error.ToString()}"); + } + + /// Called when reaching a given stage of forwarding a request. + public void OnForwarderStage(DateTime timestamp, ForwarderStage stage) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnForwarderStage :: Stage: {stage.ToString()}"); + } + + /// Called periodically while a content transfer is active. + public void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnContentTransferring :: Is request: {isRequest}, Content length: {contentLength}, IOps: {iops}, Read time: {readTime:s\\.fff}, Write time: {writeTime:s\\.fff}"); + } + + /// Called after transferring the request or response content. + public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnContentTransferred :: Is request: {isRequest}, Content length: {contentLength}, IOps: {iops}, Read time: {readTime:s\\.fff}, Write time: {writeTime:s\\.fff}"); + } + + /// Called before forwarding a request. + public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) + { + Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => OnForwarderInvoke:: Cluster id: {clusterId}, Route Id: { routeId}, Destination: {destinationId}"); + } +} +``` + +And then register the class as part of `Configure Services`, for example: + +```C# +public void ConfigureServices(IServiceCollection services) +{ + services.AddTelemetryConsumer(); + + // Add the reverse proxy to capability to the server + var proxyBuilder = services.AddReverseProxy(); + // Initialize the reverse proxy from the "ReverseProxy" section of configuration + proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy")); +} +``` + +Then you will log details on each part of the request, for example: + +```Console +Forwarder Telemetry [06:40:48.186] => OnForwarderInvoke:: Cluster id: minimumcluster, Route Id: minimumroute, Destination: example.com +Forwarder Telemetry [06:41:00.269] => OnForwarderStart :: Destination prefix: http://www.example.com/ +Forwarder Telemetry [06:41:00.298] => OnForwarderStage :: Stage: SendAsyncStart +Forwarder Telemetry [06:41:00.507] => OnForwarderStage :: Stage: SendAsyncStop +Forwarder Telemetry [06:41:00.530] => OnForwarderStage :: Stage: ResponseContentTransferStart +Forwarder Telemetry [06:41:03.655] => OnForwarderStop :: Status: 200 +``` + +The events for Telemetry are fired as they occur, so you can [fish out the HttpContext](https://docs.microsoft.com/aspnet/core/fundamentals/http-context#use-httpcontext-from-custom-components) and the YARP feature from it: + +``` C# +public void ConfigureServices(IServiceCollection services) +{ + services.AddTelemetryConsumer(); + services.AddHttpContextAccessor(); + ... +} + +public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) +{ + var context = new HttpContextAccessor().HttpContext; + var YarpFeature = context.GetReverseProxyFeature(); + + var dests = from d in YarpFeature.AvailableDestinations + select d.Model.Config.Address; + + Console.WriteLine($"Destinations: {string.Join(", ", dests)}"); +} +``` + +## Using custom middleware + +Another way to inspect the state for requests is to insert additional middleware into the request pipeline. You can insert between the other stages to see the state of the request. + +```C# +public void Configure(IApplicationBuilder app) +{ + app.UseRouting(); + app.UseEndpoints(endpoints => + { + // We can customize the proxy pipeline and add/remove/replace steps + endpoints.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(); + }); + }); +} + +public Task MyCustomProxyStep(HttpContext context, Func next) +{ + // Can read data from the request via the context + foreach (var header in context.Request.Headers) + { + Console.WriteLine($"{header.Key}: {header.Value}"); + } + + // The context also stores a ReverseProxyFeature which holds proxy specific data such as the cluster, route and destinations + var proxyFeature = context.GetReverseProxyFeature(); + Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(proxyFeature.Route.Config)); + + // Important - required to move to the next step in the proxy pipeline + return next(); +} +``` + +You can also use [ASP.NET middleware](https://docs.microsoft.com/aspnet/core/fundamentals/middleware/write) within Configure that will enable you to inspect the request before the proxy pipeline. + +> **Note:** The proxy will stream the response from the destination server back to the client, so the response headers and body are not readily accessible via middleware. + +## Using the debugger + +A debugger, such as Visual Studio can be attached to the proxy process. However, unless you have existing middleware, there is not a good place in the app code to break and inspect the state of the request. Therefore the debugger is best used in conjunction with one of the techniques above so that you have distinct places to insert breakpoints etc. diff --git a/docs/docfx/articles/distributed-tracing.md b/docs/docfx/articles/distributed-tracing.md new file mode 100644 index 000000000..2084bdc17 --- /dev/null +++ b/docs/docfx/articles/distributed-tracing.md @@ -0,0 +1,30 @@ + +# Distributed tracing + +As an ASP.NET Core component, YARP can easily integrate into different tracing systems the same as any other web application. +See detailed guides for setting up your application with: +- [OpenTelemetry] or +- [Application Insights] + +.NET 6.0 has built-in configurable support for distributed tracing that YARP takes advantage of to enable such scenarios out-of-the-box. + +## .NET 5.0 and older + +Before 6.0, `SocketsHttpHandler` could not be used with distributed tracing. +When running on .NET 3.1 or 5.0, YARP will copy tracing headers as-is, not accounting for any changes that may have occurred to the trace within the application. + +To get YARP to actively participate, you must use a workaround to manually insert the correct headers. + +The recommended workaround is to: +- Include a [custom `IForwarderHttpClientFactory`][DiagnosticsHandlerFactory] in your project and +- Register it in the DI container + ```c# + #if !NET6_0_OR_GREATER + services.AddSingleton(); + #endif + ``` +The workaround mimics the behavior of the internal `DiagnosticsHandler` class used by `HttpClient`. As such, it automatically works with instrumentation packages from OpenTelemetry or Application Insights. + +[OpenTelemetry]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/docs/trace/getting-started/README.md +[Application Insights]: https://docs.microsoft.com/azure/azure-monitor/app/asp-net-core +[DiagnosticsHandlerFactory]: https://github.com/microsoft/reverse-proxy/blob/main/samples/ReverseProxy.Code.Sample/DiagnosticsHandlerFactory.cs \ No newline at end of file diff --git a/docs/docfx/articles/getting-started.md b/docs/docfx/articles/getting-started.md index 1baaa8e3e..76ab4d986 100644 --- a/docs/docfx/articles/getting-started.md +++ b/docs/docfx/articles/getting-started.md @@ -111,7 +111,7 @@ Or create a new ASP.NET Core web application in Visual Studio 2022, and choose " ```XML - + ``` @@ -158,7 +158,7 @@ You can find out more about the available configuration options by looking at [R "Clusters": { "cluster1": { "Destinations": { - "cluster1/destination1": { + "destination1": { "Address": "https://example.com/" } } diff --git a/docs/docfx/articles/header-guidelines.md b/docs/docfx/articles/header-guidelines.md new file mode 100644 index 000000000..7d5072730 --- /dev/null +++ b/docs/docfx/articles/header-guidelines.md @@ -0,0 +1,71 @@ +--- +uid: header-guidelines +title: Header Guidelines +--- + +# HTTP Headers + +Headers are a very important part of processing HTTP requests and each have their own semantics and considerations. Most headers are proxied by default, though some used to control how the request is delivered are automatically adjusted or removed by the proxy. The connections between the client and the proxy and the proxy and destination are independent, and so headers that affect the connection and transport need to be filtered. Many headers contain information like domain names, paths, or other details that may be affected when a reverse proxy is included in the application architecture. Below is a collection of guidelines about how specific headers might be impacted and what to do about them. + +## YARP header filtering + +YARP automatically removes request and response headers that could impact its ability to forward a request correctly, or that may be used maliciously to bypass features of the proxy. A complete list can be found [here](https://github.com/microsoft/reverse-proxy/blob/v1.0.0-rc.1/src/ReverseProxy/Forwarder/RequestUtilities.cs#L62-L94), with some highlights described below. + +### Connection, KeepAlive, Close + +These headers control how the TCP connection is managed and are removed to avoid impacting the connection on the other side of the proxy. + +### Transfer-Encoding + +This header describes the format of the request or response body on the wire, e.g. 'chunked', and is removed because the format can vary between the internal and external connection. The incoming and outgoing HTTP stacks will add transport headers as needed. + +### TE + +Only the `TE: trailers` header value is allowed through the proxy since it's required for some gRPC implementations. + +### Upgrade + +This is used for protocols like WebSockets. It is removed by default and only added back for specifically supported protocols (WebSockets, SPDY). + +### Proxy-* + +These are headers used with proxies and are not considered appropriate to forward. + +### Alt-Svc + +This response header is used with HTTP/3 upgrades and only applies to the immediate connection. + +### TraceParent, Request-Id, TraceState, Baggage, Correlation-Context + +These headers relate to distributed tracing. They are automatically removed on .NET 6 or later so that the forwarding HttpClient can replace them with updated values. + +## Other Header Guidelines + +### Host + +The Host header indicates which site on the server the request is intended for. This header is removed by default since the host name used publicly by the proxy is likely to differ from the one used by the service behind the proxy. This is configurable using the [RequestHeaderOriginalHost](transforms.md#requestheaderoriginalhost) transform. + +### X-Forwarded-*, Forwarded + +Because a separate connection is used to communicate with the destination, these request headers can be used to forward information about the original connection like the IP, scheme, port, client certificate, etc.. X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Prefix are enabled by default. This information is subject to spoofing attacks so any existing headers on the request are removed and replaced by default. The destination app should be careful about how much trust it places in these values. See [transforms](transforms.md#defaults) for configuring these in the proxy. See the [ASP.NET docs](https://docs.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer) for configuring the destination app to read these headers. + +### X-http-mehtod-override, x-http-method, x-method-override + +Some clients and servers limit which HTTP methods they allow (GET, POST, etc.). These request headers are sometimes used to work around those restrictions. These headers are proxied by default. If in the proxy you want to prevent these bypasses then use the [RequestHeaderRemove](transforms.md#requestheaderremove) transform. + +### Set-Cookie + +This response header may contain fields constraining the path, domain, scheme, etc. where the cookie should be used. Using a reverse proxy may change the effective domain, path, or scheme of a site from the public view. While it would be possible to [rewrite](https://github.com/microsoft/reverse-proxy/issues/1109) response cookies using custom transforms, it's recommended instead to use the Forwarded headers described above to flow the correct values to the destination app so it can generate the correct set-cookie headers. + +### Location + +This response header is used with redirects and may contain a scheme, domain, and path that differ from the public values due to the use of the proxy. While it would be possible to [rewrite](https://github.com/microsoft/reverse-proxy/discussions/466) the Location header using custom transforms, it's recommended instead to use the Forwarded headers described above to flow the correct values to the destination app so it can generate the correct Location headers. + +### Server + +This response header indicates what server technology was used to generate the response (IIS, Kestrel, etc.). This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](transforms.md#responseheaderremove) transform, in which case the proxy's default server header will be used. Suppressing the proxy default server header is server specific, such as for [Kestrel](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.addserverheader#Microsoft_AspNetCore_Server_Kestrel_Core_KestrelServerOptions_AddServerHeader). + +### X-Powered-By + +This response header indicates what web framework was used to generate the response (ASP.NET, etc.). ASP.NET Core does not generate this header but IIS can. This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](transforms.md#responseheaderremove) transform. + diff --git a/docs/docfx/articles/https-tls.md b/docs/docfx/articles/https-tls.md new file mode 100644 index 000000000..1dd4990e3 --- /dev/null +++ b/docs/docfx/articles/https-tls.md @@ -0,0 +1,29 @@ +# HTTPS & TLS + +HTTPS (HTTP over TLS encrypted connections) is the standard way to make HTTP requests on the Internet for security, integrity, and privacy reasons. There are several HTTPS/TLS considerations to account for when using a reverse proxy like YARP. + +## TLS Termination + +YARP is a level 7 HTTP proxy which means that incoming HTTPS/TLS connections are fully decrypted by the proxy so it can process and forward the HTTP requests. This is commonly known as TLS Termination. The outgoing connections to the destination(s) may or may not be encrypted, depending on the configuration provided. + +## TLS tunneling (CONNECT) + +TLS tunneling using the CONNECT method is a feature used to proxy requests without decrypting them. This is _not_ supported by YARP and there are no plans to add it. + +## Configuring incomming connections + +YARP can run on top of all ASP.NET Core servers and configuring HTTPS/TLS for incoming connections is server specific. Check the docs for [Kestrel](https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints#listenoptionsusehttps), [IIS](https://docs.microsoft.com/iis/manage/configuring-security/how-to-set-up-ssl-on-iis), and [Http.Sys](https://docs.microsoft.com/aspnet/core/fundamentals/servers/httpsys#configure-windows-server-1) for configuration details. + +### Advanced TLS filters with Kestrel + +Kestrel supports intercepting incoming connections before the TLS handshake. YARP includes a [TlsFrameHelper](xref:Yarp.ReverseProxy.Utilities.Tls.TlsFrameHelper) API that can parse the raw TLS handshake and enable you to gather custom telemetry or eagerly reject connections. These APIs cannot modify the TLS handshake or decrypt the data stream. See this [example](https://github.com/microsoft/reverse-proxy/blob/v1.0.0-rc.1/testassets/ReverseProxy.Direct/TlsFilter.cs). + +## Configuring outgoing connections + +To enable TLS encryption when communicating with a destination specify the destination address as `https` like `"https://destinationHost"`. See the [configuration docs](config-files.md#configuration-structure) for examples. + +The host name specified in the destination address will be used for the TLS handshake by default, including SNI and server certificate validation. If proxying the [original host header](transforms.md#requestheaderoriginalhost) is enabled, that value will be used for the TLS handshake instead. If a custom host value needs to be used then use the [RequestHeader](transforms.html#requestheader) transform to set the host header. + +Outbound connections to the destinations are handled by HttpClient/SocketsHttpHandler. A different instance and settings can be configured per cluster. Some settings are available in the configuration model, while others can only be configured in code. See the [HttpClient](http-client-config.md) docs for details. + +Destination server certificates need to be trusted by the proxy or custom validation needs to be applied via the [HttpClient](http-client-config.md) configuration. diff --git a/docs/docfx/articles/load-balancing.md b/docs/docfx/articles/load-balancing.md index 44bcac557..15d08a9ec 100644 --- a/docs/docfx/articles/load-balancing.md +++ b/docs/docfx/articles/load-balancing.md @@ -91,7 +91,7 @@ public sealed class LastLoadBalancingPolicy : ILoadBalancingPolicy { public string Name => "Last"; - public DestinationState PickDestination(HttpContext context, IReadOnlyList availableDestinations) + public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { return availableDestinations[^1]; } @@ -103,14 +103,3 @@ services.AddSingleton(); // Set the LoadBalancingPolicy on the cluster cluster.LoadBalancingPolicy = "Last"; ``` - -Other information that may be necessary to decide on a destination, such as cluster configuration, can be accessed from the `HttpContext`: - -```c# -public DestinationState PickDestination(HttpContext context, IReadOnlyList availableDestinations) -{ - var proxyFeature = context.GetReverseProxyFeature(); - var cluster = proxyFeature.Cluster; - // ... -} -``` diff --git a/docs/docfx/articles/toc.yml b/docs/docfx/articles/toc.yml index 281899f47..7ef2bf1bf 100644 --- a/docs/docfx/articles/toc.yml +++ b/docs/docfx/articles/toc.yml @@ -10,6 +10,10 @@ href: direct-forwarding.md - name: HTTP client configuration href: http-client-config.md +- name: HTTPS & TLS + href: https-tls.md +- name: Header Guidelines + href: header-guidelines.md - name: Header Routing href: header-routing.md - name: Authentication and Authorization @@ -26,9 +30,13 @@ href: transforms.md - name: Destinations Health Checks href: dests-health-checks.md +- name: Distributed Tracing + href: distributed-tracing.md - name: gRPC href: grpc.md - name: Service Fabric Integration href: service-fabric-int.md - name: Packages references href: packages-refs.md +- name: Diagnosing proxy issues + href: diagnosing-yarp-issues.md diff --git a/docs/docfx/articles/transforms.md b/docs/docfx/articles/transforms.md index d106de026..d60a2f7f3 100644 --- a/docs/docfx/articles/transforms.md +++ b/docs/docfx/articles/transforms.md @@ -79,10 +79,10 @@ Here is an example of common transforms: "route2" : { "ClusterId": "cluster1", "Match": { - "Path": "/api/{plugin}/stuff/{*remainder}" + "Path": "/api/{plugin}/stuff/{**remainder}" }, "Transforms": [ - { "PathPattern": "/foo/{plugin}/bar/{remainder}" }, + { "PathPattern": "/foo/{plugin}/bar/{**remainder}" }, { "QueryStringParameter": "q", "Append": "plugin" @@ -235,27 +235,27 @@ This will set the request path with the given value. Config: ```JSON -{ "PathPattern": "/my/{plugin}/api/{remainder}" } +{ "PathPattern": "/my/{plugin}/api/{**remainder}" } ``` Code: ```csharp -routeConfig = routeConfig.WithTransformPathRouteValues(pattern: new PathString("/my/{plugin}/api/{remainder}")); +routeConfig = routeConfig.WithTransformPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); ``` ```C# -transformBuilderContext.AddPathRouteValues(pattern: new PathString("/my/{plugin}/api/{remainder}")); +transformBuilderContext.AddPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); ``` -This will set the request path with the given value and replace any `{}` segments with the associated route value. `{}` segments without a matching route value are removed. See ASP.NET Core's [routing docs](https://docs.microsoft.com/aspnet/core/fundamentals/routing#route-template-reference) for more information about route templates. +This will set the request path with the given value and replace any `{}` segments with the associated route value. `{}` segments without a matching route value are removed. The final `{}` segment can be marked as `{**remainder}` to indicate this is a catch-all segment that may contain multiple path segments. See ASP.NET Core's [routing docs](https://docs.microsoft.com/aspnet/core/fundamentals/routing#route-template-reference) for more information about route templates. Example: | Step | Value | |------|-------| -| Route definition | `/api/{plugin}/stuff/{*remainder}` | +| Route definition | `/api/{plugin}/stuff/{**remainder}` | | Request path | `/api/v1/stuff/more/stuff` | | Plugin value | `v1` | | Remainder value | `more/stuff` | -| PathPattern | `/my/{plugin}/api/{remainder}` | +| PathPattern | `/my/{plugin}/api/{**remainder}` | | Result | `/my/v1/api/more/stuff` | ### QueryValueParameter @@ -688,6 +688,10 @@ X-Client-Cert: SSdtIGEgY2VydGlmaWNhdGU... ``` As the inbound and outbound connections are independent, there needs to be a way to pass any inbound client certificate to the destination server. This transform causes the client certificate taken from `HttpContext.Connection.ClientCertificate` to be Base64 encoded and set as the value for the given header name. The destination server may need that certificate to authenticate the client. There is no standard that defines this header and implementations vary, check your destination server for support. +Servers do minimal validation on the incoming client certificate by default. The certificate should be validated either in the proxy or the destination, see the [client certificate auth](https://docs.microsoft.com/aspnet/core/security/authentication/certauth) docs for details. + +This transform will only apply if the client certificate is already present on the connection. See the [optional certs doc](https://docs.microsoft.com/aspnet/core/security/authentication/certauth#optional-client-certificates) if it needs to be requested from the client on a per-route basis. + ## Response and Response Trailers All response headers and trailers are copied from the proxied response to the outgoing client response by default. Response and response trailer transforms may specify if they should be applied only for successful responses or for all responses. diff --git a/docs/operations/BackportingToPreview.md b/docs/operations/BackportingToPreview.md index d54e0b01d..d94131bbf 100644 --- a/docs/operations/BackportingToPreview.md +++ b/docs/operations/BackportingToPreview.md @@ -18,3 +18,17 @@ Backporting changes is very similar to a regular release. Changes are made on th - `git tag v1.0.0-previewX.build.d` - `git push upstream --tags` - Create a new [release](https://github.com/microsoft/reverse-proxy/releases). + +# Internal fixes + +Issues with significant security or disclosure concerns need to be fixed privately first. All of this work will happen on the internal Azdo repo and be merged to the public github repo at the time of disclosure. + +- Make a separate clone of https://dnceng@dev.azure.com/dnceng/internal/_git/microsoft-reverse-proxy to avoid accidentally pushing to the public repo. +- Create a branch named `internal/release/{version being patched}` starting from the tagged commit of the prior release. +- Update versioning as needed. +- Create a feature branch, fix the issue, and send a PR using Azdo. +- Once approved and merged, the `internal/release/{version}` branch should build automatically and publish to the `.NET 6 Internal` channel, visible at https://dev.azure.com/dnceng/internal/_packaging?_a=feed&feed=dotnet6-internal. This is configured using the `darc` tool. +- [Release the build](https://github.com/microsoft/reverse-proxy/blob/main/docs/operations/Release.md#release-the-build) +- Tag the commit and push it to the public repo +- Cherry pick the changes to public main as needed. +- Finish the standard release checklist. diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 93b6beafb..71a685417 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -3,13 +3,13 @@ - + https://github.com/dotnet/arcade - c5a300999a6ab2792108190118280da36fb1991e + 0558f85d950fee2838bf02b9ba1f20d67f00b504 - + https://github.com/dotnet/arcade - c5a300999a6ab2792108190118280da36fb1991e + 0558f85d950fee2838bf02b9ba1f20d67f00b504 diff --git a/eng/Versions.props b/eng/Versions.props index 2cba26fff..84c8b6282 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -7,6 +7,7 @@ That ensures that if we get up to preview 10, it doesn't sort as between preview 1 and preview 2! See https://semver.org/spec/v2.0.0.html for details. --> - rc.1 + rtw + release diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 index e8ab29afe..4011d324e 100644 --- a/eng/common/post-build/sourcelink-validation.ps1 +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -22,6 +22,11 @@ $RetryWaitTimeInSeconds = 30 # Wait time between check for system load $SecondsBetweenLoadChecks = 10 +if (!$InputPath -or !(Test-Path $InputPath)){ + Write-Host "No files to validate." + ExitWithExitCode 0 +} + $ValidatePackage = { param( [string] $PackagePath # Full path to a Symbols.NuGet package diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 index a5af041ba..a4a92efbe 100644 --- a/eng/common/post-build/symbols-validation.ps1 +++ b/eng/common/post-build/symbols-validation.ps1 @@ -4,9 +4,11 @@ param( [Parameter(Mandatory = $true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use [Parameter(Mandatory = $false)][switch] $CheckForWindowsPdbs, # If we should check for the existence of windows pdbs in addition to portable PDBs [Parameter(Mandatory = $false)][switch] $ContinueOnError, # If we should keep checking symbols after an error - [Parameter(Mandatory = $false)][switch] $Clean # Clean extracted symbols directory after checking symbols + [Parameter(Mandatory = $false)][switch] $Clean, # Clean extracted symbols directory after checking symbols + [Parameter(Mandatory = $false)][string] $SymbolExclusionFile # Exclude the symbols in the file from publishing to symbol server ) +. $PSScriptRoot\..\tools.ps1 # Maximum number of jobs to run in parallel $MaxParallelJobs = 16 @@ -25,14 +27,28 @@ if ($CheckForWindowsPdbs) { $WindowsPdbVerificationParam = "--windows-pdbs" } +$ExclusionSet = New-Object System.Collections.Generic.HashSet[string]; + +if (!$InputPath -or !(Test-Path $InputPath)){ + Write-Host "No symbols to validate." + ExitWithExitCode 0 +} + +#Check if the path exists +if ($SymbolExclusionFile -and (Test-Path $SymbolExclusionFile)){ + [string[]]$Exclusions = Get-Content "$SymbolExclusionFile" + $Exclusions | foreach { if($_ -and $_.Trim()){$ExclusionSet.Add($_)} } +} +else{ + Write-Host "Symbol Exclusion file does not exists. No symbols to exclude." +} + $CountMissingSymbols = { param( [string] $PackagePath, # Path to a NuGet package [string] $WindowsPdbVerificationParam # If we should check for the existence of windows pdbs in addition to portable PDBs ) - . $using:PSScriptRoot\..\tools.ps1 - Add-Type -AssemblyName System.IO.Compression.FileSystem Write-Host "Validating $PackagePath " @@ -142,37 +158,44 @@ $CountMissingSymbols = { return $null } - $FileGuid = New-Guid - $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid - - $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault ` - -FullPath $FileName ` - -TargetServerParam '--microsoft-symbol-server' ` - -SymbolsPath "$ExpandedSymbolsPath-msdl" ` - -WindowsPdbVerificationParam $WindowsPdbVerificationParam - $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault ` - -FullPath $FileName ` - -TargetServerParam '--internal-server' ` - -SymbolsPath "$ExpandedSymbolsPath-symweb" ` - -WindowsPdbVerificationParam $WindowsPdbVerificationParam - - Write-Host -NoNewLine "`t Checking file " $FileName "... " - - if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { - Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" + $FileRelativePath = $FileName.Replace("$ExtractPath\", "") + if (($($using:ExclusionSet) -ne $null) -and ($($using:ExclusionSet).Contains($FileRelativePath) -or ($($using:ExclusionSet).Contains($FileRelativePath.Replace("\", "/"))))){ + Write-Host "Skipping $FileName from symbol validation" } - else { - $MissingSymbols++ - if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { - Write-Host 'No symbols found on MSDL or SymWeb!' + else { + $FileGuid = New-Guid + $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid + + $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--microsoft-symbol-server' ` + -SymbolsPath "$ExpandedSymbolsPath-msdl" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--internal-server' ` + -SymbolsPath "$ExpandedSymbolsPath-symweb" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + + Write-Host -NoNewLine "`t Checking file " $FileName "... " + + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" } else { - if ($SymbolsOnMSDL -eq $null) { - Write-Host 'No symbols found on MSDL!' + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host 'No symbols found on MSDL or SymWeb!' } else { - Write-Host 'No symbols found on SymWeb!' + if ($SymbolsOnMSDL -eq $null) { + Write-Host 'No symbols found on MSDL!' + } + else { + Write-Host 'No symbols found on SymWeb!' + } } } } diff --git a/eng/common/sdl/configure-sdl-tool.ps1 b/eng/common/sdl/configure-sdl-tool.ps1 index 4999c3070..8a68fc24b 100644 --- a/eng/common/sdl/configure-sdl-tool.ps1 +++ b/eng/common/sdl/configure-sdl-tool.ps1 @@ -69,13 +69,13 @@ try { # For some tools, add default and automatic args. if ($tool.Name -eq 'credscan') { if ($targetDirectory) { - $tool.Args += "TargetDirectory < $TargetDirectory" + $tool.Args += "`"TargetDirectory < $TargetDirectory`"" } - $tool.Args += "OutputType < pre" + $tool.Args += "`"OutputType < pre`"" $tool.Args += $CrScanAdditionalRunConfigParams } elseif ($tool.Name -eq 'policheck') { if ($targetDirectory) { - $tool.Args += "Target < $TargetDirectory" + $tool.Args += "`"Target < $TargetDirectory`"" } $tool.Args += $PoliCheckAdditionalRunConfigParams } diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml index 69eb67849..3aafc82e4 100644 --- a/eng/common/templates/job/execute-sdl.yml +++ b/eng/common/templates/job/execute-sdl.yml @@ -60,11 +60,7 @@ jobs: - name: GuardianPackagesConfigFile value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config pool: - # To extract archives (.tar.gz, .zip), we need access to "tar", added in Windows 10/2019. - ${{ if eq(parameters.extractArchiveArtifacts, 'false') }}: - name: Hosted VS2017 - ${{ if ne(parameters.extractArchiveArtifacts, 'false') }}: - vmImage: windows-2019 + vmImage: windows-2019 steps: - checkout: self clean: true diff --git a/eng/common/templates/job/onelocbuild.yml b/eng/common/templates/job/onelocbuild.yml index e8bc77d2e..c4fc18b3e 100644 --- a/eng/common/templates/job/onelocbuild.yml +++ b/eng/common/templates/job/onelocbuild.yml @@ -4,7 +4,7 @@ parameters: # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool pool: - vmImage: vs2017-win2016 + vmImage: 'windows-2019' CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) @@ -12,6 +12,7 @@ parameters: SourcesDirectory: $(Build.SourcesDirectory) CreatePr: true AutoCompletePr: false + ReusePr: true UseLfLineEndings: true UseCheckedInLocProjectJson: false LanguageSet: VS_Main_Languages @@ -64,6 +65,8 @@ jobs: ${{ if eq(parameters.CreatePr, true) }}: isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: diff --git a/eng/common/templates/job/source-index-stage1.yml b/eng/common/templates/job/source-index-stage1.yml index 1cc0c29e4..ae85a99a8 100644 --- a/eng/common/templates/job/source-index-stage1.yml +++ b/eng/common/templates/job/source-index-stage1.yml @@ -6,7 +6,7 @@ parameters: preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog pool: - vmImage: vs2017-win2016 + vmImage: 'windows-2019' condition: '' dependsOn: '' diff --git a/eng/common/templates/jobs/jobs.yml b/eng/common/templates/jobs/jobs.yml index a1f8fce96..8dd1fdbd1 100644 --- a/eng/common/templates/jobs/jobs.yml +++ b/eng/common/templates/jobs/jobs.yml @@ -83,7 +83,7 @@ jobs: - ${{ if eq(parameters.enableSourceBuild, true) }}: - Source_Build_Complete pool: - vmImage: vs2017-win2016 + vmImage: 'windows-2019' runAsPublic: ${{ parameters.runAsPublic }} publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} @@ -96,4 +96,4 @@ jobs: dependsOn: - Asset_Registry_Publish pool: - vmImage: vs2017-win2016 + vmImage: 'windows-2019' diff --git a/global.json b/global.json index 4a69201be..8ec577c48 100644 --- a/global.json +++ b/global.json @@ -16,7 +16,7 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.21518.1", - "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.21518.1" + "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.21529.1", + "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.21529.1" } } diff --git a/reverse-proxy.sln b/reverse-proxy.sln index c7abdab1a..48afadcec 100644 --- a/reverse-proxy.sln +++ b/reverse-proxy.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.161 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31804.322 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3E275568-3767-43AC-B51B-FA75C0B5CE91}" ProjectSection(SolutionItems) = preProject @@ -104,6 +104,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "backend", "samples\Kuberene EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Minimal.Sample", "samples\ReverseProxy.Minimal.Sample\ReverseProxy.Minimal.Sample.csproj", "{AA34BE13-7193-4036-A886-A7EE6CD36940}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Kubernetes.UnitTests", "src\OperatorFramework\test\UnitTests\Microsoft.Kubernetes.UnitTests.csproj", "{D6599484-5A3F-471C-87B0-7014C56F14CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -408,6 +410,14 @@ Global {AA34BE13-7193-4036-A886-A7EE6CD36940}.Release|Any CPU.Build.0 = Release|Any CPU {AA34BE13-7193-4036-A886-A7EE6CD36940}.Release|x64.ActiveCfg = Release|Any CPU {AA34BE13-7193-4036-A886-A7EE6CD36940}.Release|x64.Build.0 = Release|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Debug|x64.Build.0 = Debug|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Release|Any CPU.Build.0 = Release|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Release|x64.ActiveCfg = Release|Any CPU + {D6599484-5A3F-471C-87B0-7014C56F14CE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -455,6 +465,7 @@ Global {84B920E2-A501-457D-8F1A-9CC7AF8B5F2D} = {04E87669-7E7B-4217-83A4-EF80B534D14B} {EB5663B9-31BA-4930-A302-4B87B8DEB74A} = {32B6B967-6EAF-42A1-9FD6-A59F752FF76B} {AA34BE13-7193-4036-A886-A7EE6CD36940} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} + {D6599484-5A3F-471C-87B0-7014C56F14CE} = {E96BB4D7-EECC-4A78-BC7D-E167663FD6F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {31F6924A-E427-4830-96E9-B47CEB7BFE78} diff --git a/samples/BasicYarpSample/BasicYarpSample.csproj b/samples/BasicYarpSample/BasicYarpSample.csproj index 9a70510a8..8c749a5ca 100644 --- a/samples/BasicYarpSample/BasicYarpSample.csproj +++ b/samples/BasicYarpSample/BasicYarpSample.csproj @@ -6,7 +6,7 @@ - + diff --git a/samples/BasicYarpSample/Startup.cs b/samples/BasicYarpSample/Startup.cs index b589c2db6..63f84d795 100644 --- a/samples/BasicYarpSample/Startup.cs +++ b/samples/BasicYarpSample/Startup.cs @@ -21,7 +21,7 @@ public Startup(IConfiguration configuration) // the web application via services in the DI container. public void ConfigureServices(IServiceCollection services) { - // Add the reverse proxy to capability to the server + // Add the reverse proxy capability to the server var proxyBuilder = services.AddReverseProxy(); // Initialize the reverse proxy from the "ReverseProxy" section of configuration proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy")); diff --git a/samples/KuberenetesIngress.Sample/Ingress/Dockerfile b/samples/KuberenetesIngress.Sample/Ingress/Dockerfile index be8e50f22..705fcaea0 100644 --- a/samples/KuberenetesIngress.Sample/Ingress/Dockerfile +++ b/samples/KuberenetesIngress.Sample/Ingress/Dockerfile @@ -1,22 +1,33 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS publish +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS publish WORKDIR /src -COPY ["src/OperatorFramework/src/Controller", "OperatorFramework/src/Controller"] -COPY ["src/OperatorFramework/src/Core", "OperatorFramework/src/Core"] -COPY ["src/ReverseProxy", "ReverseProxy"] -COPY ["src/ReverseProxy.Kubernetes.Protocol", "ReverseProxy.Kubernetes.Protocol"] -WORKDIR /samples -COPY ["samples/KuberenetesIngress.Sample/Ingress", "KuberenetesIngress.Sample/Ingress"] +# Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds) +COPY ["samples/KuberenetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj", "samples/KuberenetesIngress.Sample/Ingress/"] +COPY ["src/OperatorFramework/src/Controller/Microsoft.Kubernetes.Controller.csproj", "src/OperatorFramework/src/Controller/"] +COPY ["src/OperatorFramework/src/Core/Microsoft.Kubernetes.Core.csproj", "src/OperatorFramework/src/Core/"] +COPY ["src/ReverseProxy/Yarp.ReverseProxy.csproj", "src/ReverseProxy/"] +COPY ["src/Kubernetes.Protocol/Yarp.Kubernetes.Protocol.csproj", "src/Kubernetes.Protocol/"] +COPY ["src/Directory.Build.props", "src/"] +COPY ["Directory.Build.*", ""] +COPY ["global.json", ""] +COPY ["NuGet.config", ""] -WORKDIR KuberenetesIngress.Sample/Ingress/ -RUN dotnet publish -c Release -o /app/publish -f net5.0 +# Build a cache layer with all of the nuget packages +RUN dotnet restore samples/KuberenetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj + +# Copy the remaining source files +WORKDIR /src +COPY . . + +WORKDIR /src/samples/KuberenetesIngress.Sample/Ingress/ +RUN dotnet publish -c Release --no-restore -o /app/publish -f net6.0 FROM base AS final WORKDIR /app diff --git a/samples/KuberenetesIngress.Sample/Ingress/ingress-controller.yaml b/samples/KuberenetesIngress.Sample/Ingress/ingress-controller.yaml index f2fc9f18c..8f591be64 100644 --- a/samples/KuberenetesIngress.Sample/Ingress/ingress-controller.yaml +++ b/samples/KuberenetesIngress.Sample/Ingress/ingress-controller.yaml @@ -124,7 +124,7 @@ spec: spec: containers: - name: yarp-controller - imagePullPolicy: Always + imagePullPolicy: IfNotPresent image: /yarp-controller: ports: - containerPort: 8000 diff --git a/samples/KuberenetesIngress.Sample/Ingress/ingress.yaml b/samples/KuberenetesIngress.Sample/Ingress/ingress.yaml index 2a334a800..269f064cd 100644 --- a/samples/KuberenetesIngress.Sample/Ingress/ingress.yaml +++ b/samples/KuberenetesIngress.Sample/Ingress/ingress.yaml @@ -36,7 +36,7 @@ spec: spec: containers: - name: yarp-proxy - imagePullPolicy: Always + imagePullPolicy: IfNotPresent image: /yarp: ports: - containerPort: 8000 diff --git a/samples/KuberenetesIngress.Sample/backend/Dockerfile b/samples/KuberenetesIngress.Sample/backend/Dockerfile index 0250ceff9..26ba98703 100644 --- a/samples/KuberenetesIngress.Sample/backend/Dockerfile +++ b/samples/KuberenetesIngress.Sample/backend/Dockerfile @@ -5,10 +5,11 @@ WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS publish -WORKDIR /src -COPY . backend -WORKDIR "backend" +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS publish +WORKDIR /samples +COPY ["samples/KuberenetesIngress.Sample/backend", "KuberenetesIngress.Sample/backend"] + +WORKDIR KuberenetesIngress.Sample/backend RUN dotnet publish -c Release -o /app/publish -f net5.0 FROM base AS final diff --git a/samples/KuberenetesIngress.Sample/backend/backend.yaml b/samples/KuberenetesIngress.Sample/backend/backend.yaml index c89b14d50..2f28eaf0c 100644 --- a/samples/KuberenetesIngress.Sample/backend/backend.yaml +++ b/samples/KuberenetesIngress.Sample/backend/backend.yaml @@ -15,6 +15,7 @@ spec: containers: - name: backend image: /backend: + imagePullPolicy: IfNotPresent ports: - containerPort: 80 --- diff --git a/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/Properties/launchSettings.json b/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/Properties/launchSettings.json index 870f99002..df6289235 100644 --- a/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/Properties/launchSettings.json +++ b/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Metrics.Promethius.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/ReverseProxy.Metrics.Promethius.Sample.csproj b/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/ReverseProxy.Metrics.Promethius.Sample.csproj index 14d2b0921..87215740e 100644 --- a/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/ReverseProxy.Metrics.Promethius.Sample.csproj +++ b/samples/Prometheus/ReverseProxy.Metrics-Promethius.Sample/ReverseProxy.Metrics.Promethius.Sample.csproj @@ -13,7 +13,7 @@ - + diff --git a/samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json index 870f99002..7e7ba00cf 100644 --- a/samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Auth.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj b/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj index 55e64e23f..732600413 100644 --- a/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj +++ b/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Code.Sample/DiagnosticsHandlerFactory.cs b/samples/ReverseProxy.Code.Sample/DiagnosticsHandlerFactory.cs new file mode 100644 index 000000000..0ad78f925 --- /dev/null +++ b/samples/ReverseProxy.Code.Sample/DiagnosticsHandlerFactory.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// These classes are a workaround for the lack of distributed tracing support in SocketsHttpHandler before .NET 6.0 +#if !NET6_0_OR_GREATER +#nullable enable + +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Yarp.ReverseProxy.Forwarder +{ + /// + /// A compatibility workaround used to enable distributed tracing support for YARP when running .NET 3.1 or 5.0 + /// + internal sealed class DiagnosticsHandlerFactory : ForwarderHttpClientFactory + { + protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) + { + handler = base.WrapHandler(context, handler); + return new DiagnosticsHandler(handler); + } + + // A modified copy of DiagnosticsHandler based on the internal version that ships with .NET 5.0 + // https://github.com/dotnet/runtime/blob/release/5.0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs + private sealed class DiagnosticsHandler : DelegatingHandler + { + private static readonly DiagnosticListener s_diagnosticListener = new("HttpHandlerDiagnosticListener"); + + private const string RequestIdHeaderName = "Request-Id"; + private const string CorrelationContextHeaderName = "Correlation-Context"; + private const string BaggageHeaderName = "baggage"; + private const string TraceParentHeaderName = "traceparent"; + private const string TraceStateHeaderName = "tracestate"; + private const string HeaderNameToUseForBaggage = CorrelationContextHeaderName; // Feel free to change this to BaggageHeaderName + + private const string ExceptionEventName = "System.Net.Http.Exception"; + private const string ActivityName = "System.Net.Http.HttpRequestOut"; + private const string ActivityStartName = ActivityName + ".Start"; + private const string ActivityStopName = ActivityName + ".Stop"; + + public DiagnosticsHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return ShouldLogDiagnostics(request, out var activity) + ? SendWithDiagnosticsAsync(request, activity, cancellationToken) + : base.SendAsync(request, cancellationToken); + } + + private static bool ShouldLogDiagnostics(HttpRequestMessage request, out Activity? activity) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + RemoveDistributedContextHeaders(request.Headers); + + var diagnosticListenerEnabled = s_diagnosticListener.IsEnabled(); + + if (Activity.Current is not null || (diagnosticListenerEnabled && s_diagnosticListener.IsEnabled(ActivityName, request))) + { + // If a diagnostics listener is enabled for the Activity, always create one + activity = new Activity(ActivityName); + + activity.Start(); + + if (diagnosticListenerEnabled && s_diagnosticListener.IsEnabled(ActivityStartName)) + { + s_diagnosticListener.Write(ActivityStartName, new ActivityStartData(request)); + } + + InjectHeaders(activity, request.Headers); + + return true; + } + else + { + activity = null; + + // There is no Activity, but we may still want to use the instrumented SendWithDiagnosticsAsync if diagnostic listeners are interested in other events + return diagnosticListenerEnabled; + } + } + + private static void RemoveDistributedContextHeaders(HttpHeaders headers) + { + // Match YARP's .NET 6.0+ behavior of removing all of these headers + // https://github.com/microsoft/reverse-proxy/blob/ff769d6c75cdcf56848a5da29990bf9df541aafe/src/ReverseProxy/Forwarder/RequestUtilities.cs#L86-L93 + headers.Remove(RequestIdHeaderName); + headers.Remove(CorrelationContextHeaderName); + headers.Remove(BaggageHeaderName); + headers.Remove(TraceParentHeaderName); + headers.Remove(TraceStateHeaderName); + } + + private static void InjectHeaders(Activity activity, HttpHeaders headers) + { + if (activity.IdFormat == ActivityIdFormat.W3C) + { + headers.TryAddWithoutValidation(TraceParentHeaderName, activity.Id); + if (activity.TraceStateString is { } traceStateString) + { + headers.TryAddWithoutValidation(TraceStateHeaderName, traceStateString); + } + } + else + { + headers.TryAddWithoutValidation(RequestIdHeaderName, activity.Id); + } + + using var e = activity.Baggage.GetEnumerator(); + if (e.MoveNext()) + { + var baggage = new StringBuilder(); + do + { + var item = e.Current; + baggage.Append(Uri.EscapeDataString(item.Key)); + baggage.Append('='); + baggage.Append(Uri.EscapeDataString(item.Value ?? string.Empty)); + baggage.Append(", "); + } + while (e.MoveNext()); + + baggage.Length -= 2; // Account for the last ", " + + headers.TryAddWithoutValidation(HeaderNameToUseForBaggage, baggage.ToString()); + } + } + + private async Task SendWithDiagnosticsAsync(HttpRequestMessage request, Activity? activity, CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + var taskStatus = TaskStatus.RanToCompletion; + try + { + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + return response; + } + catch (OperationCanceledException) + { + taskStatus = TaskStatus.Canceled; + throw; + } + catch (Exception ex) + { + if (s_diagnosticListener.IsEnabled(ExceptionEventName)) + { + s_diagnosticListener.Write(ExceptionEventName, new ExceptionData(ex, request)); + } + + taskStatus = TaskStatus.Faulted; + throw; + } + finally + { + if (activity is not null) + { + activity.SetEndTime(DateTime.UtcNow); + + if (s_diagnosticListener.IsEnabled(ActivityStopName)) + { + s_diagnosticListener.Write(ActivityStopName, new ActivityStopData(response, request, taskStatus)); + } + + activity.Stop(); + } + } + } + + private sealed class ActivityStartData + { + public ActivityStartData(HttpRequestMessage request) + { + Request = request; + } + + public HttpRequestMessage Request { get; } + + public override string ToString() => $"{{ {nameof(Request)} = {Request} }}"; + } + + private sealed class ActivityStopData + { + public ActivityStopData(HttpResponseMessage? response, HttpRequestMessage request, TaskStatus requestTaskStatus) + { + Response = response; + Request = request; + RequestTaskStatus = requestTaskStatus; + } + + public HttpResponseMessage? Response { get; } + public HttpRequestMessage Request { get; } + public TaskStatus RequestTaskStatus { get; } + + public override string ToString() => $"{{ {nameof(Response)} = {Response}, {nameof(Request)} = {Request}, {nameof(RequestTaskStatus)} = {RequestTaskStatus} }}"; + } + + private sealed class ExceptionData + { + public ExceptionData(Exception exception, HttpRequestMessage request) + { + Exception = exception; + Request = request; + } + + public Exception Exception { get; } + public HttpRequestMessage Request { get; } + + public override string ToString() => $"{{ {nameof(Exception)} = {Exception}, {nameof(Request)} = {Request} }}"; + } + } + } +} +#endif diff --git a/samples/ReverseProxy.Code.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Code.Sample/Properties/launchSettings.json index 870f99002..cb41e9500 100644 --- a/samples/ReverseProxy.Code.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Code.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Code.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj b/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj index 9a0e207bd..d83d6db00 100644 --- a/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj +++ b/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Code.Sample/Startup.cs b/samples/ReverseProxy.Code.Sample/Startup.cs index 1c1d897ab..acf5e3dab 100644 --- a/samples/ReverseProxy.Code.Sample/Startup.cs +++ b/samples/ReverseProxy.Code.Sample/Startup.cs @@ -8,9 +8,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; - namespace Yarp.Sample { /// @@ -27,6 +27,11 @@ public class Startup /// public void ConfigureServices(IServiceCollection services) { +#if !NET6_0_OR_GREATER + // Workaround the lack of distributed tracing support in SocketsHttpHandler before .NET 6.0 + services.AddSingleton(); +#endif + // Specify a custom proxy config provider, in this case defined in InMemoryConfigProvider.cs // Programatically creating route and cluster configs. This allows loading the data from an arbitrary source. services.AddReverseProxy() diff --git a/samples/ReverseProxy.Config.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Config.Sample/Properties/launchSettings.json index 870f99002..eec1c8f59 100644 --- a/samples/ReverseProxy.Config.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Config.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Config.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj b/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj index 9a0e207bd..d83d6db00 100644 --- a/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj +++ b/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Config.Sample/appsettings.json b/samples/ReverseProxy.Config.Sample/appsettings.json index 4afbb1c1f..152c3f837 100644 --- a/samples/ReverseProxy.Config.Sample/appsettings.json +++ b/samples/ReverseProxy.Config.Sample/appsettings.json @@ -27,7 +27,7 @@ } }, "allRouteProps": { - // matches /something/* and routes to "allclusterprops" + // matches /download/* and routes to "allClusterProps" "ClusterId": "allClusterProps", // Name of one of the clusters "Order": 0, // Lower numbers have higher precidence, default is 0 "Authorization Policy": "Anonymous", // Name of the policy or "Default", "Anonymous" @@ -107,7 +107,7 @@ }, "HttpClient": { // Configuration of HttpClient instance used to contact destinations "SSLProtocols": "Tls13", - "DangerousAcceptAnyServerCertificate": false, // Disables destination cert validation + "DangerousAcceptAnyServerCertificate": true, // Disables destination cert validation /* --Disabled in this sample as it requires certs to be present-- "ClientCertificate": { // Specifies a client certificate to be used // From a file, use... diff --git a/samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json b/samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json index bdb5442ca..166092fba 100644 --- a/samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.ConfigFilter.Sample": { "commandName": "Project", "environmentVariables": { "Key": "Value", @@ -24,4 +24,4 @@ } } } -} \ No newline at end of file +} diff --git a/samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj b/samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj index 9a0e207bd..d83d6db00 100644 --- a/samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj +++ b/samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json index 870f99002..10c10c9bf 100644 --- a/samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Direct.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj b/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj index 9a0e207bd..d83d6db00 100644 --- a/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj +++ b/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Direct.Sample/Startup.cs b/samples/ReverseProxy.Direct.Sample/Startup.cs index c78f42729..f72760131 100644 --- a/samples/ReverseProxy.Direct.Sample/Startup.cs +++ b/samples/ReverseProxy.Direct.Sample/Startup.cs @@ -58,8 +58,8 @@ public void Configure(IApplicationBuilder app, IHttpForwarder forwarder) queryContext.Collection.Remove("param1"); queryContext.Collection["area"] = "xx2"; - // Assign the custom uri. Be careful about extra slashes when concatenating here. - proxyRequest.RequestUri = new Uri("https://example.com" + context.Request.Path + queryContext.QueryString); + // 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; @@ -81,8 +81,8 @@ public void Configure(IApplicationBuilder app, IHttpForwarder forwarder) endpoints.Map("/{**catch-all}", async httpContext => { var error = await forwarder.SendAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer); - // Check if the proxy operation was successful - if (error != ForwarderError.None) + // Check if the proxy operation was successful + if (error != ForwarderError.None) { var errorFeature = httpContext.Features.Get(); var exception = errorFeature.Exception; @@ -118,8 +118,8 @@ public override async ValueTask TransformRequestAsync(HttpContext httpContext, H queryContext.Collection.Remove("param1"); queryContext.Collection["area"] = "xx2"; - // Assign the custom uri. Be careful about extra slashes when concatenating here. - proxyRequest.RequestUri = new Uri(destinationPrefix + httpContext.Request.Path + queryContext.QueryString); + // 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; diff --git a/samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs b/samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs index 7db9556c5..71db2419d 100644 --- a/samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs +++ b/samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs @@ -24,7 +24,6 @@ public void OnForwarderStop(DateTime timestamp, int statusCode) public void OnForwarderFailed(DateTime timestamp, ForwarderError error) { var metrics = PerRequestMetrics.Current; - metrics.ProxyStopOffset = metrics.CalcOffset(timestamp); metrics.Error = error; } diff --git a/samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs b/samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs index c76322bdf..10e75614f 100644 --- a/samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs +++ b/samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs @@ -18,11 +18,7 @@ public void OnRequestStart(DateTime timestamp, string scheme, string host, int p public void OnRequestStop(DateTime timestamp) { var metrics = PerRequestMetrics.Current; - metrics.HttpRequestContentStopOffset = metrics.CalcOffset(timestamp); - } - - public void OnRequestFailed(DateTime timestamp) - { + metrics.HttpRequestStopOffset = metrics.CalcOffset(timestamp); } public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) diff --git a/samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs b/samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs index 8c9c488ff..3ecce03af 100644 --- a/samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs +++ b/samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs @@ -38,6 +38,7 @@ private PerRequestMetrics() { } public float HttpResponseHeadersStopOffset { get; set; } public float HttpResponseContentStopOffset { get; set; } + public float HttpRequestStopOffset { get; set; } public float ProxyStopOffset { get; set; } //Info about the request diff --git a/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json index 870f99002..fc0628f69 100644 --- a/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Metrics.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj b/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj index 4d095c75d..a15feefa6 100644 --- a/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj +++ b/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.Minimal.Sample/ReverseProxy.Minimal.Sample.csproj b/samples/ReverseProxy.Minimal.Sample/ReverseProxy.Minimal.Sample.csproj index 77bf4b3fb..c43b928fa 100644 --- a/samples/ReverseProxy.Minimal.Sample/ReverseProxy.Minimal.Sample.csproj +++ b/samples/ReverseProxy.Minimal.Sample/ReverseProxy.Minimal.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/ReverseProxy.ServiceFabric.Sample/Properties/launchSettings.json b/samples/ReverseProxy.ServiceFabric.Sample/Properties/launchSettings.json index 870f99002..fc34a3e1f 100644 --- a/samples/ReverseProxy.ServiceFabric.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.ServiceFabric.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.ServiceFabric.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json index 870f99002..6331ee6fb 100644 --- a/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json +++ b/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Transforms.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj b/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj index 9a0e207bd..d83d6db00 100644 --- a/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj +++ b/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/SampleServer/Properties/launchSettings.json b/samples/SampleServer/Properties/launchSettings.json index 4a363ab66..49de4c768 100644 --- a/samples/SampleServer/Properties/launchSettings.json +++ b/samples/SampleServer/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "ReverseProxy.Sample": { + "SampleServer": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 537f406a1..0b0ae9463 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,5 +6,6 @@ true true + true diff --git a/src/Kubernetes.Controller/Dockerfile b/src/Kubernetes.Controller/Dockerfile index 7e6b94b8e..685d6224f 100644 --- a/src/Kubernetes.Controller/Dockerfile +++ b/src/Kubernetes.Controller/Dockerfile @@ -5,16 +5,28 @@ WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS publish +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS publish WORKDIR /src -COPY ["src/OperatorFramework/src/Controller", "OperatorFramework/src/Controller"] -COPY ["src/OperatorFramework/src/Core", "OperatorFramework/src/Core"] -COPY ["src/ReverseProxy.Kubernetes.Controller", "ReverseProxy.Kubernetes.Controller"] -COPY ["src/ReverseProxy.Kubernetes.Protocol", "ReverseProxy.Kubernetes.Protocol"] -COPY ["src/ReverseProxy", "ReverseProxy"] -WORKDIR ReverseProxy.Kubernetes.Controller -RUN dotnet publish -c Release -o /app/publish -f net5.0 +# Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds) +COPY ["src/OperatorFramework/src/Controller/Microsoft.Kubernetes.Controller.csproj", "src/OperatorFramework/src/Controller/"] +COPY ["src/OperatorFramework/src/Core/Microsoft.Kubernetes.Core.csproj", "src/OperatorFramework/src/Core/"] +COPY ["src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj", "src/Kubernetes.Controller/"] +COPY ["src/Kubernetes.Protocol/Yarp.Kubernetes.Protocol.csproj", "src/Kubernetes.Protocol/"] +COPY ["src/ReverseProxy/Yarp.ReverseProxy.csproj", "src/ReverseProxy/"] +COPY ["src/Directory.Build.props", "src/"] +COPY ["Directory.Build.*", ""] +COPY ["global.json", ""] +COPY ["NuGet.config", ""] + +# Build a cache layer with all of the nuget packages +RUN dotnet restore src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj + +# Copy the remaining source files +COPY . . + +WORKDIR /src/src/Kubernetes.Controller +RUN dotnet publish -c Release --no-restore -o /app/publish -f net5.0 FROM base AS final WORKDIR /app diff --git a/src/OperatorFramework/test/UnitTests/Controller/Hosting/BackgroundHostedServiceTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Hosting/BackgroundHostedServiceTests.cs index 16cbfe9d3..b704ce305 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Hosting/BackgroundHostedServiceTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Hosting/BackgroundHostedServiceTests.cs @@ -6,22 +6,18 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Kubernetes.Controller.Hosting.Fakes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Controller.Hosting { - - [TestClass] public class BackgroundHostedServiceTests { - [TestMethod] + [Fact] public async Task StartAndStopUnderHosting() { - // arrange var latches = new TestLatches(); using var host = new HostBuilder() @@ -32,21 +28,17 @@ public async Task StartAndStopUnderHosting() }) .Build(); - // act using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); await host.StartAsync(cts.Token).ConfigureAwait(false); await latches.RunEnter.WhenSignalAsync(cts.Token).ConfigureAwait(false); latches.RunResult.Signal(); await latches.RunExit.WhenSignalAsync(cts.Token).ConfigureAwait(false); await host.StopAsync(cts.Token).ConfigureAwait(false); - - // assert } - [TestMethod] + [Fact] public async Task StartAndStopUnderWebHost() { - // arrange var latches = new TestLatches(); using var host = new WebHostBuilder() @@ -59,7 +51,6 @@ public async Task StartAndStopUnderWebHost() .Configure(app => { }) .Build(); - // act // TODO: figure out why the hosting takes so long to unwind naturally // and increase this safety cancellation up from 3 seconds using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); @@ -71,15 +62,12 @@ public async Task StartAndStopUnderWebHost() await latches.RunExit.WhenSignalAsync(cts.Token).ConfigureAwait(false); await runTask.ConfigureAwait(false); - - // assert } - [TestMethod] + [Fact] public async Task IfRunAsyncThrowsItComesBackFromHost() { - // arrange var context = new TestLatches(); using var host = new WebHostBuilder() @@ -92,7 +80,6 @@ public async Task IfRunAsyncThrowsItComesBackFromHost() .Configure(app => { }) .Build(); - // act using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var runTask = host.RunAsync(cts.Token); @@ -101,10 +88,9 @@ public async Task IfRunAsyncThrowsItComesBackFromHost() context.RunResult.Throw(new ApplicationException("Unwind")); #pragma warning restore CA1303 // Do not pass literals as localized parameters - var ex = await Should.ThrowAsync(runTask).ConfigureAwait(false); + var ex = await Assert.ThrowsAsync(() => runTask); - // assert - ex.Flatten().InnerExceptions.ShouldHaveSingleItem().Message.ShouldBe("Unwind"); + Assert.Equal("Unwind", Assert.Single(ex.Flatten().InnerExceptions).Message); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Informers/ResourceInformerTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Informers/ResourceInformerTests.cs index ce2078158..4cc9a7515 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Informers/ResourceInformerTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Informers/ResourceInformerTests.cs @@ -7,23 +7,20 @@ using Microsoft.Extensions.Hosting; using Microsoft.Kubernetes.Testing; using Microsoft.Kubernetes.Utils; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Controller.Informers { - [TestClass] public class ResourceInformerTests { - [TestMethod] + [Fact] public async Task ResourcesAreListedWhenReadyAsyncIsComplete() { - // arrange using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5)); var testYaml = TestYaml.LoadFromEmbeddedStream<(V1Pod[] pods, NamespacedName[] shouldBe)>(); @@ -51,20 +48,17 @@ public async Task ResourcesAreListedWhenReadyAsyncIsComplete() pods[NamespacedName.From(pod)] = pod; }); - // act await clusterHost.StartAsync(cancellation.Token); await testHost.StartAsync(cancellation.Token); await registration.ReadyAsync(cancellation.Token); - // assert - pods.Keys.ShouldBe(testYaml.shouldBe); + Assert.Equal(testYaml.shouldBe, pods.Keys); } - [TestMethod] + [Fact] public async Task ResourcesWithApiGroupAreListed() { - // arrange using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5)); var testYaml = TestYaml.LoadFromEmbeddedStream<(V1Deployment[] deployments, NamespacedName[] shouldBe)>(); @@ -92,14 +86,12 @@ public async Task ResourcesWithApiGroupAreListed() deployments[NamespacedName.From(deployment)] = deployment; }); - // act await clusterHost.StartAsync(cancellation.Token); await testHost.StartAsync(cancellation.Token); await registration.ReadyAsync(cancellation.Token); - // assert - deployments.Keys.ShouldBe(testYaml.shouldBe); + Assert.Equal(testYaml.shouldBe, deployments.Keys); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Queues/DelayingQueueTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Queues/DelayingQueueTests.cs index 69f688112..9a6f224b5 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Queues/DelayingQueueTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Queues/DelayingQueueTests.cs @@ -2,21 +2,18 @@ // Licensed under the MIT License. using Microsoft.Kubernetes.Fakes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Controller.Queues { - [TestClass] public class DelayingQueueTests { - [TestMethod] + [Fact] public void DelayingQueuePassesCallsThrough() { - // arrange var added = new List(); var doned = new List(); var fake = new FakeQueue @@ -28,21 +25,18 @@ public void DelayingQueuePassesCallsThrough() var clock = new FakeSystemClock(); IDelayingQueue delayingQueue = new DelayingQueue(clock, fake); - // act delayingQueue.Add("one"); delayingQueue.Done("two"); var len = delayingQueue.Len(); - // assert - added.ShouldHaveSingleItem().ShouldBe("one"); - doned.ShouldHaveSingleItem().ShouldBe("two"); - len.ShouldBe(42); + Assert.Equal("one", Assert.Single(added)); + Assert.Equal("two", Assert.Single(doned)); + Assert.Equal(42, len); } - [TestMethod] + [Fact(Skip = "https://github.com/microsoft/reverse-proxy/issues/1357")] public async Task DelayingQueueAddsWhenTimePasses() { - // arrange var added = new List(); var fake = new FakeQueue { @@ -51,7 +45,6 @@ public async Task DelayingQueueAddsWhenTimePasses() var clock = new FakeSystemClock(); IDelayingQueue delayingQueue = new DelayingQueue(clock, fake); - // act delayingQueue.AddAfter("50ms", TimeSpan.FromMilliseconds(50)); delayingQueue.AddAfter("100ms", TimeSpan.FromMilliseconds(100)); clock.Advance(TimeSpan.FromMilliseconds(25)); @@ -73,19 +66,17 @@ public async Task DelayingQueueAddsWhenTimePasses() await Task.Delay(TimeSpan.FromMilliseconds(40)); var countAfter135ms = added.Count; - // assert - countAfter25ms.ShouldBe(0); - countAfter55ms.ShouldBe(1); - countAfter80ms.ShouldBe(2); - countAfter105ms.ShouldBe(3); - countAfter135ms.ShouldBe(4); - added.ShouldBe(new[] { "50ms", "75ms", "100ms", "125ms" }, ignoreOrder: false); + Assert.Equal(0, countAfter25ms); + Assert.Equal(1, countAfter55ms); + Assert.Equal(2, countAfter80ms); + Assert.Equal(3, countAfter105ms); + Assert.Equal(4, countAfter135ms); + Assert.Equal(new[] { "50ms", "75ms", "100ms", "125ms" }, added); } - [TestMethod] + [Fact] public async Task ZeroDelayAddsInline() { - // arrange var state = "setup"; var added = new List<(string state, string item)>(); var fake = new FakeQueue @@ -95,7 +86,6 @@ public async Task ZeroDelayAddsInline() var clock = new FakeSystemClock(); IDelayingQueue delayingQueue = new DelayingQueue(clock, fake); - // act state = "before-one"; delayingQueue.AddAfter("one", TimeSpan.FromMilliseconds(1)); state = "after-one"; @@ -111,14 +101,12 @@ public async Task ZeroDelayAddsInline() state = "after-three"; await Task.Delay(TimeSpan.FromMilliseconds(40)); - // assert - added.ShouldHaveSingleItem().ShouldBe(("before-two", "two")); + Assert.Equal(("before-two", "two"), Assert.Single(added)); } - [TestMethod] + [Fact(Skip = "https://github.com/microsoft/reverse-proxy/issues/1357")] public async Task NoAddingAfterShutdown() { - // arrange var added = new List(); var fake = new FakeQueue { @@ -127,15 +115,13 @@ public async Task NoAddingAfterShutdown() var clock = new FakeSystemClock(); IDelayingQueue delayingQueue = new DelayingQueue(clock, fake); - // act delayingQueue.AddAfter("one", TimeSpan.FromMilliseconds(10)); delayingQueue.ShutDown(); delayingQueue.AddAfter("two", TimeSpan.FromMilliseconds(10)); clock.Advance(TimeSpan.FromMilliseconds(25)); await Task.Delay(TimeSpan.FromMilliseconds(40)); - // assert - added.ShouldBeEmpty(); + Assert.Empty(added); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Queues/RateLimitingQueueTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Queues/RateLimitingQueueTests.cs index a82e4abd5..6ad92f38e 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Queues/RateLimitingQueueTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Queues/RateLimitingQueueTests.cs @@ -2,20 +2,17 @@ // Licensed under the MIT License. using Microsoft.Kubernetes.Fakes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Collections.Generic; +using Xunit; namespace Microsoft.Kubernetes.Controller.Queues { - [TestClass] public class RateLimitingQueueTests { - [TestMethod] + [Fact] public void AddRateLimitedCallsWhenAndAddDelay() { - // arrange var whenResults = new Dictionary { { "one", TimeSpan.FromMilliseconds(15) }, @@ -38,24 +35,23 @@ public void AddRateLimitedCallsWhenAndAddDelay() }; var queue = new RateLimitingQueue(rateLimiter, @base); - // act queue.AddRateLimited("one"); queue.AddRateLimited("two"); queue.AddRateLimited("three"); - // assert - whenCalls.ShouldBe(new[] + Assert.Equal(new[] { "one", "two", "three" - }); - addAfterCalls.ShouldBe(new[] + }, whenCalls); + + Assert.Equal(new[] { ("one", TimeSpan.FromMilliseconds(15)), ("two", TimeSpan.FromMilliseconds(0)), - ("three", TimeSpan.FromMilliseconds(30)), - }); + ("three", TimeSpan.FromMilliseconds(30)) + }, addAfterCalls); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Queues/WorkQueueTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Queues/WorkQueueTests.cs index 82f92c86b..0b6dbbcb8 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Queues/WorkQueueTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Queues/WorkQueueTests.cs @@ -1,85 +1,63 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Controller.Queues { - [TestClass] public class WorkQueueTests { - public CancellationTokenSource Cancellation { get; set; } + public CancellationTokenSource Cancellation { get; set; } = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - [TestInitialize] - public void TestInitialize() - { - Cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - } - - [TestCleanup] - public void TestCleanup() - { - Cancellation.Dispose(); - } - - [TestMethod] + [Fact] public async Task NormalUsageIsAddGetDone() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act - queue.Len().ShouldBe(0); + Assert.Equal(0, queue.Len()); queue.Add("one"); - queue.Len().ShouldBe(1); + Assert.Equal(1, queue.Len()); queue.Add("two"); - queue.Len().ShouldBe(2); + Assert.Equal(2, queue.Len()); var (item1, shutdown1) = await queue.GetAsync(Cancellation.Token); - queue.Len().ShouldBe(1); + Assert.Equal(1, queue.Len()); queue.Done(item1); - queue.Len().ShouldBe(1); + Assert.Equal(1, queue.Len()); var (item2, shutdown2) = await queue.GetAsync(Cancellation.Token); - queue.Len().ShouldBe(0); + Assert.Equal(0, queue.Len()); queue.Done(item2); - queue.Len().ShouldBe(0); + Assert.Equal(0, queue.Len()); - // assert - item1.ShouldBe("one"); - shutdown1.ShouldBeFalse(); - item2.ShouldBe("two"); - shutdown2.ShouldBeFalse(); + Assert.Equal("one", item1); + Assert.False(shutdown1); + Assert.Equal("two", item2); + Assert.False(shutdown2); } - [TestMethod] + [Fact] public void AddingSameItemAgainHasNoEffect() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var len1 = queue.Len(); queue.Add("one"); var len2 = queue.Len(); queue.Add("one"); var len3 = queue.Len(); - // assert - len1.ShouldBe(0); - len2.ShouldBe(1); - len3.ShouldBe(1); + Assert.Equal(0, len1); + Assert.Equal(1, len2); + Assert.Equal(1, len3); } - [TestMethod] + [Fact] public async Task CallingAddWhileItemIsBeingProcessedHasNoEffect() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); @@ -88,23 +66,20 @@ public async Task CallingAddWhileItemIsBeingProcessedHasNoEffect() queue.Add("one"); var lenAfterAddAgain = queue.Len(); - // assert - item1.ShouldBe("one"); - lenOriginal.ShouldBe(0); - lenAfterAdd.ShouldBe(1); - lenAfterGet.ShouldBe(0); - lenAfterAddAgain.ShouldBe(0); + Assert.Equal("one", item1); + Assert.Equal(0, lenOriginal); + Assert.Equal(1, lenAfterAdd); + Assert.Equal(0, lenAfterGet); + Assert.Equal(0, lenAfterAddAgain); - queue.Len().ShouldBe(0); + Assert.Equal(0, queue.Len()); } - [TestMethod] + [Fact] public async Task ItemCanBeAddedAgainAfterDoneIsCalled() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); @@ -115,24 +90,21 @@ public async Task ItemCanBeAddedAgainAfterDoneIsCalled() queue.Add("one"); var lenAfterAddAgain = queue.Len(); - // assert - item1.ShouldBe("one"); - lenOriginal.ShouldBe(0); - lenAfterAdd.ShouldBe(1); - lenAfterGet.ShouldBe(0); - lenAfterDone.ShouldBe(0); - lenAfterAddAgain.ShouldBe(1); + Assert.Equal("one", item1); + Assert.Equal(0, lenOriginal); + Assert.Equal(1, lenAfterAdd); + Assert.Equal(0, lenAfterGet); + Assert.Equal(0, lenAfterDone); + Assert.Equal(1, lenAfterAddAgain); - queue.Len().ShouldBe(1); + Assert.Equal(1, queue.Len()); } - [TestMethod] + [Fact] public async Task IfAddWasCalledDuringProcessingThenItemIsRequeuedByDone() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); @@ -145,76 +117,66 @@ public async Task IfAddWasCalledDuringProcessingThenItemIsRequeuedByDone() var (item2, _) = await queue.GetAsync(Cancellation.Token); var lenAfterGetAgain = queue.Len(); - // assert - item1.ShouldBe("one"); - item2.ShouldBe("one"); - lenOriginal.ShouldBe(0); - lenAfterAdd.ShouldBe(1); - lenAfterGet.ShouldBe(0); - lenAfterAddAgain.ShouldBe(0); - lenAfterDone.ShouldBe(1); - lenAfterGetAgain.ShouldBe(0); - - queue.Len().ShouldBe(0); + Assert.Equal("one", item1); + Assert.Equal("one", item2); + Assert.Equal(0, lenOriginal); + Assert.Equal(1, lenAfterAdd); + Assert.Equal(0, lenAfterGet); + Assert.Equal(0, lenAfterAddAgain); + Assert.Equal(1, lenAfterDone); + Assert.Equal(0, lenAfterGetAgain); + + Assert.Equal(0, queue.Len()); } - [TestMethod] + [Fact] public async Task GetCompletesOnceAddIsCalled() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var getTask = queue.GetAsync(Cancellation.Token); - queue.Len().ShouldBe(0); - getTask.IsCompleted.ShouldBeFalse(); + Assert.Equal(0, queue.Len()); + Assert.False(getTask.IsCompleted); queue.Add("one"); var (item1, _) = await getTask; - queue.Len().ShouldBe(0); - getTask.IsCompleted.ShouldBeTrue(); + Assert.Equal(0, queue.Len()); + Assert.True(getTask.IsCompleted); - // assert - item1.ShouldBe("one"); - queue.Len().ShouldBe(0); + Assert.Equal("one", item1); + Assert.Equal(0, queue.Len()); } - [TestMethod] + [Fact] public async Task GetReturnsShutdownTrueAfterShutdownIsCalled() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var getTask = queue.GetAsync(Cancellation.Token); - queue.Len().ShouldBe(0); - getTask.IsCompleted.ShouldBeFalse(); + Assert.Equal(0, queue.Len()); + Assert.False(getTask.IsCompleted); queue.ShutDown(); var (item1, shutdown1) = await getTask; - queue.Len().ShouldBe(0); - getTask.IsCompleted.ShouldBeTrue(); + Assert.Equal(0, queue.Len()); + Assert.True(getTask.IsCompleted); - // assert - shutdown1.ShouldBeTrue(); - queue.Len().ShouldBe(0); + Assert.True(shutdown1); + Assert.Equal(0, queue.Len()); } - [TestMethod] + [Fact] public void ShuttingDownReturnsTrueAfterShutdownIsCalled() { - // arrange using IWorkQueue queue = new WorkQueue(); - // act var shuttingDownBefore = queue.ShuttingDown(); queue.ShutDown(); var shuttingDownAfter = queue.ShuttingDown(); - // assert - shuttingDownBefore.ShouldBeFalse(); - shuttingDownAfter.ShouldBeTrue(); + Assert.False(shuttingDownBefore); + Assert.True(shuttingDownAfter); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Rate/LimitTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Rate/LimitTests.cs index 10158beaa..602990c6d 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Rate/LimitTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Rate/LimitTests.cs @@ -1,47 +1,39 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; +using Xunit; namespace Microsoft.Kubernetes.Controller.Rate { - [TestClass] public class LimitTests { - [TestMethod] - [DataRow(15, 1, 15)] - [DataRow(15, 120, 1800)] - [DataRow(15, .1, 1.5)] - [DataRow(300, 2, 600)] + [Theory] + [InlineData(15, 1, 15)] + [InlineData(15, 120, 1800)] + [InlineData(15, .1, 1.5)] + [InlineData(300, 2, 600)] public void TokensFromDuration(double perSecond, double durationSeconds, double tokens) { - // arrange var limit = new Limit(perSecond); - // act var tokensFromDuration = limit.TokensFromDuration(TimeSpan.FromSeconds(durationSeconds)); - // assert - tokensFromDuration.ShouldBe(tokens); + Assert.Equal(tokens, tokensFromDuration); } - [TestMethod] - [DataRow(15, 1, 15)] - [DataRow(15, 120, 1800)] - [DataRow(15, .1, 1.5)] - [DataRow(300, 2, 600)] + [Theory] + [InlineData(15, 1, 15)] + [InlineData(15, 120, 1800)] + [InlineData(15, .1, 1.5)] + [InlineData(300, 2, 600)] public void DurationFromTokens(double perSecond, double durationSeconds, double tokens) { - // arrange var limit = new Limit(perSecond); - // act var durationFromTokens = limit.DurationFromTokens(tokens); - // assert - durationFromTokens.ShouldBe(TimeSpan.FromSeconds(durationSeconds)); + Assert.Equal(TimeSpan.FromSeconds(durationSeconds), durationFromTokens); } } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Rate/LimiterTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Rate/LimiterTests.cs index 634eb0cb3..d30114d02 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Rate/LimiterTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Rate/LimiterTests.cs @@ -2,46 +2,40 @@ // Licensed under the MIT License. using Microsoft.Kubernetes.Fakes; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Polly; -using Shouldly; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; namespace Microsoft.Kubernetes.Controller.Rate { - [TestClass] public class LimiterTests { - [TestMethod] + [Fact] public void FirstTokenIsAvailable() { - // arrange var clock = new FakeSystemClock(); var limiter = new Limiter(new Limit(10), 1, clock); - // act var allowed = limiter.Allow(); - // assert - allowed.ShouldBe(true); + Assert.True(allowed); } - [TestMethod] - [DataRow(5)] - [DataRow(1)] - [DataRow(300)] + [Theory] + [InlineData(5)] + [InlineData(1)] + [InlineData(300)] public void AsManyAsBurstTokensAreAvailableRightAway(int burst) { - // arrange var clock = new FakeSystemClock(); var limiter = new Limiter(new Limit(10), burst, clock); - // act var allowed = new List(); foreach (var index in Enumerable.Range(1, burst)) { @@ -49,19 +43,16 @@ public void AsManyAsBurstTokensAreAvailableRightAway(int burst) } var notAllowed = limiter.Allow(); - // assert - allowed.ShouldAllBe(item => item == true); - notAllowed.ShouldBeFalse(); + Assert.All(allowed, item => Assert.True(item)); + Assert.False(notAllowed); } - [TestMethod] + [Fact] public void TokensBecomeAvailableAtLimitPerSecondRate() { - // arrange var clock = new FakeSystemClock(); var limiter = new Limiter(new Limit(10), 50, clock); - // act var initiallyAllowed = limiter.AllowN(clock.UtcNow, 50); var thenNotAllowed1 = limiter.Allow(); @@ -74,24 +65,21 @@ public void TokensBecomeAvailableAtLimitPerSecondRate() var twoTokensAvailable2 = limiter.Allow(); var thenNotAllowed3 = limiter.Allow(); - // assert - initiallyAllowed.ShouldBeTrue(); - thenNotAllowed1.ShouldBeFalse(); - oneTokenAvailable.ShouldBeTrue(); - thenNotAllowed2.ShouldBeFalse(); - twoTokensAvailable1.ShouldBeTrue(); - twoTokensAvailable2.ShouldBeTrue(); - thenNotAllowed3.ShouldBeFalse(); + Assert.True(initiallyAllowed); + Assert.False(thenNotAllowed1); + Assert.True(oneTokenAvailable); + Assert.False(thenNotAllowed2); + Assert.True(twoTokensAvailable1); + Assert.True(twoTokensAvailable2); + Assert.False(thenNotAllowed3); } - [TestMethod] + [Fact] public void ReserveTellsYouHowLongToWait() { - // arrange var clock = new FakeSystemClock(); var limiter = new Limiter(new Limit(10), 50, clock); - // act var initiallyAllowed = limiter.AllowN(clock.UtcNow, 50); var thenNotAllowed1 = limiter.Allow(); @@ -109,27 +97,24 @@ public void ReserveTellsYouHowLongToWait() var reserveHalfAvailable = limiter.Reserve(); var delayHalfAvailable = reserveHalfAvailable.Delay(); - // assert - initiallyAllowed.ShouldBeTrue(); - thenNotAllowed1.ShouldBeFalse(); - reserveOne.Ok.ShouldBeTrue(); - delayOne.ShouldBe(TimeSpan.FromMilliseconds(100)); - reserveTwoMore.Ok.ShouldBeTrue(); - delayTwoMore.ShouldBe(TimeSpan.FromMilliseconds(300)); - reserveAlreadyAvailable.Ok.ShouldBeTrue(); - delayAlreadyAvailable.ShouldBe(TimeSpan.Zero); - reserveHalfAvailable.Ok.ShouldBeTrue(); - delayHalfAvailable.ShouldBe(TimeSpan.FromMilliseconds(50)); + Assert.True(initiallyAllowed); + Assert.False(thenNotAllowed1); + Assert.True(reserveOne.Ok); + Assert.Equal(TimeSpan.FromMilliseconds(100), delayOne); + Assert.True(reserveTwoMore.Ok); + Assert.Equal(TimeSpan.FromMilliseconds(300), delayTwoMore); + Assert.True(reserveAlreadyAvailable.Ok); + Assert.Equal(TimeSpan.Zero, delayAlreadyAvailable); + Assert.True(reserveHalfAvailable.Ok); + Assert.Equal(TimeSpan.FromMilliseconds(50), delayHalfAvailable); } - [TestMethod] + [Fact(Skip = "https://github.com/microsoft/reverse-proxy/issues/1357")] public async Task WaitAsyncCausesPauseLikeReserve() { - // arrange var limiter = new Limiter(new Limit(10), 5); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - // act while (cancellation.IsCancellationRequested == false) { var task = limiter.WaitAsync(cancellation.Token); @@ -162,26 +147,23 @@ public async Task WaitAsyncCausesPauseLikeReserve() await limiter.WaitAsync(cancellation.Token).ConfigureAwait(false); delayHalfAvailable.Stop(); - // assert - delayOne.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(100), tolerance: TimeSpan.FromMilliseconds(25)); - delayTwoMore.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(200), tolerance: TimeSpan.FromMilliseconds(25)); - delayAlreadyAvailable.Elapsed.ShouldBe(TimeSpan.Zero, tolerance: TimeSpan.FromMilliseconds(5)); - delayHalfAvailable.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(50), tolerance: TimeSpan.FromMilliseconds(25)); + Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125)); + Assert.InRange(delayTwoMore.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225)); + Assert.InRange(delayAlreadyAvailable.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(5)); + Assert.InRange(delayHalfAvailable.Elapsed, TimeSpan.FromMilliseconds(25), TimeSpan.FromMilliseconds(75)); } - [TestMethod] + [Fact(Skip = "https://github.com/microsoft/reverse-proxy/issues/1357")] public async Task ManyWaitsStackUp() { await Policy - .Handle() + .Handle() .RetryAsync(3) .ExecuteAsync(async () => { - // arrange var limiter = new Limiter(new Limit(10), 5); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - // act while (cancellation.IsCancellationRequested == false) { var task = limiter.WaitAsync(cancellation.Token); @@ -208,25 +190,24 @@ await Policy limiter.WaitAsync(cancellation.Token), }; - var taskOne = await Task.WhenAny(waits.ToArray()).ConfigureAwait(false); + var taskOne = await Task.WhenAny(waits).ConfigureAwait(false); await taskOne.ConfigureAwait(false); delayOne.Stop(); waits.Remove(taskOne); - var taskTwo = await Task.WhenAny(waits.ToArray()).ConfigureAwait(false); + var taskTwo = await Task.WhenAny(waits).ConfigureAwait(false); await taskTwo.ConfigureAwait(false); delayTwo.Stop(); waits.Remove(taskTwo); - var taskThree = await Task.WhenAny(waits.ToArray()).ConfigureAwait(false); + var taskThree = await Task.WhenAny(waits).ConfigureAwait(false); await taskThree.ConfigureAwait(false); delayThree.Stop(); waits.Remove(taskThree); - // assert - delayOne.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(100), tolerance: TimeSpan.FromMilliseconds(25)); - delayTwo.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(200), tolerance: TimeSpan.FromMilliseconds(25)); - delayThree.Elapsed.ShouldBe(TimeSpan.FromMilliseconds(300), tolerance: TimeSpan.FromMilliseconds(25)); + Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125)); + Assert.InRange(delayTwo.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225)); + Assert.InRange(delayThree.Elapsed, TimeSpan.FromMilliseconds(275), TimeSpan.FromMilliseconds(325)); }).ConfigureAwait(false); } } diff --git a/src/OperatorFramework/test/UnitTests/Controller/Rate/ReservationTests.cs b/src/OperatorFramework/test/UnitTests/Controller/Rate/ReservationTests.cs index 9776b50e1..fa70b58b7 100644 --- a/src/OperatorFramework/test/UnitTests/Controller/Rate/ReservationTests.cs +++ b/src/OperatorFramework/test/UnitTests/Controller/Rate/ReservationTests.cs @@ -3,43 +3,37 @@ using Microsoft.Kubernetes.Controller.Rate; using Microsoft.Kubernetes.Fakes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; +using Xunit; namespace Microsoft.Kubernetes.Controllers.Rate { - [TestClass] public class ReservationTests { - [TestMethod] + [Fact] public void NotOkayAlwaysReturnsMaxValueDelay() { - // arrange var clock = new FakeSystemClock(); var reservation = new Reservation( clock: clock, limiter: default, ok: false); - // act var delay1 = reservation.Delay(); var delayFrom1 = reservation.DelayFrom(clock.UtcNow); clock.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); var delayFrom2 = reservation.DelayFrom(clock.UtcNow); - // assert - delay1.ShouldBe(TimeSpan.MaxValue); - delayFrom1.ShouldBe(TimeSpan.MaxValue); - delay2.ShouldBe(TimeSpan.MaxValue); - delayFrom2.ShouldBe(TimeSpan.MaxValue); + Assert.Equal(TimeSpan.MaxValue, delay1); + Assert.Equal(TimeSpan.MaxValue, delayFrom1); + Assert.Equal(TimeSpan.MaxValue, delay2); + Assert.Equal(TimeSpan.MaxValue, delayFrom2); } - [TestMethod] + [Fact] public void DelayIsZeroWhenTimeToActIsNowOrEarlier() { - // arrange var clock = new FakeSystemClock(); var reservation = new Reservation( clock: clock, @@ -48,24 +42,21 @@ public void DelayIsZeroWhenTimeToActIsNowOrEarlier() timeToAct: clock.UtcNow, limit: default); - // act var delay1 = reservation.Delay(); var delayFrom1 = reservation.DelayFrom(clock.UtcNow); clock.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); var delayFrom2 = reservation.DelayFrom(clock.UtcNow); - // assert - delay1.ShouldBe(TimeSpan.Zero); - delayFrom1.ShouldBe(TimeSpan.Zero); - delay2.ShouldBe(TimeSpan.Zero); - delayFrom2.ShouldBe(TimeSpan.Zero); + Assert.Equal(TimeSpan.Zero, delay1); + Assert.Equal(TimeSpan.Zero, delayFrom1); + Assert.Equal(TimeSpan.Zero, delay2); + Assert.Equal(TimeSpan.Zero, delayFrom2); } - [TestMethod] + [Fact] public void DelayGetsSmallerAsTimePasses() { - // arrange var clock = new FakeSystemClock(); var reservation = new Reservation( clock: clock, @@ -74,23 +65,20 @@ public void DelayGetsSmallerAsTimePasses() timeToAct: clock.UtcNow.Add(TimeSpan.FromMinutes(5)), limit: default); - // act var delay1 = reservation.Delay(); clock.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); clock.Advance(TimeSpan.FromMinutes(3)); var delay3 = reservation.Delay(); - // assert - delay1.ShouldBe(TimeSpan.FromMinutes(5)); - delay2.ShouldBe(TimeSpan.FromMinutes(2)); - delay3.ShouldBe(TimeSpan.Zero); + Assert.Equal(TimeSpan.FromMinutes(5), delay1); + Assert.Equal(TimeSpan.FromMinutes(2), delay2); + Assert.Equal(TimeSpan.Zero, delay3); } - [TestMethod] + [Fact] public void DelayFromNotChangedByTimePassing() { - // arrange var clock = new FakeSystemClock(); var reservation = new Reservation( clock: clock, @@ -102,7 +90,6 @@ public void DelayFromNotChangedByTimePassing() var twoMinutesPast = clock.UtcNow.Subtract(TimeSpan.FromMinutes(2)); var twoMinutesFuture = clock.UtcNow.Add(TimeSpan.FromMinutes(2)); - // act var delay1 = reservation.DelayFrom(clock.UtcNow); var delayPast1 = reservation.DelayFrom(twoMinutesPast); var delayFuture1 = reservation.DelayFrom(twoMinutesFuture); @@ -111,13 +98,12 @@ public void DelayFromNotChangedByTimePassing() var delayPast2 = reservation.DelayFrom(twoMinutesPast); var delayFuture2 = reservation.DelayFrom(twoMinutesFuture); - // assert - delay1.ShouldBe(TimeSpan.FromMinutes(5)); - delayPast1.ShouldBe(TimeSpan.FromMinutes(7)); - delayFuture1.ShouldBe(TimeSpan.FromMinutes(3)); - delay2.ShouldBe(TimeSpan.FromMinutes(2)); - delayPast2.ShouldBe(TimeSpan.FromMinutes(7)); - delayFuture2.ShouldBe(TimeSpan.FromMinutes(3)); + Assert.Equal(TimeSpan.FromMinutes(5), delay1); + Assert.Equal(TimeSpan.FromMinutes(7), delayPast1); + Assert.Equal(TimeSpan.FromMinutes(3), delayFuture1); + Assert.Equal(TimeSpan.FromMinutes(2), delay2); + Assert.Equal(TimeSpan.FromMinutes(7), delayPast2); + Assert.Equal(TimeSpan.FromMinutes(3), delayFuture2); } } } diff --git a/src/OperatorFramework/test/UnitTests/CustomResources/CustomResourceDefinitionGeneratorTests.cs b/src/OperatorFramework/test/UnitTests/CustomResources/CustomResourceDefinitionGeneratorTests.cs index bd9340573..0c3dae5c9 100644 --- a/src/OperatorFramework/test/UnitTests/CustomResources/CustomResourceDefinitionGeneratorTests.cs +++ b/src/OperatorFramework/test/UnitTests/CustomResources/CustomResourceDefinitionGeneratorTests.cs @@ -2,142 +2,119 @@ // Licensed under the MIT License. using k8s.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.CustomResources { - [TestClass] public class CustomResourceDefinitionGeneratorTests { - [TestMethod] + [Fact] public async Task MetadataNameComesFromPluralNameAndGroup() { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync("Namespaced"); - // assert - crd.Name().ShouldBe("testkinds.test-group"); + Assert.Equal("testkinds.test-group", crd.Name()); } - [TestMethod] + [Fact] public async Task ApiVersionAndKindAreCorrect() { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync("Namespaced"); - // assert - crd.ApiGroupVersion().ShouldBe("v1"); - crd.ApiGroupVersion().ShouldBe(V1CustomResourceDefinition.KubeApiVersion); - crd.ApiGroup().ShouldBe("apiextensions.k8s.io"); - crd.ApiGroup().ShouldBe(V1CustomResourceDefinition.KubeGroup); - crd.Kind.ShouldBe("CustomResourceDefinition"); - crd.Kind.ShouldBe(V1CustomResourceDefinition.KubeKind); + Assert.Equal("v1", crd.ApiGroupVersion()); + Assert.Equal(V1CustomResourceDefinition.KubeApiVersion, crd.ApiGroupVersion()); + Assert.Equal("apiextensions.k8s.io", crd.ApiGroup()); + Assert.Equal(V1CustomResourceDefinition.KubeGroup, crd.ApiGroup()); + Assert.Equal("CustomResourceDefinition", crd.Kind); + Assert.Equal(V1CustomResourceDefinition.KubeKind, crd.Kind); crd.Validate(); } - [TestMethod] - [DataRow("Namespaced")] - [DataRow("Cluster")] + [Theory] + [InlineData("Namespaced")] + [InlineData("Cluster")] public async Task ScopeProvidedByGenerateParameter(string scope) { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync(scope); - // assert - crd.Spec.Scope.ShouldBe(scope); + Assert.Equal(scope, crd.Spec.Scope); } - [TestMethod] - [DataRow(typeof(SimpleResource), "test-group", "TestKind", "testkinds")] - [DataRow(typeof(AnotherResource), "another-group", "AnotherKind", "anotherkinds")] + [Theory] + [InlineData(typeof(SimpleResource), "test-group", "TestKind", "testkinds")] + [InlineData(typeof(AnotherResource), "another-group", "AnotherKind", "anotherkinds")] public async Task GroupAndNamesComesFromKubernetesEntityAttribute(Type resourceType, string group, string kind, string plural) { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync(resourceType, "Namespaced"); - // assert - crd.Spec.Group.ShouldBe(group); - crd.Spec.Names.Kind.ShouldBe(kind); - crd.Spec.Names.Plural.ShouldBe(plural); + Assert.Equal(group, crd.Spec.Group); + Assert.Equal(kind, crd.Spec.Names.Kind); + Assert.Equal(plural, crd.Spec.Names.Plural); } - [TestMethod] - [DataRow(typeof(SimpleResource), "test-version")] - [DataRow(typeof(AnotherResource), "another-version")] + [Theory] + [InlineData(typeof(SimpleResource), "test-version")] + [InlineData(typeof(AnotherResource), "another-version")] public async Task CreateWithSingleVersionThatIsStoredAndServed(Type resourceType, string version) { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync(resourceType, "Namespaced"); - // assert - var crdVersion = crd.Spec.Versions.ShouldHaveSingleItem(); - crdVersion.Name.ShouldBe(version); - crdVersion.Served.ShouldBe(true); - crdVersion.Storage.ShouldBe(true); + var crdVersion = Assert.Single(crd.Spec.Versions); + Assert.Equal(version, crdVersion.Name); + Assert.True(crdVersion.Served); + Assert.True(crdVersion.Storage); } - [TestMethod] + [Fact] public async Task TypicalResourceHasSchema() { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync("Namespaced"); - // assert - var schema = crd - .Spec.ShouldNotBeNull() - .Versions.ShouldHaveSingleItem() - .Schema.ShouldNotBeNull() - .OpenAPIV3Schema.ShouldNotBeNull(); + Assert.NotNull(crd.Spec); + var version = Assert.Single(crd.Spec.Versions); + Assert.NotNull(version.Schema); + var schema = version.Schema.OpenAPIV3Schema; + Assert.NotNull(schema); - schema.Properties.Keys.ShouldBe(new[] { "apiVersion", "kind", "metadata", "spec", "status" }); + Assert.Equal(new[] { "apiVersion", "kind", "metadata", "spec", "status" }, schema.Properties.Keys); - schema.Properties["apiVersion"].Type.ShouldBe("string"); - schema.Properties["kind"].Type.ShouldBe("string"); - schema.Properties["metadata"].Type.ShouldBe("object"); - schema.Properties["spec"].Type.ShouldBe("object"); - schema.Properties["status"].Type.ShouldBe("object"); + Assert.Equal("string", schema.Properties["apiVersion"].Type); + Assert.Equal("string", schema.Properties["kind"].Type); + Assert.Equal("object", schema.Properties["metadata"].Type); + Assert.Equal("object", schema.Properties["spec"].Type); + Assert.Equal("object", schema.Properties["status"].Type); } - [TestMethod] + [Fact] public async Task DescriptionsComeFromDocComments() { - // arrange var generator = new CustomResourceDefinitionGenerator(); - // act var crd = await generator.GenerateCustomResourceDefinitionAsync("Namespaced"); - // assert - var schema = crd - .Spec.ShouldNotBeNull() - .Versions.ShouldHaveSingleItem() - .Schema.ShouldNotBeNull() - .OpenAPIV3Schema.ShouldNotBeNull(); + Assert.NotNull(crd.Spec); + var version = Assert.Single(crd.Spec.Versions); + Assert.NotNull(version.Schema); + var schema = version.Schema.OpenAPIV3Schema; + Assert.NotNull(schema); - schema.Description.ShouldNotBeNull().ShouldContain("TypicalResource doc comment"); - schema.Properties["spec"].Description.ShouldNotBeNull().ShouldContain("Spec doc comment"); - schema.Properties["status"].Description.ShouldNotBeNull().ShouldContain("Status doc comment"); + Assert.Contains("TypicalResource doc comment", schema.Description, StringComparison.Ordinal); + Assert.Contains("Spec doc comment", schema.Properties["spec"].Description, StringComparison.Ordinal); + Assert.Contains("Status doc comment", schema.Properties["status"].Description, StringComparison.Ordinal); } } } diff --git a/src/OperatorFramework/test/UnitTests/GroupKindNamespacedNameTests.cs b/src/OperatorFramework/test/UnitTests/GroupKindNamespacedNameTests.cs index 185ec1d5a..e6168b10d 100644 --- a/src/OperatorFramework/test/UnitTests/GroupKindNamespacedNameTests.cs +++ b/src/OperatorFramework/test/UnitTests/GroupKindNamespacedNameTests.cs @@ -2,19 +2,15 @@ // Licensed under the MIT License. using k8s.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; +using Xunit; namespace Microsoft.Kubernetes { - [TestClass] public class GroupKindNamespacedNameTests { - - [TestMethod] + [Fact] public void GroupKindAndNamespacedNameFromResource() { - // arrange var resource = new V1Role( apiVersion: $"{V1Role.KubeGroup}/{V1Role.KubeApiVersion}", kind: V1Role.KubeKind, @@ -22,20 +18,17 @@ public void GroupKindAndNamespacedNameFromResource() name: "the-name", namespaceProperty: "the-namespace")); - // act var key = GroupKindNamespacedName.From(resource); - // assert - key.Group.ShouldBe("rbac.authorization.k8s.io"); - key.Kind.ShouldBe("Role"); - key.NamespacedName.Namespace.ShouldBe("the-namespace"); - key.NamespacedName.Name.ShouldBe("the-name"); + Assert.Equal("rbac.authorization.k8s.io", key.Group); + Assert.Equal("Role", key.Kind); + Assert.Equal("the-namespace", key.NamespacedName.Namespace); + Assert.Equal("the-name", key.NamespacedName.Name); } - [TestMethod] + [Fact] public void GroupCanBeEmpty() { - // arrange var resource = new V1ConfigMap( apiVersion: V1ConfigMap.KubeApiVersion, kind: V1ConfigMap.KubeKind, @@ -43,46 +36,41 @@ public void GroupCanBeEmpty() name: "the-name", namespaceProperty: "the-namespace")); - // act var key = GroupKindNamespacedName.From(resource); - // assert - key.Group.ShouldBe(""); - key.Kind.ShouldBe("ConfigMap"); - key.NamespacedName.Namespace.ShouldBe("the-namespace"); - key.NamespacedName.Name.ShouldBe("the-name"); + Assert.Equal("", key.Group); + Assert.Equal("ConfigMap", key.Kind); + Assert.Equal("the-namespace", key.NamespacedName.Namespace); + Assert.Equal("the-name", key.NamespacedName.Name); } - [TestMethod] + [Fact] public void NamespaceCanBeNull() { - // arrange var resource = new V1ClusterRole( apiVersion: $"{V1ClusterRole.KubeGroup}/{V1ClusterRole.KubeApiVersion}", kind: V1ClusterRole.KubeKind, metadata: new V1ObjectMeta( name: "the-name")); - // act var key = GroupKindNamespacedName.From(resource); - // assert - key.Group.ShouldBe("rbac.authorization.k8s.io"); - key.Kind.ShouldBe("ClusterRole"); - key.NamespacedName.Namespace.ShouldBeNull(); - key.NamespacedName.Name.ShouldBe("the-name"); + Assert.Equal("rbac.authorization.k8s.io", key.Group); + Assert.Equal("ClusterRole", key.Kind); + Assert.Null(key.NamespacedName.Namespace); + Assert.Equal("the-name", key.NamespacedName.Name); } - [TestMethod] - [DataRow("group", "kind", "ns", "name", "group", "kind", "ns", "name", true)] - [DataRow("group", "kind", null, "name", "group", "kind", null, "name", true)] - [DataRow("", "kind", "ns", "name", "", "kind", "ns", "name", true)] - [DataRow("", "kind", null, "name", "", "kind", null, "name", true)] - [DataRow("group", "kind", "ns", "name", "group2", "kind", "ns", "name", false)] - [DataRow("group", "kind", "ns", "name", "group", "kind2", "ns", "name", false)] - [DataRow("group", "kind", "ns", "name", "group", "kind", "ns2", "name", false)] - [DataRow("group", "kind", "ns", "name", "group", "kind", null, "name", false)] - [DataRow("group", "kind", "ns", "name", "group", "kind", "ns", "name2", false)] + [Theory] + [InlineData("group", "kind", "ns", "name", "group", "kind", "ns", "name", true)] + [InlineData("group", "kind", null, "name", "group", "kind", null, "name", true)] + [InlineData("", "kind", "ns", "name", "", "kind", "ns", "name", true)] + [InlineData("", "kind", null, "name", "", "kind", null, "name", true)] + [InlineData("group", "kind", "ns", "name", "group2", "kind", "ns", "name", false)] + [InlineData("group", "kind", "ns", "name", "group", "kind2", "ns", "name", false)] + [InlineData("group", "kind", "ns", "name", "group", "kind", "ns2", "name", false)] + [InlineData("group", "kind", "ns", "name", "group", "kind", null, "name", false)] + [InlineData("group", "kind", "ns", "name", "group", "kind", "ns", "name2", false)] public void EqualityAndInequality( string group1, string kind1, @@ -94,11 +82,9 @@ public void EqualityAndInequality( string name2, bool shouldBeEqual) { - // arrange var key1 = new GroupKindNamespacedName(group1, kind1, new NamespacedName(ns1, name1)); var key2 = new GroupKindNamespacedName(group2, kind2, new NamespacedName(ns2, name2)); - // act var areEqual = key1 == key2; var areNotEqual = key1 != key2; #pragma warning disable CS1718 // Comparison made to same variable @@ -108,13 +94,12 @@ public void EqualityAndInequality( var sameNotEqual2 = key2 != key2; #pragma warning restore CS1718 // Comparison made to same variable - // assert - areEqual.ShouldNotBe(areNotEqual); - areEqual.ShouldBe(shouldBeEqual); - sameEqual1.ShouldBeTrue(); - sameNotEqual1.ShouldBeFalse(); - sameEqual2.ShouldBeTrue(); - sameNotEqual2.ShouldBeFalse(); + Assert.NotEqual(areNotEqual, areEqual); + Assert.Equal(shouldBeEqual, areEqual); + Assert.True(sameEqual1); + Assert.False(sameNotEqual1); + Assert.True(sameEqual2); + Assert.False(sameNotEqual2); } } } diff --git a/src/OperatorFramework/test/UnitTests/KubernetesCoreExtensionsTests.cs b/src/OperatorFramework/test/UnitTests/KubernetesCoreExtensionsTests.cs index 8e801c255..feddbeb8a 100644 --- a/src/OperatorFramework/test/UnitTests/KubernetesCoreExtensionsTests.cs +++ b/src/OperatorFramework/test/UnitTests/KubernetesCoreExtensionsTests.cs @@ -4,57 +4,46 @@ using k8s; using Microsoft.Extensions.DependencyInjection; using Microsoft.Kubernetes.Resources; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; +using Xunit; namespace Microsoft.Kubernetes { - [TestClass] public class KubernetesCoreExtensionsTests { - [TestMethod] + [Fact] public void KubernetesClientIsAdded() { - // arrange var services = new ServiceCollection(); - // act services.AddKubernetesCore(); - // assert var serviceProvider = services.BuildServiceProvider(); - serviceProvider.GetService().ShouldNotBeNull(); + Assert.NotNull(serviceProvider.GetService()); } - [TestMethod] + [Fact] public void HelperServicesAreAdded() { - // arrange var services = new ServiceCollection(); - // act services.AddKubernetesCore(); - // assert var serviceProvider = services.BuildServiceProvider(); - serviceProvider.GetService().ShouldNotBeNull(); + Assert.NotNull(serviceProvider.GetService()); } - [TestMethod] + [Fact] public void ExistingClientIsNotReplaced() { - // arrange using var client = new k8s.Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); var services = new ServiceCollection(); - // act services.AddSingleton(client); services.AddKubernetesCore(); - // assert var serviceProvider = services.BuildServiceProvider(); - serviceProvider.GetService().ShouldBeSameAs(client); + Assert.Same(client, serviceProvider.GetService()); } } } diff --git a/src/OperatorFramework/test/UnitTests/Microsoft.Kubernetes.UnitTests.csproj b/src/OperatorFramework/test/UnitTests/Microsoft.Kubernetes.UnitTests.csproj index 186012b06..ade2e24ea 100644 --- a/src/OperatorFramework/test/UnitTests/Microsoft.Kubernetes.UnitTests.csproj +++ b/src/OperatorFramework/test/UnitTests/Microsoft.Kubernetes.UnitTests.csproj @@ -21,10 +21,7 @@ - - - diff --git a/src/OperatorFramework/test/UnitTests/NamespacedNameTests.cs b/src/OperatorFramework/test/UnitTests/NamespacedNameTests.cs index deeeae0b9..65eb5dae8 100644 --- a/src/OperatorFramework/test/UnitTests/NamespacedNameTests.cs +++ b/src/OperatorFramework/test/UnitTests/NamespacedNameTests.cs @@ -2,42 +2,36 @@ // Licensed under the MIT License. using k8s.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System.Collections.Generic; +using Xunit; namespace Microsoft.Kubernetes { - [TestClass] public class NamespacedNameTests { - [TestMethod] + [Fact] public void WorksAsDictionaryKey() { - // arrange var dictionary = new Dictionary(); var name1 = new NamespacedName("ns", "n1"); var name2 = new NamespacedName("ns", "n2"); var name3 = new NamespacedName("ns", "n3"); - // act dictionary[name1] = "one"; dictionary[name1] = "one again"; dictionary[name2] = "two"; - // assert - dictionary.ShouldSatisfyAllConditions( - () => dictionary.ShouldContainKeyAndValue(name1, "one again"), - () => dictionary.ShouldContainKeyAndValue(name2, "two"), - () => dictionary.ShouldNotContainKey(name3)); + Assert.Contains(new KeyValuePair(name1, "one again"), dictionary); + Assert.Contains(new KeyValuePair(name2, "two"), dictionary); + Assert.DoesNotContain(name3, dictionary.Keys); } - [TestMethod] - [DataRow("ns", "n1", "ns", "n1", true)] - [DataRow("ns", "n1", "ns", "n2", false)] - [DataRow("ns", "n1", "ns-x", "n1", false)] - [DataRow(null, "n1", null, "n1", true)] - [DataRow(null, "n1", null, "n2", false)] + [Theory] + [InlineData("ns", "n1", "ns", "n1", true)] + [InlineData("ns", "n1", "ns", "n2", false)] + [InlineData("ns", "n1", "ns-x", "n1", false)] + [InlineData(null, "n1", null, "n1", true)] + [InlineData(null, "n1", null, "n2", false)] public void EqualityAndInequality( string namespace1, string name1, @@ -45,11 +39,9 @@ public void EqualityAndInequality( string name2, bool shouldBeEqual) { - // arrange var namespacedName1 = new NamespacedName(namespace1, name1); var namespacedName2 = new NamespacedName(namespace2, name2); - // act var areEqual = namespacedName1 == namespacedName2; var areNotEqual = namespacedName1 != namespacedName2; #pragma warning disable CS1718 // Comparison made to same variable @@ -59,19 +51,17 @@ public void EqualityAndInequality( var sameNotEqual2 = namespacedName2 != namespacedName2; #pragma warning restore CS1718 // Comparison made to same variable - // assert - areEqual.ShouldNotBe(areNotEqual); - areEqual.ShouldBe(shouldBeEqual); - sameEqual1.ShouldBeTrue(); - sameNotEqual1.ShouldBeFalse(); - sameEqual2.ShouldBeTrue(); - sameNotEqual2.ShouldBeFalse(); + Assert.NotEqual(areNotEqual, areEqual); + Assert.Equal(shouldBeEqual, areEqual); + Assert.True(sameEqual1); + Assert.False(sameNotEqual1); + Assert.True(sameEqual2); + Assert.False(sameNotEqual2); } - [TestMethod] + [Fact] public void NamespaceAndNameFromResource() { - // arrange var resource = new V1ConfigMap( apiVersion: V1ConfigMap.KubeApiVersion, kind: V1ConfigMap.KubeKind, @@ -79,30 +69,25 @@ public void NamespaceAndNameFromResource() name: "the-name", namespaceProperty: "the-namespace")); - // act var nn = NamespacedName.From(resource); - // assert - nn.Name.ShouldBe("the-name"); - nn.Namespace.ShouldBe("the-namespace"); + Assert.Equal("the-name", nn.Name); + Assert.Equal("the-namespace", nn.Namespace); } - [TestMethod] + [Fact] public void JustNameFromClusterResource() { - // arrange var resource = new V1ClusterRole( apiVersion: V1ClusterRole.KubeApiVersion, kind: V1ClusterRole.KubeKind, metadata: new V1ObjectMeta( name: "the-name")); - // act var nn = NamespacedName.From(resource); - // assert - nn.Name.ShouldBe("the-name"); - nn.Namespace.ShouldBeNull(); + Assert.Equal("the-name", nn.Name); + Assert.Null(nn.Namespace); } } } diff --git a/src/OperatorFramework/test/UnitTests/Operator/OperatorHandlerTests.cs b/src/OperatorFramework/test/UnitTests/Operator/OperatorHandlerTests.cs index 6a0f76bd6..914aaced9 100644 --- a/src/OperatorFramework/test/UnitTests/Operator/OperatorHandlerTests.cs +++ b/src/OperatorFramework/test/UnitTests/Operator/OperatorHandlerTests.cs @@ -10,21 +10,17 @@ using Microsoft.Kubernetes.Fakes; using Microsoft.Kubernetes.Operator.Caches; using Microsoft.Kubernetes.Operator.Generators; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Shouldly; using System.Collections.Generic; +using Xunit; namespace Microsoft.Kubernetes.Operator { - [TestClass] public class OperatorHandlerTests { - - [TestMethod] + [Fact] public void NotifyWithPrimaryResourceCausesCacheEntryAndQueueItem() { - // arrange var generator = Mock.Of>(); var typicalInformer = new FakeResourceInformer(); var podInformer = new FakeResourceInformer(); @@ -53,7 +49,6 @@ public void NotifyWithPrimaryResourceCausesCacheEntryAndQueueItem() }) .Build(); - // act var handler = host.Services.GetRequiredService>(); var typical = new TypicalResource @@ -94,17 +89,16 @@ public void NotifyWithPrimaryResourceCausesCacheEntryAndQueueItem() podInformer.Callback(WatchEventType.Added, unrelatedPod); podInformer.Callback(WatchEventType.Added, relatedPod); - // assert var expectedName = new NamespacedName("test-namespace", "test-name"); - addCalls.ShouldBe(new[] { expectedName, expectedName }); + Assert.Equal(new[] { expectedName, expectedName }, addCalls); - cache.TryGetWorkItem(expectedName, out var cacheItem).ShouldBeTrue(); + Assert.True(cache.TryGetWorkItem(expectedName, out var cacheItem)); - cacheItem.Resource.ShouldBe(typical); + Assert.Equal(typical, cacheItem.Resource); - var related = cacheItem.Related.ShouldHaveSingleItem(); - related.Key.ShouldBe(GroupKindNamespacedName.From(relatedPod)); - related.Value.ShouldBe(relatedPod); + var related = Assert.Single(cacheItem.Related); + Assert.Equal(GroupKindNamespacedName.From(relatedPod), related.Key); + Assert.Equal(relatedPod, related.Value); } } } diff --git a/src/OperatorFramework/test/UnitTests/ResourceKinds/OpenApi/OpenApiResourceKindProviderTests.cs b/src/OperatorFramework/test/UnitTests/ResourceKinds/OpenApi/OpenApiResourceKindProviderTests.cs index 2b8e87379..3f6a5a64d 100644 --- a/src/OperatorFramework/test/UnitTests/ResourceKinds/OpenApi/OpenApiResourceKindProviderTests.cs +++ b/src/OperatorFramework/test/UnitTests/ResourceKinds/OpenApi/OpenApiResourceKindProviderTests.cs @@ -1,221 +1,186 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.ResourceKinds.OpenApi { - [TestClass] public class OpenApiResourceKindProviderTests { - public static OpenApiResourceKindProvider SharedProvider { get; set; } + public static OpenApiResourceKindProvider SharedProvider { get; set; } = new(new FakeLogger()); - [ClassInitialize] - public static void ClassInitialize(TestContext testContext) - { - if (testContext is null) - { - throw new System.ArgumentNullException(nameof(testContext)); - } - - SharedProvider = new OpenApiResourceKindProvider(new FakeLogger()); - } - - [TestMethod] - [DataRow("v1", "Pod")] - [DataRow("rbac.authorization.k8s.io/v1", "RoleBinding")] + [Theory] + [InlineData("v1", "Pod")] + [InlineData("rbac.authorization.k8s.io/v1", "RoleBinding")] public async Task BuiltInResourceKindsCanBeFound(string apiVersion, string kind) { - // arrange var provider = SharedProvider; - // act var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind); - // assert - resourceKind.ShouldNotBeNull(); - resourceKind.ApiVersion.ShouldBe(apiVersion); - resourceKind.Kind.ShouldBe(kind); - resourceKind.Schema.ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.MergeObject); + Assert.NotNull(resourceKind); + Assert.Equal(apiVersion, resourceKind.ApiVersion); + Assert.Equal(kind, resourceKind.Kind); + Assert.NotNull(resourceKind.Schema); + Assert.Equal(ElementMergeStrategy.MergeObject, resourceKind.Schema.MergeStrategy); } - - - [TestMethod] - [DataRow("v1", "Pod")] - [DataRow("rbac.authorization.k8s.io/v1", "RoleBinding")] + [Theory] + [InlineData("v1", "Pod")] + [InlineData("rbac.authorization.k8s.io/v1", "RoleBinding")] public async Task UnknownPropertiesComeBackAsMergeStrategyUnknown(string apiVersion, string kind) { - // arrange var provider = SharedProvider; - // act var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind); - // assert - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("badPropertyName").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.Unknown); + Assert.NotNull(resourceKind.Schema); + var property = resourceKind.Schema.GetPropertyElementType("badPropertyName"); + Assert.NotNull(property); + Assert.Equal(ElementMergeStrategy.Unknown, property.MergeStrategy); } - [TestMethod] - [DataRow("v1", "Pod")] - [DataRow("rbac.authorization.k8s.io/v1", "RoleBinding")] + [Theory] + [InlineData("v1", "Pod")] + [InlineData("rbac.authorization.k8s.io/v1", "RoleBinding")] public async Task ApiVersionAndKindArePrimative(string apiVersion, string kind) { - // arrange var provider = SharedProvider; - // act var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind); - // assert - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("apiVersion").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.ReplacePrimative); + Assert.NotNull(resourceKind.Schema); - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("kind").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.ReplacePrimative); + var apiVersionProperty = resourceKind.Schema.GetPropertyElementType("apiVersion"); + Assert.NotNull(apiVersionProperty); + Assert.Equal(ElementMergeStrategy.ReplacePrimative, apiVersionProperty.MergeStrategy); + + var kindProperty = resourceKind.Schema.GetPropertyElementType("kind"); + Assert.NotNull(kindProperty); + Assert.Equal(ElementMergeStrategy.ReplacePrimative, kindProperty.MergeStrategy); } - [TestMethod] - [DataRow("v1", "Pod")] - [DataRow("rbac.authorization.k8s.io/v1", "RoleBinding")] + [Theory] + [InlineData("v1", "Pod")] + [InlineData("rbac.authorization.k8s.io/v1", "RoleBinding")] public async Task MetadataNameAndNamespaceArePrimative(string apiVersion, string kind) { - // arrange var provider = SharedProvider; - // act var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind); - // assert - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("metadata").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.MergeObject); + Assert.NotNull(resourceKind); + Assert.NotNull(resourceKind.Schema); + var metadata = resourceKind.Schema.GetPropertyElementType("metadata"); + Assert.NotNull(metadata); + Assert.Equal(ElementMergeStrategy.MergeObject, metadata.MergeStrategy); - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("metadata").ShouldNotBeNull() - .GetPropertyElementType("name").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.ReplacePrimative); + var name = metadata.GetPropertyElementType("name"); + Assert.NotNull(name); + Assert.Equal(ElementMergeStrategy.ReplacePrimative, name.MergeStrategy); - resourceKind.Schema.ShouldNotBeNull() - .GetPropertyElementType("metadata").ShouldNotBeNull() - .GetPropertyElementType("namespace").ShouldNotBeNull() - .MergeStrategy.ShouldBe(ElementMergeStrategy.ReplacePrimative); + var @namespace = metadata.GetPropertyElementType("namespace"); + Assert.NotNull(@namespace); + Assert.Equal(ElementMergeStrategy.ReplacePrimative, @namespace.MergeStrategy); } - [TestMethod] + [Fact] public async Task ResourceKindAreCachedByAtProviderLevel() { - // arrange var provider1 = new OpenApiResourceKindProvider(new FakeLogger()); var provider2 = new OpenApiResourceKindProvider(new FakeLogger()); - // act var pod1a = await provider1.GetResourceKindAsync("v1", "Pod"); var pod1b = await provider1.GetResourceKindAsync("v1", "Pod"); var pod2a = await provider2.GetResourceKindAsync("v1", "Pod"); var pod2b = await provider2.GetResourceKindAsync("v1", "Pod"); - // assert - pod1a.ShouldBeSameAs(pod1b); - pod2a.ShouldBeSameAs(pod2b); - pod1a.ShouldNotBeSameAs(pod2a); - pod1a.ShouldNotBeSameAs(pod2b); - pod1b.ShouldNotBeSameAs(pod2a); - pod1b.ShouldNotBeSameAs(pod2b); + Assert.Same(pod1b, pod1a); + Assert.Same(pod2b, pod2a); + Assert.NotSame(pod2a, pod1a); + Assert.NotSame(pod2b, pod1a); + Assert.NotSame(pod2a, pod1b); + Assert.NotSame(pod2b, pod1b); } - [TestMethod] + [Fact] public async Task MergeKeyAttributesAreRecognized() { - // arrange var provider = SharedProvider; - // act var pod = await provider.GetResourceKindAsync("v1", "Pod"); - // assert - var containers = pod.ShouldNotBeNull() - .Schema.ShouldNotBeNull() - .GetPropertyElementType("spec").ShouldNotBeNull() - .GetPropertyElementType("containers").ShouldNotBeNull(); + Assert.NotNull(pod); + Assert.NotNull(pod.Schema); + var spec = pod.Schema.GetPropertyElementType("spec"); + Assert.NotNull(spec); + var containers = spec.GetPropertyElementType("containers"); + Assert.NotNull(containers); - containers.MergeStrategy.ShouldBe(ElementMergeStrategy.MergeListOfObject); - containers.MergeKey.ShouldBe("name"); + Assert.Equal(ElementMergeStrategy.MergeListOfObject, containers.MergeStrategy); + Assert.Equal("name", containers.MergeKey); } - [TestMethod] + [Fact] public async Task ArrayOfPrimativeWithoutExtensionsIsReplaceListOfPrimative() { - // arrange var provider = SharedProvider; - // act var pod = await provider.GetResourceKindAsync("v1", "Pod"); - // assert - var args = pod.ShouldNotBeNull() - .Schema.ShouldNotBeNull() - .GetPropertyElementType("spec").ShouldNotBeNull() - .GetPropertyElementType("containers").ShouldNotBeNull() - .GetCollectionElementType().ShouldNotBeNull() - .GetPropertyElementType("args"); - - args.MergeStrategy.ShouldBe(ElementMergeStrategy.ReplaceListOfPrimative); + Assert.NotNull(pod); + Assert.NotNull(pod.Schema); + var spec = pod.Schema.GetPropertyElementType("spec"); + Assert.NotNull(spec); + var containers = spec.GetPropertyElementType("containers"); + Assert.NotNull(containers); + var containersCollection = containers.GetCollectionElementType(); + Assert.NotNull(containersCollection); + var args = containersCollection.GetPropertyElementType("args"); + + Assert.Equal(ElementMergeStrategy.ReplaceListOfPrimative, args.MergeStrategy); } - [TestMethod] + [Fact] public async Task ArrayOfPrimativeCanHaveMergeExtensions() { - // arrange var provider = SharedProvider; - // act var pod = await provider.GetResourceKindAsync("v1", "Pod"); - // assert - var finalizers = pod.ShouldNotBeNull() - .Schema.ShouldNotBeNull() - .GetPropertyElementType("metadata").ShouldNotBeNull() - .GetPropertyElementType("finalizers").ShouldNotBeNull(); + Assert.NotNull(pod); + Assert.NotNull(pod.Schema); + var metadata = pod.Schema.GetPropertyElementType("metadata"); + Assert.NotNull(metadata); + var finalizers = metadata.GetPropertyElementType("finalizers"); + Assert.NotNull(finalizers); - finalizers.MergeStrategy.ShouldBe(ElementMergeStrategy.MergeListOfPrimative); + Assert.Equal(ElementMergeStrategy.MergeListOfPrimative, finalizers.MergeStrategy); } - [TestMethod] - [DataRow("v2", "Pod")] - [DataRow("v1", "FluxCapacitor")] + [Theory] + [InlineData("v2", "Pod")] + [InlineData("v1", "FluxCapacitor")] public async Task NotFoundResourceKindComesProducesNull(string apiVersion, string kind) { - // arrange var provider = SharedProvider; - // act var resourceKind = await provider.GetResourceKindAsync(apiVersion, kind); - // assert - resourceKind.ShouldBeNull(); + Assert.Null(resourceKind); } - [TestMethod] + [Fact] public async Task DictionaryHasInformationAboutContents() { - // arrange var provider = SharedProvider; - // act var pod = await provider.GetResourceKindAsync("v1", "Pod"); var labels = pod.Schema.GetPropertyElementType("metadata").GetPropertyElementType("labels"); - // assert - labels.MergeStrategy.ShouldBe(ElementMergeStrategy.MergeMap); - labels.GetCollectionElementType().MergeStrategy.ShouldBe(ElementMergeStrategy.ReplacePrimative); + Assert.Equal(ElementMergeStrategy.MergeMap, labels.MergeStrategy); + Assert.Equal(ElementMergeStrategy.ReplacePrimative, labels.GetCollectionElementType().MergeStrategy); } } } diff --git a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherOpenApiSchemaTests.cs b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherOpenApiSchemaTests.cs index 2d9e4856c..c7e8a1942 100644 --- a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherOpenApiSchemaTests.cs +++ b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherOpenApiSchemaTests.cs @@ -5,118 +5,105 @@ using Microsoft.Kubernetes.ResourceKinds.OpenApi; using Microsoft.Kubernetes.Resources.Models; using Microsoft.Kubernetes.Utils; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Resources { - [TestClass] public class ResourcePatcherOpenApiSchemaTests : ResourcePatcherTestsBase { - public static ResourceKindManager SharedManager { get; set; } + public static ResourceKindManager SharedManager { get; set; } = new(new[] { new OpenApiResourceKindProvider(new FakeLogger()) }); - [ClassInitialize] - public static void ClassInitialize(TestContext testContext) - { - if (testContext is null) - { - throw new System.ArgumentNullException(nameof(testContext)); - } - - SharedManager = new ResourceKindManager(new[] { new OpenApiResourceKindProvider(new FakeLogger()) }); - } - - [TestInitialize] - public void TestInitialize() + public ResourcePatcherOpenApiSchemaTests() { Manager = SharedManager; } - [TestMethod] + [Fact] public async Task PrimativePropertiesCanBePatched() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task PrimativePropertiesCanAddedAndRemoved() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task DictionaryOfPrimativesCanBePatched() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task DictionaryOfPrimativesAddedAndRemoved() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task DictionaryOnlyRemoveIfWasLastApplied() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task ArrayOfPrimativesReplacedEntirelyWhenDifferent() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfPrimativesCanAddItems() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfPrimativesCanRemoveItemsIfLastApplied() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfPrimativesPreserveOrderOfAppliedValues() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfPrimativesPreserveItemsIfNotLastApplied() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfObjectsCanAddItems() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfObjectsCanRemoveItemsIfLastApplied() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfObjectsPreserveItemsIfNotLastApplied() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergedArrayOfObjectsPreserveOrderOfLiveValues() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task ArrayWithMergeKeyTreatedAsDictionary() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); diff --git a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherTestsBase.cs b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherTestsBase.cs index 2911eb67d..18be2d681 100644 --- a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherTestsBase.cs +++ b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherTestsBase.cs @@ -3,16 +3,14 @@ using Microsoft.Kubernetes.ResourceKinds; using Microsoft.Kubernetes.Resources.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Resources { - [TestClass] public abstract partial class ResourcePatcherTestsBase { public virtual IResourceKindManager Manager { get; set; } @@ -33,10 +31,8 @@ public async Task RunStandardTest(StandardTestYaml testYaml) private async Task RunThreeWayMerge(StandardTestYaml testYaml) { - // arrange IResourcePatcher patcher = new ResourcePatcher(); - // act var parameters = new CreatePatchParameters { ApplyResource = testYaml.Apply, @@ -53,17 +49,17 @@ private async Task RunThreeWayMerge(StandardTestYaml testYaml) var patch = patcher.CreateJsonPatch(parameters); - // assert - var operations = new ResourceSerializers().Convert(patch); - operations.ShouldBe(testYaml.Patch, ignoreOrder: true); + var operations = new ResourceSerializers().Convert>(patch); + + var expected = testYaml.Patch.OrderBy(op => op.ToString()).ToList(); + operations = operations.OrderBy(op => op.ToString()).ToList(); + Assert.Equal(expected, operations); } private async Task RunApplyLiveOnlyMerge(StandardTestYaml testYaml) { - // arrange IResourcePatcher patcher = new ResourcePatcher(); - // act var parameters = new CreatePatchParameters { ApplyResource = testYaml.Apply, @@ -79,9 +75,11 @@ private async Task RunApplyLiveOnlyMerge(StandardTestYaml testYaml) var patch = patcher.CreateJsonPatch(parameters); - // assert var operations = new ResourceSerializers().Convert>(patch); - operations.ShouldBe(testYaml.Patch, ignoreOrder: true); + + var expected = testYaml.Patch.OrderBy(op => op.ToString()).ToList(); + operations = operations.OrderBy(op => op.ToString()).ToList(); + Assert.Equal(expected, operations); } } } diff --git a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherUnknownSchemaTests.cs b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherUnknownSchemaTests.cs index 39fbcb543..256776f6d 100644 --- a/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherUnknownSchemaTests.cs +++ b/src/OperatorFramework/test/UnitTests/Resources/ResourcePatcherUnknownSchemaTests.cs @@ -3,75 +3,74 @@ using Microsoft.Kubernetes.Resources.Models; using Microsoft.Kubernetes.Utils; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; +using Xunit; namespace Microsoft.Kubernetes.Resources { - [TestClass] public class ResourcePatcherUnknownSchemaTests : ResourcePatcherTestsBase { - [TestMethod] + [Fact] public async Task ObjectPropertyIsAddedWhenMissing() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task NestedPropertyIsAddedWhenMissing() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task TildaAndForwardSlashAreEscapedInPatchPaths() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task AdditionalPropertyIsAddedWhenMissing() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task PropertiesOfStringAreOnlyRemovedWhenPreviouslyAdded() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task PropertiesOfObjectAreOnlyRemovedWhenPreviouslyAdded() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task PropertiesOfNullAreOnlyRemovedWhenPreviouslyAdded() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task ArrayAreAddedAndRemovedEntirelyAsNeeded() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task ArraysAreReplacedEntirelyWhenDifferent() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergingWhenApplyElementTypeHasChanged() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); } - [TestMethod] + [Fact] public async Task MergingWhenLiveElementTypeHasChanged() { await RunStandardTest(TestYaml.LoadFromEmbeddedStream()); diff --git a/src/OperatorFramework/test/UnitTests/Resources/ResourceSerializersTests.cs b/src/OperatorFramework/test/UnitTests/Resources/ResourceSerializersTests.cs index f8a6be14d..69da9c2b2 100644 --- a/src/OperatorFramework/test/UnitTests/Resources/ResourceSerializersTests.cs +++ b/src/OperatorFramework/test/UnitTests/Resources/ResourceSerializersTests.cs @@ -2,22 +2,20 @@ // Licensed under the MIT License. using k8s.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; -using Shouldly; +using System; using System.Collections.Generic; +using Xunit; namespace Microsoft.Kubernetes.Resources { - [TestClass] public class ResourceSerializersTests { private IResourceSerializers Serializers { get; } = new ResourceSerializers(); - [TestMethod] + [Fact] public void ResourceSerializesToJson() { - // arrange var resource = new V1Role( apiVersion: $"{V1Role.KubeGroup}/{V1Role.KubeApiVersion}", kind: V1Role.KubeKind, @@ -31,22 +29,19 @@ public void ResourceSerializesToJson() verbs: new []{"*"}), }); - // act var json = Serializers.SerializeJson(resource); - // assert - json.ShouldContain(@"""kind"":""Role"""); - json.ShouldContain(@"""apiVersion"":""rbac.authorization.k8s.io/v1"""); - json.ShouldContain(@"""name"":""the-name"""); - json.ShouldContain(@"""namespace"":""the-namespace"""); - json.ShouldContain(@"""resourceNames"":[""*""]"); - json.ShouldContain(@"""verbs"":[""*""]"); + Assert.Contains(@"""kind"":""Role""", json, StringComparison.Ordinal); + Assert.Contains(@"""apiVersion"":""rbac.authorization.k8s.io/v1""", json, StringComparison.Ordinal); + Assert.Contains(@"""name"":""the-name""", json, StringComparison.Ordinal); + Assert.Contains(@"""namespace"":""the-namespace""", json, StringComparison.Ordinal); + Assert.Contains(@"""resourceNames"":[""*""]", json, StringComparison.Ordinal); + Assert.Contains(@"""verbs"":[""*""]", json, StringComparison.Ordinal); } - [TestMethod] + [Fact] public void DictionarySerializesToJson() { - // arrange var dictionary = new Dictionary { { "apiVersion", $"{V1Role.KubeGroup}/{V1Role.KubeApiVersion}" }, { "kind", V1Role.KubeKind }, @@ -62,22 +57,19 @@ public void DictionarySerializesToJson() }}, }; - // act var json = Serializers.SerializeJson(dictionary); - // assert - json.ShouldContain(@"""kind"":""Role"""); - json.ShouldContain(@"""apiVersion"":""rbac.authorization.k8s.io/v1"""); - json.ShouldContain(@"""name"":""the-name"""); - json.ShouldContain(@"""namespace"":""the-namespace"""); - json.ShouldContain(@"""resourceNames"":[""*""]"); - json.ShouldContain(@"""verbs"":[""*""]"); + Assert.Contains(@"""kind"":""Role""", json, StringComparison.Ordinal); + Assert.Contains(@"""apiVersion"":""rbac.authorization.k8s.io/v1""", json, StringComparison.Ordinal); + Assert.Contains(@"""name"":""the-name""", json, StringComparison.Ordinal); + Assert.Contains(@"""namespace"":""the-namespace""", json, StringComparison.Ordinal); + Assert.Contains(@"""resourceNames"":[""*""]", json, StringComparison.Ordinal); + Assert.Contains(@"""verbs"":[""*""]", json, StringComparison.Ordinal); } - [TestMethod] + [Fact] public void DeserializeJsonToResource() { - // arrange var json = $@" {{ ""apiVersion"": ""{V1Role.KubeGroup}/{V1Role.KubeApiVersion}"", @@ -93,23 +85,20 @@ public void DeserializeJsonToResource() }} "; - // act var role = Serializers.DeserializeJson(json); - // assert - role.ApiGroupAndVersion().ShouldBe(("rbac.authorization.k8s.io", "v1")); - role.Kind.ShouldBe("Role"); - role.Name().ShouldBe("the-name"); - role.Namespace().ShouldBe("the-namespace"); - var rule = role.Rules.ShouldHaveSingleItem(); - rule.ResourceNames.ShouldBe(new[] { "*" }); - rule.Verbs.ShouldBe(new[] { "*" }); + Assert.Equal(("rbac.authorization.k8s.io", "v1"), role.ApiGroupAndVersion()); + Assert.Equal("Role", role.Kind); + Assert.Equal("the-name", role.Name()); + Assert.Equal("the-namespace", role.Namespace()); + var rule = Assert.Single(role.Rules); + Assert.Equal(new[] { "*" }, rule.ResourceNames); + Assert.Equal(new[] { "*" }, rule.Verbs); } - [TestMethod] + [Fact] public void DeserializeYamlToResource() { - // arrange var yaml = $@" apiVersion: {V1Role.KubeGroup}/{V1Role.KubeApiVersion} kind: Role @@ -123,23 +112,20 @@ public void DeserializeYamlToResource() - ""*"" "; - // act var role = Serializers.DeserializeYaml(yaml); - // assert - role.ApiGroupAndVersion().ShouldBe(("rbac.authorization.k8s.io", "v1")); - role.Kind.ShouldBe("Role"); - role.Name().ShouldBe("the-name"); - role.Namespace().ShouldBe("the-namespace"); - var rule = role.Rules.ShouldHaveSingleItem(); - rule.ResourceNames.ShouldBe(new[] { "*" }); - rule.Verbs.ShouldBe(new[] { "*" }); + Assert.Equal(("rbac.authorization.k8s.io", "v1"), role.ApiGroupAndVersion()); + Assert.Equal("Role", role.Kind); + Assert.Equal("the-name", role.Name()); + Assert.Equal("the-namespace", role.Namespace()); + var rule = Assert.Single(role.Rules); + Assert.Equal(new[] { "*" }, rule.ResourceNames); + Assert.Equal(new[] { "*" }, rule.Verbs); } - [TestMethod] + [Fact] public void ConvertDictionaryToResource() { - // arrange var dictionary = new Dictionary { { "apiVersion", $"{V1Role.KubeGroup}/{V1Role.KubeApiVersion}" }, { "kind", V1Role.KubeKind }, @@ -155,41 +141,36 @@ public void ConvertDictionaryToResource() }}, }; - // act var role = Serializers.Convert(dictionary); - // assert - role.ApiGroupAndVersion().ShouldBe(("rbac.authorization.k8s.io", "v1")); - role.Kind.ShouldBe("Role"); - role.Name().ShouldBe("the-name"); - role.Namespace().ShouldBe("the-namespace"); - var rule = role.Rules.ShouldHaveSingleItem(); - rule.ResourceNames.ShouldBe(new[] { "*" }); - rule.Verbs.ShouldBe(new[] { "*" }); + Assert.Equal(("rbac.authorization.k8s.io", "v1"), role.ApiGroupAndVersion()); + Assert.Equal("Role", role.Kind); + Assert.Equal("the-name", role.Name()); + Assert.Equal("the-namespace", role.Namespace()); + var rule = Assert.Single(role.Rules); + Assert.Equal(new[] { "*" }, rule.ResourceNames); + Assert.Equal(new[] { "*" }, rule.Verbs); } - [TestMethod] + [Fact] public void DeserializeUntypedYamlWithIntDoubleAndBool() { - // arrange var yaml = @" an-object: an-integer: 1 a-float: 1.0 a-bool: true - a-string: 1.0. + a-string: ""1.0."" "; - // act var data = Serializers.DeserializeYaml(yaml); - // assert - var root = data.ShouldBeOfType(); - var anObject = root["an-object"].ShouldBeOfType(); - anObject["an-integer"].Type.ShouldBe(JTokenType.Integer); - anObject["a-float"].Type.ShouldBe(JTokenType.Float); - anObject["a-bool"].Type.ShouldBe(JTokenType.Boolean); - anObject["a-string"].Type.ShouldBe(JTokenType.String); + var root = Assert.IsType(data); + var anObject = Assert.IsType(root["an-object"]); + Assert.Equal(JTokenType.Integer, anObject["an-integer"].Type); + Assert.Equal(JTokenType.Float, anObject["a-float"].Type); + Assert.Equal(JTokenType.Boolean, anObject["a-bool"].Type); + Assert.Equal(JTokenType.String, anObject["a-string"].Type); } } } diff --git a/src/ReverseProxy/Configuration/IProxyConfigFilter.cs b/src/ReverseProxy/Configuration/IProxyConfigFilter.cs index d3eb4f374..a1f8e5b24 100644 --- a/src/ReverseProxy/Configuration/IProxyConfigFilter.cs +++ b/src/ReverseProxy/Configuration/IProxyConfigFilter.cs @@ -15,6 +15,7 @@ public interface IProxyConfigFilter /// Allows modification of a cluster configuration. /// /// The instance to configure. + /// ValueTask ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel); /// @@ -22,6 +23,7 @@ public interface IProxyConfigFilter /// /// The instance to configure. /// The instance related to . + /// ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig? cluster, CancellationToken cancel); } } diff --git a/src/ReverseProxy/Forwarder/HttpForwarder.cs b/src/ReverseProxy/Forwarder/HttpForwarder.cs index f94996947..99029a25a 100644 --- a/src/ReverseProxy/Forwarder/HttpForwarder.cs +++ b/src/ReverseProxy/Forwarder/HttpForwarder.cs @@ -585,8 +585,8 @@ private async ValueTask HandleUpgradedResponse(HttpContext conte // :: Step 7-A-2: Copy duplex streams using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(); - var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, _clock, activityCancellationSource).AsTask(); - var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, _clock, activityCancellationSource).AsTask(); + var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask(); + var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask(); // Make sure we report the first failure. var firstTask = await Task.WhenAny(requestTask, responseTask); @@ -643,7 +643,7 @@ ForwarderError ReportResult(HttpContext context, bool reqeuest, StreamCopyResult if (destinationResponseContent != null) { using var destinationResponseStream = await destinationResponseContent.ReadAsStreamAsync(); - return await StreamCopier.CopyAsync(isRequest: false, destinationResponseStream, clientResponseStream, _clock, activityCancellationSource); + return await StreamCopier.CopyAsync(isRequest: false, destinationResponseStream, clientResponseStream, _clock, activityCancellationSource, activityCancellationSource.Token); } return (StreamCopyResult.Success, null); diff --git a/src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs b/src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs index 4625ff6b7..554067e79 100644 --- a/src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs +++ b/src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs @@ -16,6 +16,7 @@ public static class IHttpForwarderExtensions /// /// Forwards the incoming request to the destination server, and the response back to the client. /// + /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. @@ -34,6 +35,7 @@ public static ValueTask SendAsync(this IHttpForwarder forwarder, /// /// Forwards the incoming request to the destination server, and the response back to the client. /// + /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. @@ -53,6 +55,7 @@ public static ValueTask SendAsync(this IHttpForwarder forwarder, /// /// Forwards the incoming request to the destination server, and the response back to the client. /// + /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. @@ -67,6 +70,7 @@ public static ValueTask SendAsync(this IHttpForwarder forwarder, /// /// Forwards the incoming request to the destination server, and the response back to the client. /// + /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. diff --git a/src/ReverseProxy/Forwarder/RequestUtilities.cs b/src/ReverseProxy/Forwarder/RequestUtilities.cs index 82ba5fb2e..724f26e59 100644 --- a/src/ReverseProxy/Forwarder/RequestUtilities.cs +++ b/src/ReverseProxy/Forwarder/RequestUtilities.cs @@ -335,10 +335,14 @@ internal static void AddHeader(HttpRequestMessage request, string headerName, St internal static void RemoveHeader(HttpRequestMessage request, string headerName) { - if (!request.Headers.Remove(headerName)) + if (_contentHeaders.Contains(headerName)) { request.Content?.Headers.Remove(headerName); } + else + { + request.Headers.Remove(headerName); + } } } } diff --git a/src/ReverseProxy/Forwarder/StreamCopier.cs b/src/ReverseProxy/Forwarder/StreamCopier.cs index 88bb9fc66..61fb9087b 100644 --- a/src/ReverseProxy/Forwarder/StreamCopier.cs +++ b/src/ReverseProxy/Forwarder/StreamCopier.cs @@ -25,7 +25,7 @@ internal static class StreamCopier /// Based on Microsoft.AspNetCore.Http.StreamCopyOperationInternal.CopyToAsync. /// See: . /// - public static async ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, IClock clock, ActivityCancellationTokenSource activityToken) + public static async ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, IClock clock, ActivityCancellationTokenSource activityToken, CancellationToken cancellation) { _ = input ?? throw new ArgumentNullException(nameof(input)); _ = output ?? throw new ArgumentNullException(nameof(output)); @@ -40,7 +40,6 @@ internal static class StreamCopier var readTime = TimeSpan.Zero; var writeTime = TimeSpan.Zero; var firstReadTime = TimeSpan.FromMilliseconds(-1); - var cancellation = activityToken.Token; try { diff --git a/src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs b/src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs index 31dd00eca..131465724 100644 --- a/src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs +++ b/src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs @@ -56,7 +56,7 @@ public StreamCopyHttpContent(Stream source, bool autoFlushHttpClientOutgoingStre /// /// Gets a that completes in successful or failed state - /// mimicking the result of . + /// mimicking the result of SerializeToStreamAsync. /// public Task<(StreamCopyResult, Exception?)> ConsumptionTask => _tcs.Task; @@ -137,17 +137,19 @@ async Task SerializeToStreamAsync(Stream stream, TransportContext? context, Canc // On HTTP/1.1: Linked HttpContext.RequestAborted + Request Timeout // On HTTP/2.0: SocketsHttpHandler error / the server wants us to stop sending content / H2 connection closed // _cancellation will be the same as cancellationToken for HTTP/1.1, so we can avoid the overhead of linking them - CancellationTokenRegistration registration = default; + CancellationTokenSource? linkedCts = null; #if NET if (_activityToken.Token != cancellationToken) { Debug.Assert(cancellationToken.CanBeCanceled); - registration = _activityToken.LinkTo(cancellationToken); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_activityToken.Token, cancellationToken); + cancellationToken = linkedCts.Token; } #else // On .NET Core 3.1, cancellationToken will always be CancellationToken.None Debug.Assert(!cancellationToken.CanBeCanceled); + cancellationToken = _activityToken.Token; #endif try @@ -166,7 +168,7 @@ async Task SerializeToStreamAsync(Stream stream, TransportContext? context, Canc // https://github.com/dotnet/corefx/issues/39586#issuecomment-516210081 try { - await stream.FlushAsync(_activityToken.Token); + await stream.FlushAsync(cancellationToken); } catch (OperationCanceledException oex) { @@ -179,7 +181,7 @@ async Task SerializeToStreamAsync(Stream stream, TransportContext? context, Canc return; } - var (result, error) = await StreamCopier.CopyAsync(isRequest: true, _source, stream, _clock, _activityToken); + var (result, error) = await StreamCopier.CopyAsync(isRequest: true, _source, stream, _clock, _activityToken, cancellationToken); _tcs.TrySetResult((result, error)); // Check for errors that weren't the result of the destination failing. @@ -197,7 +199,7 @@ async Task SerializeToStreamAsync(Stream stream, TransportContext? context, Canc } finally { - registration.Dispose(); + linkedCts?.Dispose(); } } diff --git a/src/ReverseProxy/Management/ProxyConfigManager.cs b/src/ReverseProxy/Management/ProxyConfigManager.cs index f9651fc1a..e87dbd353 100644 --- a/src/ReverseProxy/Management/ProxyConfigManager.cs +++ b/src/ReverseProxy/Management/ProxyConfigManager.cs @@ -25,14 +25,11 @@ namespace Yarp.ReverseProxy.Management { /// - /// Provides a method to apply Proxy configuration changes - /// by leveraging . + /// Provides a method to apply Proxy configuration changes. /// Also an Implementation of that supports being dynamically updated /// in a thread-safe manner while avoiding locks on the hot path. /// - /// - /// This takes inspiration from . - /// + // https://github.com/dotnet/aspnetcore/blob/cbe16474ce9db7ff588aed89596ff4df5c3f62e1/src/Mvc/Mvc.Core/src/Routing/ActionEndpointDataSourceBase.cs internal sealed class ProxyConfigManager : EndpointDataSource, IDisposable { private static readonly IReadOnlyDictionary _emptyClusterDictionary = new ReadOnlyDictionary(new Dictionary()); diff --git a/src/ReverseProxy/Transforms/PathRouteValuesTransform.cs b/src/ReverseProxy/Transforms/PathRouteValuesTransform.cs index 905df6a15..c401c4192 100644 --- a/src/ReverseProxy/Transforms/PathRouteValuesTransform.cs +++ b/src/ReverseProxy/Transforms/PathRouteValuesTransform.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; namespace Yarp.ReverseProxy.Transforms @@ -24,10 +25,10 @@ public PathRouteValuesTransform(string pattern, TemplateBinderFactory binderFact { _ = pattern ?? throw new ArgumentNullException(nameof(pattern)); _binderFactory = binderFactory ?? throw new ArgumentNullException(nameof(binderFactory)); - Template = TemplateParser.Parse(pattern); + Pattern = RoutePatternFactory.Parse(pattern); } - internal RouteTemplate Template { get; } + internal RoutePattern Pattern { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) @@ -39,11 +40,20 @@ public override ValueTask ApplyAsync(RequestTransformContext context) // TemplateBinder.BindValues will modify the RouteValueDictionary // We make a copy so that the original request is not modified by the transform - var routeValues = new RouteValueDictionary(context.HttpContext.Request.RouteValues); + var routeValues = context.HttpContext.Request.RouteValues; + var routeValuesCopy = new RouteValueDictionary(); - // Route values that are not considered defaults will be appended as query parameters. Make them all defaults. - var binder = _binderFactory.Create(Template, defaults: routeValues); - context.Path = binder.BindValues(acceptedValues: routeValues); + // Only copy route values used in the pattern, otherwise they'll be added as query parameters. + foreach (var pattern in Pattern.Parameters) + { + if (routeValues.TryGetValue(pattern.Name, out var value)) + { + routeValuesCopy[pattern.Name] = value; + } + } + + var binder = _binderFactory.Create(Pattern); + context.Path = binder.BindValues(acceptedValues: routeValuesCopy); return default; } diff --git a/src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs b/src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs index 238803cbe..408e24de6 100644 --- a/src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs +++ b/src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms @@ -32,8 +31,7 @@ public override ValueTask ApplyAsync(RequestTransformContext context) throw new ArgumentNullException(nameof(context)); } - var proxyRequestHeaders = context.ProxyRequest.Headers; - proxyRequestHeaders.Remove(HeaderName); + RemoveHeader(context, HeaderName); var clientCert = context.HttpContext.Connection.ClientCertificate; if (clientCert != null) diff --git a/src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs b/src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs index 602b932b9..cbb9ab306 100644 --- a/src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs +++ b/src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs @@ -31,10 +31,7 @@ public override ValueTask ApplyAsync(RequestTransformContext context) throw new ArgumentNullException(nameof(context)); } - if (!context.ProxyRequest.Headers.Remove(HeaderName)) - { - context.ProxyRequest.Content?.Headers.Remove(HeaderName); - } + RemoveHeader(context, HeaderName); return default; } diff --git a/src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs b/src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs index 128d1e3df..1abbcf8ff 100644 --- a/src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs +++ b/src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs @@ -17,7 +17,7 @@ public class RequestHeaderXForwardedForTransform : RequestTransform /// Creates a new transform. /// /// The header name. - /// Action to applied to the header. + /// Action to applied to the header. public RequestHeaderXForwardedForTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) diff --git a/src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs b/src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs index 0d08bd1b0..fa18387a7 100644 --- a/src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs +++ b/src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs @@ -17,7 +17,7 @@ public class RequestHeaderXForwardedHostTransform : RequestTransform /// Creates a new transform. /// /// The header name. - /// Action to applied to the header. + /// Action to applied to the header. public RequestHeaderXForwardedHostTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) diff --git a/src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs b/src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs index 4cd700e3d..16707cf4f 100644 --- a/src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs +++ b/src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs @@ -17,7 +17,7 @@ public class RequestHeaderXForwardedProtoTransform : RequestTransform /// Creates a new transform. /// /// The header name. - /// Action to applied to the header. + /// Action to applied to the header. public RequestHeaderXForwardedProtoTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) diff --git a/src/ReverseProxy/Transforms/RequestTransform.cs b/src/ReverseProxy/Transforms/RequestTransform.cs index cae788f8f..772b39312 100644 --- a/src/ReverseProxy/Transforms/RequestTransform.cs +++ b/src/ReverseProxy/Transforms/RequestTransform.cs @@ -25,6 +25,7 @@ public abstract class RequestTransform /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// + /// The transform context. /// The name of the header to take. /// The requested header value, or StringValues.Empty if none. public static StringValues TakeHeader(RequestTransformContext context, string headerName) diff --git a/src/ReverseProxy/Transforms/ResponseTrailersTransform.cs b/src/ReverseProxy/Transforms/ResponseTrailersTransform.cs index 61a3f5a63..766ce6938 100644 --- a/src/ReverseProxy/Transforms/ResponseTrailersTransform.cs +++ b/src/ReverseProxy/Transforms/ResponseTrailersTransform.cs @@ -26,6 +26,7 @@ public abstract class ResponseTrailersTransform /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// + /// The transform context. /// The name of the header to take. /// The response header value, or StringValues.Empty if none. public static StringValues TakeHeader(ResponseTrailersTransformContext context, string headerName) diff --git a/src/ReverseProxy/Transforms/ResponseTransform.cs b/src/ReverseProxy/Transforms/ResponseTransform.cs index d41d44fb9..c35c6514c 100644 --- a/src/ReverseProxy/Transforms/ResponseTransform.cs +++ b/src/ReverseProxy/Transforms/ResponseTransform.cs @@ -24,6 +24,7 @@ public abstract class ResponseTransform /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// + /// The transform context. /// The name of the header to take. /// The response header value, or StringValues.Empty if none. public static StringValues TakeHeader(ResponseTransformContext context, string headerName) diff --git a/src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs b/src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs index f5f6d48ec..436623d45 100644 --- a/src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs +++ b/src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs @@ -46,7 +46,7 @@ public static ActivityCancellationTokenSource Rent(TimeSpan activityTimeout, Can #endif cts._activityTimeoutMs = (int)activityTimeout.TotalMilliseconds; - cts._linkedRegistration = cts.LinkTo(linkedToken); + cts._linkedRegistration = linkedToken.UnsafeRegister(_linkedTokenCancelDelegate, cts); cts.ResetTimeout(); return cts; @@ -72,10 +72,5 @@ public void Return() Dispose(); } - - public CancellationTokenRegistration LinkTo(CancellationToken linkedToken) - { - return linkedToken.UnsafeRegister(_linkedTokenCancelDelegate, this); - } } } diff --git a/src/ServiceFabric/ServiceDiscovery/Discoverer.cs b/src/ServiceFabric/ServiceDiscovery/Discoverer.cs index b289606ca..90e6d1a05 100644 --- a/src/ServiceFabric/ServiceDiscovery/Discoverer.cs +++ b/src/ServiceFabric/ServiceDiscovery/Discoverer.cs @@ -243,7 +243,7 @@ private bool HttpsSchemeSelector(string urlScheme) /// /// Finds all eligible destinations (replica endpoints) for the specified, - /// and populates the specified 's accordingly. + /// and returns the accordingly. /// /// All non-fatal exceptions are caught and logged. private async Task> DiscoverDestinationsAsync( diff --git a/src/ServiceFabric/ServiceDiscovery/Util/LabelsParser.cs b/src/ServiceFabric/ServiceDiscovery/Util/LabelsParser.cs index 7fa65ccf3..3c43e4eed 100644 --- a/src/ServiceFabric/ServiceDiscovery/Util/LabelsParser.cs +++ b/src/ServiceFabric/ServiceDiscovery/Util/LabelsParser.cs @@ -29,7 +29,7 @@ internal static class LabelsParser /// private static readonly Regex _allowedHeaderNamesRegex = new Regex(@"^\[\d\d*\]$", RegexOptions.Compiled); - + /// /// Requires all transform names to follow the .[0]. pattern to simulate indexing in an array /// private static readonly Regex _allowedTransformNamesRegex = new Regex(@"^\[\d\d*\]$", RegexOptions.Compiled); diff --git a/src/TelemetryConsumption/IMetricsConsumer.cs b/src/TelemetryConsumption/IMetricsConsumer.cs index 363270aa0..5317a35f4 100644 --- a/src/TelemetryConsumption/IMetricsConsumer.cs +++ b/src/TelemetryConsumption/IMetricsConsumer.cs @@ -4,7 +4,7 @@ namespace Yarp.Telemetry.Consumption { /// - /// A consumer of . + /// A consumer of . /// public interface IMetricsConsumer { diff --git a/test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs b/test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs index 7f91064e8..24741dea3 100644 --- a/test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs +++ b/test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs @@ -29,7 +29,7 @@ public async Task CopyAsync_Works(bool isRequest) var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); - await StreamCopier.CopyAsync(isRequest, source, destination, new Clock(), cts); + await StreamCopier.CopyAsync(isRequest, source, destination, new Clock(), cts, cts.Token); Assert.False(cts.Token.IsCancellationRequested); @@ -51,7 +51,7 @@ public async Task SourceThrows_Reported(bool isRequest) var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); - var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, clock, cts); + var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, clock, cts, cts.Token); Assert.Equal(StreamCopyResult.InputError, result); Assert.IsAssignableFrom(error); @@ -80,7 +80,7 @@ public async Task DestinationThrows_Reported(bool isRequest) var destination = new SlowStream(new ThrowStream(), clock, destinationWaitTime); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); - var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, clock, cts); + var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, clock, cts, cts.Token); Assert.Equal(StreamCopyResult.OutputError, result); Assert.IsAssignableFrom(error); @@ -104,7 +104,7 @@ public async Task Cancelled_Reported(bool isRequest) using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts.Cancel(); - var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, new Clock(), cts); + var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, new Clock(), cts, cts.Token); Assert.Equal(StreamCopyResult.Canceled, result); Assert.IsAssignableFrom(error); @@ -138,7 +138,8 @@ await StreamCopier.CopyAsync( new SlowStream(source, clock, sourceWaitTime), new SlowStream(destination, clock, destinationWaitTime), clock, - cts); + cts, + cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); @@ -174,7 +175,8 @@ await StreamCopier.CopyAsync( new SlowStream(source, clock, sourceWaitTime) { MaxBytesPerRead = BytesPerRead }, new SlowStream(destination, clock, destinationWaitTime), clock, - cts); + cts, + cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); diff --git a/test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs b/test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs index b3c2b9308..da4aebdff 100644 --- a/test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs +++ b/test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs @@ -17,7 +17,7 @@ public class PathRouteValuesTransformTests [InlineData("/{a}/{b}/{c}", "/6/7/8")] [InlineData("/{a}/foo/{b}/{c}/{d}", "/6/foo/7/8")] // Unknown value (d) dropped [InlineData("/{a}/foo/{b}", "/6/foo/7")] // Extra values (c) dropped - public async Task Set_PathPattern_ReplacesPathWithRouteValues(string transformValue, string expected) + public async Task ReplacesPatternWithRouteValues(string transformValue, string expected) { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); @@ -45,5 +45,35 @@ public async Task Set_PathPattern_ReplacesPathWithRouteValues(string transformVa // The transform should not modify the original request's route values Assert.Equal(routeValues, httpContext.Request.RouteValues); } + + [Fact] + public async Task RouteValuesWithSlashesNotEncoded() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddOptions(); + serviceCollection.AddRouting(); + using var services = serviceCollection.BuildServiceProvider(); + + var routeValues = new Dictionary + { + { "a", "abc" }, + { "b", "def" }, + { "remainder", "klm/nop/qrs" }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(routeValues); + var context = new RequestTransformContext() + { + Path = "/", + HttpContext = httpContext + }; + var transform = new PathRouteValuesTransform("/{a}/{b}/{**remainder}", services.GetRequiredService()); + await transform.ApplyAsync(context); + Assert.Equal("/abc/def/klm/nop/qrs", context.Path.Value); + + // The transform should not modify the original request's route values + Assert.Equal(routeValues, httpContext.Request.RouteValues); + } } } diff --git a/test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs b/test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs index 928f1f853..1f852a957 100644 --- a/test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs +++ b/test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs @@ -137,7 +137,7 @@ private static void ValidatePathRouteValues(TransformBuilderContext builderConte { var requestTransform = Assert.Single(builderContext.RequestTransforms); var pathRouteValuesTransform = Assert.IsType(requestTransform); - Assert.Equal("/path#", pathRouteValuesTransform.Template.TemplateText); + Assert.Equal("/path#", pathRouteValuesTransform.Pattern.RawText); } } } diff --git a/test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs b/test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs index d2b428502..a240e7c34 100644 --- a/test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs +++ b/test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Linq; using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; +using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms.Tests { @@ -81,5 +83,50 @@ public void TakeHeader_HeadersCopied_ReturnsNothing() }, "name"); Assert.Equal(StringValues.Empty, result); } + + [Theory] + [InlineData("header1", "header1", "")] + [InlineData("header1", "headerX", "header1")] + [InlineData("header1; header2; header3", "header2", "header1; header3")] + [InlineData("header1", "Content-Encoding", "header1")] + [InlineData("header1; Content-Encoding", "Content-Encoding", "header1")] + [InlineData("header1; Content-Encoding", "header1", "Content-Encoding")] + [InlineData("header1; Content-Encoding", "Content-Type", "header1; Content-Encoding")] + [InlineData("header1; Content-Encoding", "headerX", "header1; Content-Encoding")] + [InlineData("header1; Content-Encoding; Accept-Encoding", "header1", "Content-Encoding; Accept-Encoding")] + [InlineData("header1; Content-Encoding; Accept-Encoding", "Content-Encoding", "header1; Accept-Encoding")] + [InlineData("header1; Content-Encoding; Accept-Encoding", "Accept-Encoding", "header1; Content-Encoding")] + [InlineData("header1; Content-Encoding; Accept-Encoding", "headerX", "header1; Content-Encoding; Accept-Encoding")] + public void RemoveHeader_RemovesProxyRequestHeader(string names, string removedHeader, string expected) + { + var httpContext = new DefaultHttpContext(); + var proxyRequest = new HttpRequestMessage() + { + Content = new EmptyHttpContent() + }; + + foreach (var name in names.Split("; ")) + { + httpContext.Request.Headers.Add(name, "value0"); + RequestUtilities.AddHeader(proxyRequest, name, "value1"); + } + + RequestTransform.RemoveHeader(new RequestTransformContext() + { + HttpContext = httpContext, + ProxyRequest = proxyRequest + }, removedHeader); + + foreach (var name in names.Split("; ")) + { + Assert.True(httpContext.Request.Headers.TryGetValue(name, out var value)); + Assert.Equal("value0", value); + } + + var expectedHeaders = expected.Split("; ", System.StringSplitOptions.RemoveEmptyEntries).OrderBy(h => h); + var remainingHeaders = proxyRequest.Headers.Union(proxyRequest.Content.Headers).OrderBy(h => h.Key); + Assert.Equal(expectedHeaders, remainingHeaders.Select(h => h.Key)); + Assert.All(remainingHeaders, h => Assert.Equal("value1", Assert.Single(h.Value))); + } } } diff --git a/testassets/ReverseProxy.Code/Properties/launchSettings.json b/testassets/ReverseProxy.Code/Properties/launchSettings.json index 870f99002..2621747a2 100644 --- a/testassets/ReverseProxy.Code/Properties/launchSettings.json +++ b/testassets/ReverseProxy.Code/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Code": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/testassets/ReverseProxy.Config/Properties/launchSettings.json b/testassets/ReverseProxy.Config/Properties/launchSettings.json index 870f99002..a7bb920ae 100644 --- a/testassets/ReverseProxy.Config/Properties/launchSettings.json +++ b/testassets/ReverseProxy.Config/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Config": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/testassets/ReverseProxy.Config/appsettings.json b/testassets/ReverseProxy.Config/appsettings.json index 0f65f824c..e23f592f9 100644 --- a/testassets/ReverseProxy.Config/appsettings.json +++ b/testassets/ReverseProxy.Config/appsettings.json @@ -78,10 +78,10 @@ "ClusterId": "cluster2", "Match": { "Hosts": [ "localhost" ], - "Path": "/api/{plugin}/stuff/{*remainder}" + "Path": "/api/{plugin}/stuff/{**remainder}" }, "Transforms": [ - { "PathPattern": "/foo/{plugin}/bar/{remainder}" }, + { "PathPattern": "/foo/{plugin}/bar/{**remainder}" }, { "X-Forwarded": "Append", "HeaderPrefix": "X-Forwarded-" @@ -93,10 +93,6 @@ }, { "ClientCert": "X-Client-Cert" }, - { "PathSet": "/apis" }, - { "PathPrefix": "/apis" }, - { "PathRemovePrefix": "/apis" }, - { "RequestHeadersCopy": "true" }, { "RequestHeaderOriginalHost": "true" }, { diff --git a/testassets/ReverseProxy.Direct/Properties/launchSettings.json b/testassets/ReverseProxy.Direct/Properties/launchSettings.json index 870f99002..c6cb84ed3 100644 --- a/testassets/ReverseProxy.Direct/Properties/launchSettings.json +++ b/testassets/ReverseProxy.Direct/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ReverseProxy.Sample": { + "ReverseProxy.Direct": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { diff --git a/testassets/TestClient/Properties/launchSettings.json b/testassets/TestClient/Properties/launchSettings.json index 6736ec71c..58d58722d 100644 --- a/testassets/TestClient/Properties/launchSettings.json +++ b/testassets/TestClient/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "SampleClient": { + "TestClient": { "commandName": "Project", "commandLineArgs": "-t https://localhost:5001" } diff --git a/testassets/TestServer/Properties/launchSettings.json b/testassets/TestServer/Properties/launchSettings.json index 10caee066..2cd4b9fe2 100644 --- a/testassets/TestServer/Properties/launchSettings.json +++ b/testassets/TestServer/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "ReverseProxy.Sample": { + "TestServer": { "commandName": "Project", "launchBrowser": false, "environmentVariables": {