Setup rudimentary configuration validation

This commit is contained in:
Jordan Dominion
2025-04-16 16:03:37 -04:00
parent 90bee92e25
commit ee685e5ee4
7 changed files with 164 additions and 3 deletions
@@ -0,0 +1,24 @@
using System;
using Microsoft.Extensions.Options;
namespace Tgstation.Server.Host.Configuration.Validators
{
/// <summary>
/// Configuration validator for <see cref="GeneralConfiguration"/>.
/// </summary>
sealed class GeneralConfigValidator : IValidateOptions<GeneralConfiguration>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, GeneralConfiguration options)
{
ArgumentNullException.ThrowIfNull(options);
var metricsValidation = HostingSpecificationConfigValidator.ValidateHostingSpecifications(options.MetricsEndPoints, nameof(options.MetricsEndPoints), false);
if (metricsValidation.Failed)
return metricsValidation;
return HostingSpecificationConfigValidator.ValidateHostingSpecifications(options.ApiEndPoints, nameof(GeneralConfiguration.ApiEndPoints), true);
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Net;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host
{
/// <summary>
/// Configuration validation for <see cref="HostingSpecification"/>s.
/// </summary>
static class HostingSpecificationConfigValidator
{
/// <summary>
/// Validate given a set of <paramref name="endPoints"/>.
/// </summary>
/// <param name="endPoints">The <see cref="HostingSpecification"/>s to validate.</param>
/// <param name="name">The name of the config subsection.</param>
/// <param name="required">If these endpoints are required.</param>
/// <returns>The <see cref="ValidateOptionsResult"/> for the <paramref name="endPoints"/>.</returns>
public static ValidateOptionsResult ValidateHostingSpecifications(IReadOnlyList<HostingSpecification> endPoints, string name, bool required)
{
ArgumentNullException.ThrowIfNull(endPoints);
ArgumentNullException.ThrowIfNull(name);
if (endPoints == null || endPoints.Count == 0)
{
if (!required)
return ValidateOptionsResult.Success;
return ValidateOptionsResult.Fail($"At least one hosting specification for {name} must be specified");
}
foreach (var endPoint in endPoints)
{
if (endPoint == null)
return ValidateOptionsResult.Fail("A hosting specification cannot be null!");
if (endPoint.Port == 0)
return ValidateOptionsResult.Fail($"Hosting specification port in {name} cannot be 0!");
if (endPoint.IPAddress != null && !IPAddress.TryParse(endPoint.IPAddress, out _))
return ValidateOptionsResult.Fail($"Could not part hosting IP address in {name}: {endPoint.IPAddress}");
}
return ValidateOptionsResult.Success;
}
}
}
@@ -0,0 +1,23 @@
using System;
using Microsoft.Extensions.Options;
namespace Tgstation.Server.Host.Configuration.Validators
{
/// <summary>
/// Configuration validator for <see cref="SessionConfiguration"/>.
/// </summary>
sealed class SessionConfigValidator : IValidateOptions<SessionConfiguration>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, SessionConfiguration options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.BridgePort == 0)
return ValidateOptionsResult.Fail($"{nameof(SessionConfiguration.BridgePort)} cannot be zero!");
return ValidateOptionsResult.Success;
}
}
}
@@ -0,0 +1,29 @@
using System;
using Microsoft.Extensions.Options;
namespace Tgstation.Server.Host.Configuration.Validators
{
/// <summary>
/// Configuration validator for <see cref="SwarmConfiguration"/>.
/// </summary>
sealed class SwarmConfigValidator : IValidateOptions<SwarmConfiguration>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, SwarmConfiguration options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.PrivateKey == null)
return ValidateOptionsResult.Success;
if (options.UpdateRequiredNodeCount == 0)
return ValidateOptionsResult.Fail($"{nameof(SwarmConfiguration.UpdateRequiredNodeCount)} must be greater than 0!");
if (options.Address == null)
return ValidateOptionsResult.Fail($"{nameof(SwarmConfiguration.Address)} must be set to an http endpoint of this swarm service accessible from other servers in the swarm!");
return HostingSpecificationConfigValidator.ValidateHostingSpecifications(options.EndPoints, nameof(SwarmConfiguration.EndPoints), true);
}
}
}
@@ -822,6 +822,10 @@ namespace Tgstation.Server.Host.Core
protected override void ConfigureHostedService(IServiceCollection services)
=> services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
/// <inheritdoc />
protected override void UseValidatedConfig<TConfig, TValidator>(IServiceCollection services)
=> services.UseValidatedConfig<TConfig, TValidator>(Configuration);
/// <summary>
/// Configure the <paramref name="services"/> for the authentication pipeline.
/// </summary>
@@ -165,6 +165,22 @@ namespace Tgstation.Server.Host.Extensions
.BindConfiguration(sectionName);
}
/// <summary>
/// Add a standard <typeparamref name="TConfig"/> binding.
/// </summary>
/// <typeparam name="TConfig">The <see langword="class"/> to bind. Must have a <see langword="public"/> const/static <see cref="string"/> field named "Section".</typeparam>
/// <typeparam name="TValidator">The <see cref="IValidateOptions{TOptions}"/> <see cref="Type"/> for <typeparamref name="TConfig"/>s.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to configure.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> containing the <typeparamref name="TConfig"/>.</param>
/// <returns><paramref name="serviceCollection"/>.</returns>
public static OptionsBuilder<TConfig> UseValidatedConfig<TConfig, TValidator>(this IServiceCollection serviceCollection, IConfiguration configuration)
where TConfig : class
where TValidator : class, IValidateOptions<TConfig>
{
serviceCollection.AddSingleton<IValidateOptions<TConfig>, TValidator>();
return UseStandardConfig<TConfig>(serviceCollection, configuration);
}
/// <summary>
/// Clear previous providers and configure logging.
/// </summary>
@@ -3,9 +3,12 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Serilog.Events;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Configuration.Validators;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
@@ -56,14 +59,14 @@ namespace Tgstation.Server.Host.Setup
services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
// these configs are what's injected into PostSetupServices
services.UseStandardConfig<GeneralConfiguration>(Configuration);
UseValidatedConfig<GeneralConfiguration, GeneralConfigValidator>(services);
services.UseStandardConfig<DatabaseConfiguration>(Configuration);
services.UseStandardConfig<SecurityConfiguration>(Configuration);
services.UseStandardConfig<FileLoggingConfiguration>(Configuration);
services.UseStandardConfig<ElasticsearchConfiguration>(Configuration);
services.UseStandardConfig<InternalConfiguration>(Configuration);
services.UseStandardConfig<SwarmConfiguration>(Configuration);
services.UseStandardConfig<SessionConfiguration>(Configuration);
UseValidatedConfig<SwarmConfiguration, SwarmConfigValidator>(services);
UseValidatedConfig<SessionConfiguration, SessionConfigValidator>(services);
ConfigureHostedService(services);
}
@@ -77,5 +80,16 @@ namespace Tgstation.Server.Host.Setup
services.AddSingleton<IPostSetupServices, PostSetupServices>();
services.AddSingleton<IHostedService, SetupWizard>();
}
/// <summary>
/// Configures the <see cref="IHostedService"/>.
/// </summary>
/// <typeparam name="TConfig">The <see langword="class"/> to bind. Must have a <see langword="public"/> const/static <see cref="string"/> field named "Section".</typeparam>
/// <typeparam name="TValidator">The <see cref="IValidateOptions{TOptions}"/> <see cref="Type"/> for <typeparamref name="TConfig"/>s.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
protected virtual void UseValidatedConfig<TConfig, TValidator>(IServiceCollection services)
where TConfig : class
where TValidator : class, IValidateOptions<TConfig>
=> services.UseStandardConfig<TConfig>(Configuration); // don't enable config validation during setup
}
}