using Microsoft.Extensions.Configuration; using ZB.MOM.WW.Configuration; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// Fail-fast startup validator for (archreview 06/S-11), /// built on the shared ZB.MOM.WW.Configuration . /// Wired via AddValidatedOptions so a provably-crashing historian config fails host startup /// with a named, aggregated OptionsValidationException instead of the raw /// the gateway client factory's new Uri(...) would /// otherwise throw at first resolution (a crash-loop under a service/docker restart policy). /// /// /// /// Consumer-gated, not section-gated. The load-bearing question is not "is /// ServerHistorian enabled?" but "is its gateway connection consumed by anything /// enabled?". The alarm-history sink (AlarmHistorian:Enabled=true) sources its /// endpoint/key/TLS from the ServerHistorian section independently of /// , so it must also gate this check — otherwise the /// AlarmHistorian:Enabled=true / ServerHistorian:Enabled=false corner crashes with /// zero warning. Continuous historization is already co-gated on /// in the Host, so Enabled covers it. /// /// /// Fail tier = provably-crashing configs only. Three settings qualify, and they all fail /// the same way — HistorianGatewayClientOptions.Validate() throws inside the client /// factory before any call is attempted, so the host dies at startup: /// an empty / non-absolute / non-http(s) Endpoint; an empty ApiKey ("The gateway /// API key must not be empty"); and a UseTls that disagrees with the endpoint scheme /// ("UseTls requires an https gateway endpoint" / "An https gateway endpoint requires UseTls"). /// A non-positive MaxTieClusterOverfetch genuinely degrades rather than crashes (the node /// manager surfaces a Bad read), so it stays an operator warning in /// . The Endpoint value is not a secret and /// is echoed to make each error actionable; the ApiKey is never surfaced. /// /// /// The ApiKey and UseTls checks were added after two consecutive live-rig /// crash-loops (LocalDb Phase 2 gate). Both had been classified as degrading, on the /// assumption that a bad client configuration would connect and be rejected by the gateway — but /// the client validates its own options at construction, so the process dies during Akka startup /// instead, which is precisely the failure this validator exists to convert into a named, /// aggregated error. The lesson generalises: "degrades" is only true of settings the client /// does not itself validate. /// /// public sealed class ServerHistorianOptionsValidator : OptionsValidatorBase { private readonly IConfiguration _configuration; /// Creates the validator over the app configuration (read to resolve the consumer gate). /// The app configuration; used to read AlarmHistorian:Enabled. public ServerHistorianOptionsValidator(IConfiguration configuration) => _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); /// protected override void Validate(ValidationBuilder builder, ServerHistorianOptions options) { var alarmHistorianEnabled = _configuration.GetValue("AlarmHistorian:Enabled"); var consumed = options.Enabled || alarmHistorianEnabled; // A section nobody consumes may legitimately be empty (the docker-dev default) — no checks. if (!consumed) return; var endpointValid = Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); // Name the DRIVING section(s) so the operator knows why an empty/malformed endpoint fails — // especially in the alarm-only corner where ServerHistorian itself is disabled. var reason = options.Enabled ? "required because ServerHistorian:Enabled=true" : "required because AlarmHistorian:Enabled=true sources its gateway connection (endpoint/key/TLS) from the ServerHistorian section"; builder.RequireThat( endpointValid, $"ServerHistorian:Endpoint is empty or not an absolute http(s) URI ('{options.Endpoint}') — {reason}."); // Deliberately not echoed: unlike the endpoint, the key is a secret. builder.RequireThat( !string.IsNullOrWhiteSpace(options.ApiKey), $"ServerHistorian:ApiKey is empty — {reason}. The gateway client rejects a keyless " + "configuration at construction, so the host would crash on startup rather than degrade. " + "Supply it via the environment variable ServerHistorian__ApiKey."); // The flag and the scheme must agree in BOTH directions — the client throws either way // ("UseTls requires an https gateway endpoint" / "An https gateway endpoint requires UseTls"). // Only meaningful once the endpoint parsed; a malformed one already failed above. if (endpointValid) { var https = uri!.Scheme == Uri.UriSchemeHttps; builder.RequireThat( options.UseTls == https, $"ServerHistorian:UseTls is {options.UseTls.ToString().ToLowerInvariant()} but the " + $"endpoint is '{options.Endpoint}' — {reason}. The flag must match the scheme " + "(https ⇒ UseTls=true, http ⇒ UseTls=false); the gateway client rejects a mismatch " + "at construction, crashing the host on startup."); } } }