Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor Configvalidator #2293

Merged
merged 18 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class DestinationValidator : IClusterValidator
{
public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
if (cluster.Destinations is null)
{
return ValueTask.CompletedTask;
}

foreach (var (name, destination) in cluster.Destinations)
{
if (string.IsNullOrEmpty(destination.Address))
{
errors.Add(new ArgumentException($"No address found for destination '{name}' on cluster '{cluster.ClusterId}'."));
}
}

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Health;
using Yarp.ReverseProxy.Utilities;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class HealthCheckValidator : IClusterValidator
{
private readonly FrozenDictionary<string, IAvailableDestinationsPolicy> _availableDestinationsPolicies;
private readonly FrozenDictionary<string, IActiveHealthCheckPolicy> _activeHealthCheckPolicies;
private readonly FrozenDictionary<string, IPassiveHealthCheckPolicy> _passiveHealthCheckPolicies;
public HealthCheckValidator(IEnumerable<IAvailableDestinationsPolicy> availableDestinationsPolicies,
larsbj1988 marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<IActiveHealthCheckPolicy> activeHealthCheckPolicies,
IEnumerable<IPassiveHealthCheckPolicy> passiveHealthCheckPolicies)
larsbj1988 marked this conversation as resolved.
Show resolved Hide resolved
{
_availableDestinationsPolicies = availableDestinationsPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(availableDestinationsPolicies));
_activeHealthCheckPolicies = activeHealthCheckPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(availableDestinationsPolicies));
_passiveHealthCheckPolicies = passiveHealthCheckPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(availableDestinationsPolicies));
}

public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
var availableDestinationsPolicy = cluster.HealthCheck?.AvailableDestinationsPolicy;
if (string.IsNullOrEmpty(availableDestinationsPolicy))
{
// The default.
availableDestinationsPolicy = HealthCheckConstants.AvailableDestinations.HealthyOrPanic;
}

if (!_availableDestinationsPolicies.ContainsKey(availableDestinationsPolicy))
{
errors.Add(new ArgumentException($"No matching {nameof(IAvailableDestinationsPolicy)} found for the available destinations policy '{availableDestinationsPolicy}' set on the cluster.'{cluster.ClusterId}'."));
}

ValidateActiveHealthCheck(cluster, errors);
ValidatePassiveHealthCheck(cluster, errors);

return ValueTask.CompletedTask;
}

private void ValidateActiveHealthCheck(ClusterConfig cluster, IList<Exception> errors)
{
if (!(cluster.HealthCheck?.Active?.Enabled ?? false))
{
// Active health check is disabled
return;
}

var activeOptions = cluster.HealthCheck.Active;
var policy = activeOptions.Policy;
if (string.IsNullOrEmpty(policy))
{
// default policy
policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures;
}
if (!_activeHealthCheckPolicies.ContainsKey(policy))
{
errors.Add(new ArgumentException($"No matching {nameof(IActiveHealthCheckPolicy)} found for the active health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'."));
}

if (activeOptions.Interval is not null && activeOptions.Interval <= TimeSpan.Zero)
{
errors.Add(new ArgumentException($"Destination probing interval set on the cluster '{cluster.ClusterId}' must be positive."));
}

if (activeOptions.Timeout is not null && activeOptions.Timeout <= TimeSpan.Zero)
{
errors.Add(new ArgumentException($"Destination probing timeout set on the cluster '{cluster.ClusterId}' must be positive."));
}
}

