From 3c6ff2ca1bb7b880e4ddc3c61744004ae66004cb Mon Sep 17 00:00:00 2001 From: robbieknuth Date: Tue, 12 Mar 2024 03:50:29 -0700 Subject: [PATCH] Allow for using query strings in DefaultProbingRequestFactory. (#2421) * Introduce a Query field on ActiveHealthCheckConfig. If provided, Query is appended to the probing request. * <3 unit tests. Fix assignment in configurationconfigprovider. * Add a test for a path with an embedded query string * Mention the Query property in docs --------- Co-authored-by: Robbie Knuth Co-authored-by: Miha Zupan --- docs/docfx/articles/config-files.md | 3 ++- docs/docfx/articles/dests-health-checks.md | 7 ++++-- .../Configuration/ActiveHealthCheckConfig.cs | 11 ++++++++-- .../ConfigurationConfigProvider.cs | 3 ++- .../Health/DefaultProbingRequestFactory.cs | 4 +++- .../ActiveHealthCheckConfigTests.cs | 18 +++++++++++++++ .../ConfigurationConfigProviderTests.cs | 7 ++++-- .../DefaultProbingRequestFactoryTests.cs | 22 +++++++++++++------ 8 files changed, 59 insertions(+), 16 deletions(-) diff --git a/docs/docfx/articles/config-files.md b/docs/docfx/articles/config-files.md index a871a5022..d9c91aee5 100644 --- a/docs/docfx/articles/config-files.md +++ b/docs/docfx/articles/config-files.md @@ -189,7 +189,8 @@ For additional fields see [ClusterConfig](xref:Yarp.ReverseProxy.Configuration.C "Interval": "00:00:10", "Timeout": "00:00:10", "Policy": "ConsecutiveFailures", - "Path": "/api/health" // API endpoint to query for health state + "Path": "/api/health", // API endpoint to query for health state + "Query": "?foo=bar" }, "Passive": { // Disables destinations based on HTTP response codes "Enabled": true, // Defaults to false diff --git a/docs/docfx/articles/dests-health-checks.md b/docs/docfx/articles/dests-health-checks.md index 00bbb3321..0e7591122 100644 --- a/docs/docfx/articles/dests-health-checks.md +++ b/docs/docfx/articles/dests-health-checks.md @@ -16,7 +16,8 @@ There are several cluster-wide configuration settings controlling active health "Interval": "00:00:10", "Timeout": "00:00:10", "Policy": "ConsecutiveFailures", - "Path": "/api/health" + "Path": "/api/health", + "Query": "?foo=bar", } }, "Metadata": { @@ -49,7 +50,8 @@ var clusters = new[] Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(10), Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, - Path = "/api/health" + Path = "/api/health", + Query = "?foo=bar", } }, Metadata = new Dictionary { { ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName, "5" } }, @@ -74,6 +76,7 @@ Active health check settings can also be defined in code via the corresponding t - `Timeout` - probing request timeout. Default `00:00:10` - `Policy` - name of a policy evaluating destinations' active health states. Mandatory parameter - `Path` - health check path on all cluster's destinations. Default `null`. +- `Query` - health check query on all cluster's destinations. Default `null`. `Destination` section and [DestinationConfig](xref:Yarp.ReverseProxy.Configuration.DestinationConfig). diff --git a/src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs b/src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs index 3300888b2..b2fde55c4 100644 --- a/src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs +++ b/src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs @@ -35,6 +35,11 @@ public sealed record ActiveHealthCheckConfig /// public string? Path { get; init; } + /// + /// Query string to append to the probe, including the leading '?'. + /// + public string? Query { get; init; } + public bool Equals(ActiveHealthCheckConfig? other) { if (other is null) @@ -46,7 +51,8 @@ public bool Equals(ActiveHealthCheckConfig? other) && Interval == other.Interval && Timeout == other.Timeout && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase) - && string.Equals(Path, other.Path, StringComparison.Ordinal); + && string.Equals(Path, other.Path, StringComparison.Ordinal) + && string.Equals(Query, other.Query, StringComparison.Ordinal); } public override int GetHashCode() @@ -55,6 +61,7 @@ public override int GetHashCode() Interval, Timeout, Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase), - Path?.GetHashCode(StringComparison.Ordinal)); + Path?.GetHashCode(StringComparison.Ordinal), + Query?.GetHashCode(StringComparison.Ordinal)); } } diff --git a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs index bd5b66ffe..a370a4304 100644 --- a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs +++ b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs @@ -312,7 +312,8 @@ private static RouteQueryParameter CreateRouteQueryParameter(IConfigurationSecti Interval = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Interval)), Timeout = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Timeout)), Policy = section[nameof(ActiveHealthCheckConfig.Policy)], - Path = section[nameof(ActiveHealthCheckConfig.Path)] + Path = section[nameof(ActiveHealthCheckConfig.Path)], + Query = section[nameof(ActiveHealthCheckConfig.Query)] }; } diff --git a/src/ReverseProxy/Health/DefaultProbingRequestFactory.cs b/src/ReverseProxy/Health/DefaultProbingRequestFactory.cs index 52b0fbf9e..f0269f11d 100644 --- a/src/ReverseProxy/Health/DefaultProbingRequestFactory.cs +++ b/src/ReverseProxy/Health/DefaultProbingRequestFactory.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Reflection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Model; @@ -20,7 +21,8 @@ public HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel d var probeAddress = !string.IsNullOrEmpty(destination.Config.Health) ? destination.Config.Health : destination.Config.Address; var probePath = cluster.Config.HealthCheck?.Active?.Path; UriHelper.FromAbsolute(probeAddress, out var destinationScheme, out var destinationHost, out var destinationPathBase, out _, out _); - var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, default); + var query = QueryString.FromUriComponent(cluster.Config.HealthCheck?.Active?.Query ?? ""); + var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, query); var request = new HttpRequestMessage(HttpMethod.Get, probeUri) { diff --git a/test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs b/test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs index 1b7216747..c4b925ef3 100644 --- a/test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs +++ b/test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs @@ -61,6 +61,24 @@ public void Equals_Different_Value_Returns_False() Assert.False(equals); } + [Fact] + public void Equals_DifferingQueries_Returns_False() + { + var options1 = new ActiveHealthCheckConfig + { + Query = "?key=value1" + }; + + var options2 = new ActiveHealthCheckConfig + { + Query = "?key=value2" + }; + + var equals = options1.Equals(options2); + + Assert.False(equals); + } + [Fact] public void Equals_Second_Null_Returns_False() { diff --git a/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs b/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs index a73f47d28..4c00d7e5e 100644 --- a/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs +++ b/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs @@ -69,7 +69,8 @@ public class ConfigurationConfigProviderTests Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", - Path = "healthCheckPath" + Path = "healthCheckPath", + Query = "?key=value" }, AvailableDestinationsPolicy = "HealthyOrPanic" }, @@ -239,7 +240,8 @@ public class ConfigurationConfigProviderTests ""Interval"": ""00:00:04"", ""Timeout"": ""00:00:06"", ""Policy"": ""Any5xxResponse"", - ""Path"": ""healthCheckPath"" + ""Path"": ""healthCheckPath"", + ""Query"": ""?key=value"" }, ""AvailableDestinationsPolicy"": ""HealthyOrPanic"" }, @@ -540,6 +542,7 @@ private void VerifyValidAbstractConfig(IProxyConfig validConfig, IProxyConfig ab Assert.Equal(cluster1.HealthCheck.Active.Timeout, abstractCluster1.HealthCheck.Active.Timeout); Assert.Equal(cluster1.HealthCheck.Active.Policy, abstractCluster1.HealthCheck.Active.Policy); Assert.Equal(cluster1.HealthCheck.Active.Path, abstractCluster1.HealthCheck.Active.Path); + Assert.Equal(cluster1.HealthCheck.Active.Query, abstractCluster1.HealthCheck.Active.Query); Assert.Equal(LoadBalancingPolicies.Random, abstractCluster1.LoadBalancingPolicy); Assert.Equal(cluster1.SessionAffinity.Enabled, abstractCluster1.SessionAffinity.Enabled); Assert.Equal(cluster1.SessionAffinity.FailurePolicy, abstractCluster1.SessionAffinity.FailurePolicy); diff --git a/test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs b/test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs index 6f1b48674..448d1b268 100644 --- a/test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs +++ b/test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs @@ -14,13 +14,20 @@ namespace Yarp.ReverseProxy.Health.Tests; public class DefaultProbingRequestFactoryTests { [Theory] - [InlineData("https://localhost:10000/", null, null, "https://localhost:10000/")] - [InlineData("https://localhost:10000/", "https://localhost:20000/", null, "https://localhost:20000/")] - [InlineData("https://localhost:10000/", null, "/api/health/", "https://localhost:10000/api/health/")] - [InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", "https://localhost:20000/api/health/")] - [InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", "https://localhost:20000/health/")] - [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", "https://localhost:20000/api/health/")] - public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string expectedRequestUri) + [InlineData("https://localhost:10000/", null, null, null, "https://localhost:10000/")] + [InlineData("https://localhost:10000/", "https://localhost:20000/", null, null, "https://localhost:20000/")] + [InlineData("https://localhost:10000/", null, "/api/health/", null, "https://localhost:10000/api/health/")] + [InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", null, "https://localhost:20000/api/health/")] + [InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", null, "https://localhost:20000/health/")] + [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", null, "https://localhost:20000/api/health/")] + [InlineData("https://localhost:10000/", null, null, "?key=value", "https://localhost:10000/?key=value")] + [InlineData("https://localhost:10000/", "https://localhost:20000/", null, "?key=value", "https://localhost:20000/?key=value")] + [InlineData("https://localhost:10000/", null, "/api/health/", "?key=value", "https://localhost:10000/api/health/?key=value")] + [InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", "?key=value", "https://localhost:20000/api/health/?key=value")] + [InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", "?key=value", "https://localhost:20000/health/?key=value")] + [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", "?key=value", "https://localhost:20000/api/health/?key=value")] + [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health?foo=bar", "?key=value", "https://localhost:20000/api/health%3Ffoo=bar?key=value")] + public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string query, string expectedRequestUri) { var clusterModel = GetClusterConfig("cluster0", new ActiveHealthCheckConfig() @@ -28,6 +35,7 @@ public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(strin Enabled = true, Policy = "policy", Path = healthPath, + Query = query, }, HttpVersion.Version20); var destinationModel = new DestinationModel(new DestinationConfig { Address = address, Health = health }); var factory = new DefaultProbingRequestFactory();