using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.OtOpcUa.Cluster;
///
/// Fails the host at startup on a shape that would leave a
/// driver node unable to fetch its configuration.
///
///
/// Like , every fault caught here otherwise surfaces as
/// an absence — a deployment that never applies because the node has no endpoint to fetch
/// from or no key to authenticate with. An absence has no stack trace and no failing node to point
/// at; refusing to start is cheaper to diagnose.
///
public sealed class ConfigSourceOptionsValidator : IValidateOptions
{
///
public ValidateOptionsResult Validate(string? name, ConfigSourceOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var errors = new List();
var isDirect = string.Equals(options.Mode, ConfigSourceOptions.ModeDirect, StringComparison.OrdinalIgnoreCase);
var isFetch = string.Equals(
options.Mode, ConfigSourceOptions.ModeFetchAndCache, StringComparison.OrdinalIgnoreCase);
if (!isDirect && !isFetch)
{
errors.Add(
$"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)} is "
+ $"'{options.Mode}'. Expected '{ConfigSourceOptions.ModeDirect}' or "
+ $"'{ConfigSourceOptions.ModeFetchAndCache}'.");
}
// The fetch surface is only load-bearing under FetchAndCache. Requiring it under the default
// would make the section mandatory everywhere for a feature that is switched off — including on
// admin-only nodes, which never fetch.
if (isFetch)
{
if (options.CentralFetchEndpoints.Length == 0)
{
errors.Add(
$"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.CentralFetchEndpoints)} "
+ "is empty. Under FetchAndCache a driver node cannot reach central to fetch its "
+ "configuration, and every deployment would time out naming a node that is running "
+ "perfectly.");
}
foreach (var endpoint in options.CentralFetchEndpoints)
{
if (!endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
errors.Add(
$"Central fetch endpoint '{endpoint}' must start with 'http://' (h2c) or "
+ "'https://' — it is a gRPC base address (scheme://host:port), not a bare "
+ "host:port.");
}
}
if (string.IsNullOrEmpty(options.ApiKey))
{
errors.Add(
$"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.ApiKey)} is empty. "
+ "Under FetchAndCache the shared bearer key is the whole authentication boundary to "
+ "central's artifact service; without it every fetch is rejected. Supply it via the "
+ "environment (ConfigSource__ApiKey).");
}
if (options.FetchTimeoutSeconds <= 0)
{
errors.Add(
$"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.FetchTimeoutSeconds)} "
+ $"must be greater than 0 (was {options.FetchTimeoutSeconds}).");
}
}
return errors.Count == 0
? ValidateOptionsResult.Success
: ValidateOptionsResult.Fail(string.Join(" ", errors));
}
}