Files
scadaproj/components/configuration/GAPS.md
T

10 KiB
Raw Blame History

Configuration validation — gaps & adoption backlog

Divergence of each project from spec/SPEC.md, and the ordered backlog to adopt the shared ZB.MOM.WW.Configuration library. The library is BUILT @ 0.1.0 (42 tests, re-run green 2026-07-18) at ../../ZB.MOM.WW.Configuration/ and was ADOPTED across all three apps on 2026-06-01 — published to the Gitea feed, then consumed on each repo's local default branch. Re-verified 2026-07-18: 0.1.0 is on the feed and all four apps reference it (HistorianGateway too, which this file predates) on their pushed default branches — the earlier "merged, not yet pushed to remotes" caveat is stale and has been removed. The adoption items below are now largely closed: MxGateway + ScadaBridge migrated to OptionsValidatorBase/AddValidatedOptions behaviour-preservingly (validator messages byte-identical), ScadaBridge's StartupValidator → ConfigPreflight, and OtOpcUa gained net-new Ldap/OpcUa validators (plus a follow-on pass: real Security:Ldap binding, ValidateOnStart wired for ScadaBridge Cluster/HealthMonitoring, and assorted hardening).

Status legend: ⛔ gap · 🟡 partial · ✅ matches.

Note (reconciled 2026-08-07): the divergence tables below are the pre-adoption 2026-06-01 snapshot. In particular, cells reading OtOpcUa "⛔ n/a (no validators)" describe the state before adoption — as the header above records, OtOpcUa gained net-new Ldap/OpcUa validators (via OptionsValidatorBase + AddValidatedOptions) in the adoption pass, so those cells and the "OtOpcUa has no validators at all" prose are historical, not current. Gaps B3/A1/W3 are closed.

Divergence vs spec

§1 IValidateOptions base — failure accumulation (everyone hand-rolls it)

OtOpcUa MxAccessGateway ScadaBridge
Uses OptionsValidatorBase ⛔ n/a (no validators) ⛔ hand-rolled ⛔ hand-rolled ×4
Private List<string> accumulation n/a 🟡 GatewayOptionsValidator 🟡 four validators
Aggregates ALL failures n/a ✅ (yes, manually) ✅ (yes, manually)

MxGateway and ScadaBridge already accumulate all failures correctly — they just open-code the plumbing (the List<string>, the Count == 0 ? Success : Fail tail, and in MxGateway the AddIfBlank/AddIfNotPositive helpers). OtOpcUa has no validators at all.

→ Gap B1: MxGateway: GatewayOptionsValidator → OptionsValidatorBase<GatewayOptions>. → Gap B2: ScadaBridge: four *OptionsValidator → OptionsValidatorBase<T>. → Gap B3: OtOpcUa: optionally add OptionsValidatorBase subclasses for Ldap/OpcUa (no existing validators to migrate — additive only).

§2 Rule primitives (re-implemented as private helpers)

Primitive OtOpcUa MxAccessGateway ScadaBridge
required-string ⛔ none 🟡 AddIfBlank 🟡 inline IsNullOrWhiteSpace
port range ⛔ none 🟡 inline Port check 🟡 inline (in StartupValidator)
positive TimeSpan ⛔ none n/a 🟡 inline <= TimeSpan.Zero ×7
one-of-set ⛔ none 🟡 inline enum/string checks 🟡 inline HashSet.Contains
min-count ⛔ none n/a 🟡 inline Count < n

The same five rules recur as private helpers / inline checks across both heavy consumers. The shared ValidationBuilder primitives (Required, Port, HostPort, PositiveTimeSpan, OneOf, MinCount) plus RequireThat/Add replace them with identical wording (the internal Checks seam).

→ Gap P1: MxGateway/ScadaBridge: re-express inline checks/helpers as ValidationBuilder primitives; keep app-specific rules (.exe path, heartbeat ordering, seed-node topology) as RequireThat/Add.

§3 DI wiring — AddValidatedOptions (everyone open-codes the triple)

OtOpcUa MxAccessGateway ScadaBridge
bind + register-validator + ValidateOnStart in one call ⛔ bare .Bind(), no validate ⛔ AddGatewayConfiguration open-codes it ⛔ per-module AddXxx open-codes it ×4

MxGateway's AddGatewayConfiguration (AddOptions().BindConfiguration().ValidateOnStart() + AddSingleton<IValidateOptions...>) and ScadaBridge's four module extensions all spell out exactly what AddValidatedOptions<TOptions, TValidator>(config, sectionPath) collapses into one line. OtOpcUa binds with bare .Bind() and never validates.

→ Gap W1: MxGateway: AddGatewayConfiguration → AddValidatedOptions. → Gap W2: ScadaBridge: four module AddXxx → AddValidatedOptions. → Gap W3: OtOpcUa: replace bare .Bind() (Program.cs:99, OtOpcUaServerHostedService.cs:63) with AddValidatedOptions if validators are added (B3).

§4 Pre-host preflight — ConfigPreflight (only ScadaBridge has the concern)