private void ValidatePassiveHealthCheck(ClusterConfig cluster, IList<Exception> errors)
{
if (!(cluster.HealthCheck?.Passive?.Enabled ?? false))
{
// Passive health check is disabled
return;
}

var passiveOptions = cluster.HealthCheck.Passive;
var policy = passiveOptions.Policy;
if (string.IsNullOrEmpty(policy))
{
// default policy
policy = HealthCheckConstants.PassivePolicy.TransportFailureRate;
}
if (!_passiveHealthCheckPolicies.ContainsKey(policy))
{
errors.Add(new ArgumentException($"No matching {nameof(IPassiveHealthCheckPolicy)} found for the passive health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'."));
}

if (passiveOptions.ReactivationPeriod is not null && passiveOptions.ReactivationPeriod <= TimeSpan.Zero)
{
errors.Add(new ArgumentException($"Unhealthy destination reactivation period set on the cluster '{cluster.ClusterId}' must be positive."));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

public interface IClusterValidator
{
public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors);
larsbj1988 marked this conversation as resolved.
Show resolved Hide resolved
larsbj1988 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Tasks;
using Yarp.ReverseProxy.LoadBalancing;
using Yarp.ReverseProxy.Utilities;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class LoadBalancingValidator : IClusterValidator
{
private readonly FrozenDictionary<string, ILoadBalancingPolicy> _loadBalancingPolicies;
public LoadBalancingValidator(IEnumerable<ILoadBalancingPolicy> loadBalancingPolicies)
{
_loadBalancingPolicies = loadBalancingPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(loadBalancingPolicies));
}

public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
var loadBalancingPolicy = cluster.LoadBalancingPolicy;
if (string.IsNullOrEmpty(loadBalancingPolicy))
{
// The default.
loadBalancingPolicy = LoadBalancingPolicies.PowerOfTwoChoices;
}

if (!_loadBalancingPolicies.ContainsKey(loadBalancingPolicy))
{
errors.Add(new ArgumentException($"No matching {nameof(ILoadBalancingPolicy)} found for the load balancing policy '{loadBalancingPolicy}' set on the cluster '{cluster.ClusterId}'."));
}

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class ProxyHttpClientValidator : IClusterValidator
{
public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
if (cluster.HttpClient is null)
{
// Proxy http client options are not set.
return ValueTask.CompletedTask;
}

if (cluster.HttpClient.MaxConnectionsPerServer is not null && cluster.HttpClient.MaxConnectionsPerServer <= 0)
{
errors.Add(new ArgumentException($"Max connections per server limit set on the cluster '{cluster.ClusterId}' must be positive."));
}

var requestHeaderEncoding = cluster.HttpClient.RequestHeaderEncoding;
if (requestHeaderEncoding is not null)
{
try
{
Encoding.GetEncoding(requestHeaderEncoding);
}
catch (ArgumentException aex)
{
errors.Add(new ArgumentException($"Invalid request header encoding '{requestHeaderEncoding}'.", aex));
}
}

var responseHeaderEncoding = cluster.HttpClient.ResponseHeaderEncoding;
if (responseHeaderEncoding is null)
{
return ValueTask.CompletedTask;
}

try
{
Encoding.GetEncoding(responseHeaderEncoding);
}
catch (ArgumentException aex)
{
errors.Add(new ArgumentException($"Invalid response header encoding '{responseHeaderEncoding}'.", aex));
}

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class ProxyHttpRequestValidator : IClusterValidator
{
private readonly ILogger<ProxyHttpRequestValidator> _logger;

public ProxyHttpRequestValidator(ILogger<ProxyHttpRequestValidator> logger)
larsbj1988 marked this conversation as resolved.
Show resolved Hide resolved
{
_logger = logger;
}

public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
if (cluster.HttpRequest is null)
{
// Proxy http request options are not set.
return ValueTask.CompletedTask;
}

if (cluster.HttpRequest.Version is not null &&
cluster.HttpRequest.Version != HttpVersion.Version10 &&
cluster.HttpRequest.Version != HttpVersion.Version11 &&
cluster.HttpRequest.Version != HttpVersion.Version20 &&
cluster.HttpRequest.Version != HttpVersion.Version30)
{
errors.Add(new ArgumentException($"Outgoing request version '{cluster.HttpRequest.Version}' is not any of supported HTTP versions (1.0, 1.1, 2 and 3)."));
}

if (cluster.HttpRequest.Version == HttpVersion.Version10)
{
Log.Http10Version(_logger);
}

return ValueTask.CompletedTask;
}

private static class Log
{
private static readonly Action<ILogger, Exception?> _http10RequestVersionDetected = LoggerMessage.Define(
LogLevel.Warning,
EventIds.Http10RequestVersionDetected,
"The HttpRequest version is set to 1.0 which can result in poor performance and port exhaustion. Use 1.1, 2, or 3 instead.");

public static void Http10Version(ILogger logger)
{
_http10RequestVersionDetected(logger, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Tasks;
using Yarp.ReverseProxy.SessionAffinity;
using Yarp.ReverseProxy.Utilities;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

internal sealed class SessionAffinityValidator : IClusterValidator
{
private readonly FrozenDictionary<string, IAffinityFailurePolicy> _affinityFailurePolicies;

public SessionAffinityValidator(IEnumerable<IAffinityFailurePolicy> affinityFailurePolicies)
{
_affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies));
}

public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)
{
if (!(cluster.SessionAffinity?.Enabled ?? false))
{
// Session affinity is disabled
return ValueTask.CompletedTask;
}

// Note some affinity validation takes place in AffinitizeTransformProvider.ValidateCluster.
var affinityFailurePolicy = cluster.SessionAffinity.FailurePolicy;
if (string.IsNullOrEmpty(affinityFailurePolicy))
{
// The default.
affinityFailurePolicy = SessionAffinityConstants.FailurePolicies.Redistribute;
}

if (!_affinityFailurePolicies.ContainsKey(affinityFailurePolicy))
{
errors.Add(new ArgumentException($"No matching {nameof(IAffinityFailurePolicy)} found for the affinity failure policy name '{affinityFailurePolicy}' set on the cluster '{cluster.ClusterId}'."));
}

if (string.IsNullOrEmpty(cluster.SessionAffinity.AffinityKeyName))
{
errors.Add(new ArgumentException($"Affinity key name set on the cluster '{cluster.ClusterId}' must not be null."));
}

var cookieConfig = cluster.SessionAffinity.Cookie;

if (cookieConfig is null)
{
return ValueTask.CompletedTask;
}

if (cookieConfig.Expiration is not null && cookieConfig.Expiration <= TimeSpan.Zero)
{
errors.Add(new ArgumentException($"Session affinity cookie expiration must be positive or null."));
}

if (cookieConfig.MaxAge is not null && cookieConfig.MaxAge <= TimeSpan.Zero)
{
errors.Add(new ArgumentException($"Session affinity cookie max-age must be positive or null."));
}

return ValueTask.CompletedTask;
}
}
Loading