diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesExtensions.cs index 816e18d4580..63ee142ac63 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesExtensions.cs @@ -47,6 +47,10 @@ public static IServiceCollection AddKubernetesProbes(this IServiceCollection ser _ = Throw.IfNull(services); _ = Throw.IfNull(configure); + _ = services + .AddOptionsWithValidateOnStart() + .Configure(configure); + var wrapperOptions = new KubernetesProbesOptions(); return services diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesOptionsValidator.cs new file mode 100644 index 00000000000..51b81cc1398 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Probes/KubernetesProbesOptionsValidator.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.Probes; + +internal sealed class KubernetesProbesOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, KubernetesProbesOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (options.LivenessProbe.TcpPort == options.StartupProbe.TcpPort + || options.LivenessProbe.TcpPort == options.ReadinessProbe.TcpPort + || options.StartupProbe.TcpPort == options.ReadinessProbe.TcpPort) + { + builder.AddError("Liveness, startup and readiness probes must use different ports."); + } + + return builder.Build(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/KubernetesProbesOptionsValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/KubernetesProbesOptionsValidatorTests.cs new file mode 100644 index 00000000000..f0c31b22319 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.Probes.Tests/KubernetesProbesOptionsValidatorTests.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Probes.Test; + +public class KubernetesProbesOptionsValidatorTests +{ + [Fact] + public void Validator_DefaultValues_Succeeds() + { + var options = new KubernetesProbesOptions(); + ValidateOptionsResult result = new KubernetesProbesOptionsValidator().Validate(nameof(options), options); + + Assert.True(result.Succeeded); + } + + [Fact] + public void Validator_GivenValidOptions_Succeeds() + { + var options = new KubernetesProbesOptions(); + options.LivenessProbe.TcpPort = 2305; + options.StartupProbe.TcpPort = 2306; + options.ReadinessProbe.TcpPort = 2307; + + ValidateOptionsResult result = new KubernetesProbesOptionsValidator().Validate(nameof(options), options); + + Assert.True(result.Succeeded); + } + + [Fact] + public void Validator_GivenInvalidOptions_Fails() + { + var options = new KubernetesProbesOptions(); + options.LivenessProbe.TcpPort = 2305; + options.StartupProbe.TcpPort = 2305; + options.ReadinessProbe.TcpPort = 2307; + + var validator = new KubernetesProbesOptionsValidator(); + ValidateOptionsResult result = validator.Validate(nameof(options), options); + Assert.True(result.Failed); + + options.LivenessProbe.TcpPort = 2305; + options.StartupProbe.TcpPort = 2306; + options.ReadinessProbe.TcpPort = 2305; + result = validator.Validate(nameof(options), options); + Assert.True(result.Failed); + + options.LivenessProbe.TcpPort = 2305; + options.StartupProbe.TcpPort = 2306; + options.ReadinessProbe.TcpPort = 2306; + result = validator.Validate(nameof(options), options); + Assert.True(result.Failed); + + options.LivenessProbe.TcpPort = 2305; + options.StartupProbe.TcpPort = 2305; + options.ReadinessProbe.TcpPort = 2305; + result = validator.Validate(nameof(options), options); + Assert.True(result.Failed); + } + + [Fact] + public async Task Validator_WhenHostStarts_Succeeds() + { + using IHost host = CreateHost(services => + { + services.AddKubernetesProbes(options => + { + options.LivenessProbe.TcpPort = 22305; + options.StartupProbe.TcpPort = 22306; + options.ReadinessProbe.TcpPort = 22307; + }).AddHealthChecks(); + }); + + try + { + host.Start(); + await host.StopAsync(); + } + catch (OptionsValidationException ex) + { + Assert.Fail("Unexpected OptionsValidationException: " + ex.Message); + } + catch (Exception ex) + { + Assert.Fail("Unexpected exception: " + ex.Message); + throw; + } + } + + [Fact] + public void Validator_WhenHostStarts_Fails() + { + Action action = () => + { + using IHost host = CreateHost(services => + { + services.AddKubernetesProbes(options => + { + options.LivenessProbe.TcpPort = 22305; + options.StartupProbe.TcpPort = 22305; + options.ReadinessProbe.TcpPort = 22307; + }).AddHealthChecks(); + }); + + host.Start(); + }; + + Assert.Throws(action); + } + + private static IHost CreateHost(Action configureServices) + { + return Host.CreateDefaultBuilder() + .ConfigureServices(configureServices) + .Build(); + } +}