OtOpcUa MxAccessGateway ScadaBridge
Pre-host raw-config validation ⛔ none ⛔ none (single host, no pre-Akka stage) ✅ StartupValidator (open-coded)
Message byte-compatible with ConfigPreflight n/a n/a ✅ confirmed

ScadaBridge's StartupValidator is the reason ConfigPreflight exists. Its thrown InvalidOperationException message is byte-identical to ConfigPreflight.ThrowIfInvalid() ("Configuration validation failed:\n - <field> <reason>", verified against ConfigPreflight.cs:63–68 and StartupValidator.cs:81–83), so the swap is behaviour-preserving.

→ Gap F1: ScadaBridge: StartupValidator → ConfigPreflight (gated on the byte-compatibility test). OtOpcUa/MxGateway have no pre-host stage — not applicable.

§5 OtOpcUa has no startup options validation at all (surprise)

OtOpcUa has zero IValidateOptions / ValidateOnStart usages in src/. Its Ldap and OpcUa sections are bound and trusted; a bad value fails opaquely on first use (the exact failure mode ScadaBridge's SecurityOptionsValidator was written to prevent). This is an absence, not a drift — adoption here is additive, optional, and the lowest-stakes of the three.

→ Gap A1: OtOpcUa: add fail-fast validation for Ldap (required server/search-base) and any OpcUa invariants via OptionsValidatorBase + AddValidatedOptions. Optional; low priority.

§6 Draft validation is out of scope (no gap)

OtOpcUa's draft/generation-content validation (the dormant C# DraftValidator / DraftSnapshot, plus the live DB sp_ValidateDraft it complements — validating operator config content) is not options/config validation and is explicitly out of the shared library's scope (SPEC §0). It is not a gap and requires no change on adoption — listed here only to record that it was considered and deliberately excluded.

Adoption backlog (ordered)

# Item Projects Priority Effort Risk Notes
1 MxGateway: GatewayOptionsValidator → OptionsValidatorBase; helpers → primitives (Gaps B1, P1) MxGateway P2 M Low One validator (~360 LOC); messages preserved verbatim
2 MxGateway: AddGatewayConfiguration → AddValidatedOptions (Gap W1) MxGateway P2 S Low Bundles with #1; pass GatewayOptions.SectionName as sectionPath
3 ScadaBridge: four *OptionsValidator → OptionsValidatorBase; inline checks → primitives (Gaps B2, P1) ScadaBridge P2 M Low Cluster/Security/HealthMonitoring/AuditLog; messages preserved
4 ScadaBridge: four module AddXxx → AddValidatedOptions (Gap W2) ScadaBridge P2 S Low Bundles with #3; keep HealthMonitoring idempotency guard
5 ScadaBridge: StartupValidator → ConfigPreflight (Gap F1) ScadaBridge P2 S Low Gated on byte-compatibility test; Program.cs:39 call site unchanged
6 OtOpcUa: add Ldap/OpcUa validators via OptionsValidatorBase + AddValidatedOptions (Gaps A1, B3, W3) OtOpcUa P3 S Low Additive (no validators today); lowest stakes

Sequencing: items #1–#2 (MxGateway) and #3–#5 (ScadaBridge) are independent and can land in either order; each pair lands in its own sister repo once the 0.1.0 nupkg is referenced. Item #6 (OtOpcUa) is optional new work, deferrable indefinitely. No item is a breaking change — every migration is a behaviour-preserving plumbing swap (the ConfigPreflight swap is the only one that changes a thrown-message implementation, and it is byte-compatible by construction). There is no ops-coordination risk (unlike the observability ms→s / Meter-rename items) because no externally-observed contract (metric label, dashboard, wire format) changes.

Decisions settled (no longer open)

  • Draft validation excluded (SETTLED): OtOpcUa's draft/generation-content validation (DB-side sp_ValidateDraft; the C# DraftValidator/DraftSnapshot is dormant, no src/ caller) is config-content validation, not startup options validation, and stays per-project. See SPEC §0 and current-state/otopcua/CURRENT-STATE.md.
  • ConfigPreflight message envelope pinned (SETTLED): the library reproduces ScadaBridge's StartupValidator envelope byte-for-byte (InvalidOperationException, "Configuration validation failed:\n - <field> <reason>"), so the migration is behaviour-preserving. Verified in ConfigPreflightTests.
  • Single package, no ASP.NET Core dependency (SETTLED): the library closes over only Microsoft.Extensions.* abstractions — validators run in plain DI, no framework reference. See shared-contract/ZB.MOM.WW.Configuration.md.

Decisions still open

  • Filesystem-path validity primitive: MxGateway's AddIfInvalidPath (valid-path + .exe extension) is currently mapped to a custom RequireThat/Add rule. If a second app grows the same need, consider promoting a Path/FilePath primitive to ValidationBuilder — for now it stays app-specific.
  • No-validator ScadaBridge modules: Communication, DataConnectionLayer, Transport, Notification*, etc. bind options without validation today. Whether to add validators (and thus AddValidatedOptions) for them is a per-module call, out of scope for the initial adoption.