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

144 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Configuration validation — gaps & adoption backlog
Divergence of each project from [`spec/SPEC.md`](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/`](../../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 `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.
## 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:6368` and `StartupValidator.cs:8183`), 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`](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`](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.