fix(r2-06): GREEN — ServerHistorianOptionsValidator (consumer-gated fail-fast on bad Endpoint) (06/S-11)
This commit is contained in:
@@ -27,7 +27,7 @@
|
|||||||
{
|
{
|
||||||
"id": "T4",
|
"id": "T4",
|
||||||
"subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)",
|
"subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"T3"
|
"T3"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using ZB.MOM.WW.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fail-fast startup validator for <see cref="ServerHistorianOptions"/> (archreview 06/S-11),
|
||||||
|
/// built on the shared <c>ZB.MOM.WW.Configuration</c> <see cref="OptionsValidatorBase{TOptions}"/>.
|
||||||
|
/// Wired via <c>AddValidatedOptions</c> so a provably-crashing historian config fails host startup
|
||||||
|
/// with a named, aggregated <c>OptionsValidationException</c> instead of the raw
|
||||||
|
/// <see cref="System.UriFormatException"/> the gateway client factory's <c>new Uri(...)</c> would
|
||||||
|
/// otherwise throw at first resolution (a crash-loop under a service/docker restart policy).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Consumer-gated, not section-gated.</b> The load-bearing question is not "is
|
||||||
|
/// <c>ServerHistorian</c> enabled?" but "is its gateway connection <em>consumed</em> by anything
|
||||||
|
/// enabled?". The alarm-history sink (<c>AlarmHistorian:Enabled=true</c>) sources its
|
||||||
|
/// endpoint/key/TLS from the <c>ServerHistorian</c> section independently of
|
||||||
|
/// <see cref="ServerHistorianOptions.Enabled"/>, so it must also gate this check — otherwise the
|
||||||
|
/// <c>AlarmHistorian:Enabled=true</c> / <c>ServerHistorian:Enabled=false</c> corner crashes with
|
||||||
|
/// zero warning. Continuous historization is already co-gated on
|
||||||
|
/// <see cref="ServerHistorianOptions.Enabled"/> in the Host, so <c>Enabled</c> covers it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Fail tier = provably-crashing configs only.</b> Only an empty / non-absolute / non-http(s)
|
||||||
|
/// <c>Endpoint</c> fails here (it throws in the factory). Empty <c>ApiKey</c> and non-positive
|
||||||
|
/// <c>MaxTieClusterOverfetch</c> degrade rather than crash (the gateway rejects calls / the node
|
||||||
|
/// manager surfaces a Bad read), so they stay operator warnings in
|
||||||
|
/// <see cref="ServerHistorianOptions.Validate"/>. The <c>Endpoint</c> value is not a secret and
|
||||||
|
/// is echoed to make the error actionable; the <c>ApiKey</c> is never surfaced.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ServerHistorianOptionsValidator : OptionsValidatorBase<ServerHistorianOptions>
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>Creates the validator over the app configuration (read to resolve the consumer gate).</summary>
|
||||||
|
/// <param name="configuration">The app configuration; used to read <c>AlarmHistorian:Enabled</c>.</param>
|
||||||
|
public ServerHistorianOptionsValidator(IConfiguration configuration) =>
|
||||||
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Validate(ValidationBuilder builder, ServerHistorianOptions options)
|
||||||
|
{
|
||||||
|
var alarmHistorianEnabled = _configuration.GetValue<bool>("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}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user