c3ab37523a
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.
144 lines
9.4 KiB
Markdown
144 lines
9.4 KiB
Markdown
# 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: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`](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.
|