Skip to content

Commit

Permalink
Refactor ConfigValidator (#2293)
Browse files Browse the repository at this point in the history
  • Loading branch information
larsbj1988 authored Nov 22, 2023
1 parent bdab2c7 commit 4e32b6b
Show file tree
Hide file tree
Showing 19 changed files with 828 additions and 584 deletions.
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,101 @@
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,
IEnumerable<IActiveHealthCheckPolicy> activeHealthCheckPolicies,
IEnumerable<IPassiveHealthCheckPolicy> passiveHealthCheckPolicies)
{
_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,20 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Yarp.ReverseProxy.Configuration.ClusterValidators;

/// <summary>
/// Provides method to validate cluster configuration.
/// </summary>
public interface IClusterValidator
{

/// <summary>
/// Perform validation on a cluster configuration by adding exceptions to the provided collection.
/// </summary>
/// <param name="cluster">Cluster configuration to validate</param>
/// <param name="errors">Collection of all validation exceptions</param>
/// <returns>A ValueTask representing the asynchronous validation operation.</returns>
public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors);
}
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,48 @@
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(ILogger<ConfigValidator> logger) : IClusterValidator
{
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

0 comments on commit 4e32b6b

Please sign in to comment.