Files
scadaproj/components/configuration/GAPS.md
T
Joseph Doherty c3ab37523a docs: record ZB.MOM.WW.Configuration fleet-wide adoption + add design/plan
Configuration is now adopted across all three sister apps (local branches),
so flip the status lines in CLAUDE.md, components/configuration/GAPS.md, and the
lib README/CLAUDE.md from 'not adopted' to adopted (also corrects 27->42 tests).
Adds the brainstorm design doc + bite-sized implementation plan (+tasks.json)
under docs/plans/ that drove the adoption.
2026-06-01 23:18:02 -04:00

9.4 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) 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 (merged, not yet pushed to remotes). The adoption items below are now largely closed: MxGateway + ScadaBridge migrated to OptionsValidatorBase/AddValidatedOptions behaviour-preservingly (validator messages byte-identical), ScadaBridge's StartupValidatorConfigPreflight, 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.

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: GatewayOptionsValidatorOptionsValidatorBase<GatewayOptions>. → Gap B2: ScadaBridge: four *OptionsValidatorOptionsValidatorBase<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: AddGatewayConfigurationAddValidatedOptions. → Gap W2: ScadaBridge: four module AddXxxAddValidatedOptions. → 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:6368 and StartupValidator.cs:8183), so the swap is behaviour-preserving.

Gap F1: ScadaBridge: StartupValidatorConfigPreflight (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: GatewayOptionsValidatorOptionsValidatorBase; helpers → primitives (Gaps B1, P1) MxGateway P2 M Low One validator (~360 LOC); messages preserved verbatim
2 MxGateway: AddGatewayConfigurationAddValidatedOptions (Gap W1) MxGateway P2 S Low Bundles with #1; pass GatewayOptions.SectionName as sectionPath
3 ScadaBridge: four *OptionsValidatorOptionsValidatorBase; inline checks → primitives (Gaps B2, P1) ScadaBridge P2 M Low Cluster/Security/HealthMonitoring/AuditLog; messages preserved
4 ScadaBridge: four module AddXxxAddValidatedOptions (Gap W2) ScadaBridge P2 S Low Bundles with #3; keep HealthMonitoring idempotency guard
5 ScadaBridge: StartupValidatorConfigPreflight (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 mss / 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.