diff --git a/src/Tgstation.Server.Host/Configuration/Validators/GeneralConfigValidator.cs b/src/Tgstation.Server.Host/Configuration/Validators/GeneralConfigValidator.cs new file mode 100644 index 0000000000..990d7b950f --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/Validators/GeneralConfigValidator.cs @@ -0,0 +1,24 @@ +using System; + +using Microsoft.Extensions.Options; + +namespace Tgstation.Server.Host.Configuration.Validators +{ + /// + /// Configuration validator for . + /// + sealed class GeneralConfigValidator : IValidateOptions + { + /// + 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); + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/Validators/HostingSpecificationConfigValidator.cs b/src/Tgstation.Server.Host/Configuration/Validators/HostingSpecificationConfigValidator.cs new file mode 100644 index 0000000000..2911921269 --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/Validators/HostingSpecificationConfigValidator.cs @@ -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 +{ + /// + /// Configuration validation for s. + /// + static class HostingSpecificationConfigValidator + { + /// + /// Validate given a set of . + /// + /// The s to validate. + /// The name of the config subsection. + /// If these endpoints are required. + /// The for the . + public static ValidateOptionsResult ValidateHostingSpecifications(IReadOnlyList 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; + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/Validators/SessionConfigValidator.cs b/src/Tgstation.Server.Host/Configuration/Validators/SessionConfigValidator.cs new file mode 100644 index 0000000000..0b450e36dc --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/Validators/SessionConfigValidator.cs @@ -0,0 +1,23 @@ +using System; + +using Microsoft.Extensions.Options; + +namespace Tgstation.Server.Host.Configuration.Validators +{ + /// + /// Configuration validator for . + /// + sealed class SessionConfigValidator : IValidateOptions + { + /// + 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; + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/Validators/SwarmConfigValidator.cs b/src/Tgstation.Server.Host/Configuration/Validators/SwarmConfigValidator.cs new file mode 100644 index 0000000000..cb67d69606 --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/Validators/SwarmConfigValidator.cs @@ -0,0 +1,29 @@ +using System; + +using Microsoft.Extensions.Options; + +namespace Tgstation.Server.Host.Configuration.Validators +{ + /// + /// Configuration validator for . + /// + sealed class SwarmConfigValidator : IValidateOptions + { + /// + 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); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index b3c4820d70..74b566458b 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -822,6 +822,10 @@ namespace Tgstation.Server.Host.Core protected override void ConfigureHostedService(IServiceCollection services) => services.AddSingleton(x => x.GetRequiredService()); + /// + protected override void UseValidatedConfig(IServiceCollection services) + => services.UseValidatedConfig(Configuration); + /// /// Configure the for the authentication pipeline. /// diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index ea41b321d4..1a79a32395 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -165,6 +165,22 @@ namespace Tgstation.Server.Host.Extensions .BindConfiguration(sectionName); } + /// + /// Add a standard binding. + /// + /// The to bind. Must have a const/static field named "Section". + /// The for s. + /// The to configure. + /// The containing the . + /// . + public static OptionsBuilder UseValidatedConfig(this IServiceCollection serviceCollection, IConfiguration configuration) + where TConfig : class + where TValidator : class, IValidateOptions + { + serviceCollection.AddSingleton, TValidator>(); + return UseStandardConfig(serviceCollection, configuration); + } + /// /// Clear previous providers and configure logging. /// diff --git a/src/Tgstation.Server.Host/Setup/SetupApplication.cs b/src/Tgstation.Server.Host/Setup/SetupApplication.cs index ef5c999d03..7c31ee843f 100644 --- a/src/Tgstation.Server.Host/Setup/SetupApplication.cs +++ b/src/Tgstation.Server.Host/Setup/SetupApplication.cs @@ -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(); // these configs are what's injected into PostSetupServices - services.UseStandardConfig(Configuration); + UseValidatedConfig(services); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); - services.UseStandardConfig(Configuration); - services.UseStandardConfig(Configuration); + UseValidatedConfig(services); + UseValidatedConfig(services); ConfigureHostedService(services); } @@ -77,5 +80,16 @@ namespace Tgstation.Server.Host.Setup services.AddSingleton(); services.AddSingleton(); } + + /// + /// Configures the . + /// + /// The to bind. Must have a const/static field named "Section". + /// The for s. + /// The to configure. + protected virtual void UseValidatedConfig(IServiceCollection services) + where TConfig : class + where TValidator : class, IValidateOptions + => services.UseStandardConfig(Configuration); // don't enable config validation during setup } }