Files
scadaproj/components/configuration/current-state/otopcua/CURRENT-STATE.md
T

99 lines
6.0 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 — current state: OtOpcUa
Repo: `~/Desktop/OtOpcUa`. Stack: .NET 10, OPC UA, gRPC; solution `ZB.MOM.WW.OtOpcUa.slnx`.
All paths relative to repo root. Verified 2026-06-01.
**Headline:** OtOpcUa has **no startup options validation at all**. A repo-wide search for
`IValidateOptions` and `ValidateOnStart` returns **zero** hits in `src/`. Options are bound with
bare `.Bind(...)` and never validated. The only "validation" in the configuration namespace is
`DraftValidator` — but that is **runtime draft/snapshot validation of operator config drafts**,
not `IConfiguration`/options validation, and it is **out of scope** for the shared library.
This makes OtOpcUa the **lightest** consumer: there is nothing to *replace*, only an optional
opportunity to *add* the missing startup validation using the shared base.
## 1. Options binding — no validation
`src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs`:
- `:99``builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap"));`
Bound, **not** validated — no `ValidateOnStart()`, no registered `IValidateOptions<LdapOptions>`.
A blank `Ldap:Server` / `Ldap:SearchBase` would surface only later, as a low-level LDAP error on
the first login (the exact failure mode ScadaBridge's `SecurityOptionsValidator` exists to
prevent).
`src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs`:
- `:63``_configuration.GetSection("OpcUa").Bind(options);` — the `OpcUa` section is bound
imperatively inside the hosted service, again with no validation pass.
There is no `*OptionsValidator` type and no `AddValidatedOptions`-style helper anywhere in `src/`.
The repo simply trusts its config sections.
## 2. `DraftValidator` / `DraftSnapshot` — runtime draft validation (OUT OF SCOPE)
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs`:
- `:14``public static class DraftValidator` — a **managed pre-publish validator** (its own
doc-comment, `:713`, frames it as the managed-code complement to the T-SQL `sp_ValidateDraft`).
- `:24``public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)` — runs seven
rule groups (`:2834`): UNS segment regex (`:42`), path length ≤ 200 (`:64`), EquipmentUuid
immutability (`:89`), same-cluster namespace binding (`:104`), reservation pre-flight (`:125`),
EquipmentId derivation (`:153`), driver/namespace compatibility (`:165`).
- `:206``public static IReadOnlyList<ValidationError> ValidateClusterTopology(...)` — a second
managed guard for cluster topology vs `RedundancyMode`.
- It returns **every** failing rule in one pass — same "surface all errors" philosophy this
component normalizes — but over **database draft rows** (`DraftSnapshot`), not `IConfiguration`.
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshot.cs`:
- `:9``public sealed class DraftSnapshot` — the input bag: namespaces, driver instances,
equipment, UNS areas/lines, tags, poll groups, plus prior-generation rows for cross-generation
invariants. These are domain entities (`ZB.MOM.WW.OtOpcUa.Configuration.Entities`), not options.
`DraftValidator` is referenced only by its tests
(`tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorTests.cs`) and the publish
pipeline — never from any DI / options registration. It produces `ValidationError`
(`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/ValidationError.cs`), a domain record, not
`ValidateOptionsResult`.
**Why it stays per-project:** it validates an operator's configuration *content* (the equipment
hierarchy they are about to publish), with rules that are entirely OtOpcUa domain knowledge (UNS
regex, EquipmentId derivation, Galaxy driver/namespace rules). It is not the cross-cutting
"validate the host's config section at startup" concern the shared library normalizes. Nothing
about it changes on adoption.
## 3. Summary
| Surface | What exists | Shared-lib relevance |
|---|---|---|
| Startup options validation | **None**`LdapOptions`/`OpcUa` bound with bare `.Bind()` | **Gap** — could adopt `OptionsValidatorBase` + `AddValidatedOptions` |
| `IValidateOptions` / `ValidateOnStart` | **Zero usages in `src/`** | nothing to migrate |
| Pre-host raw-config preflight | **None** | could adopt `ConfigPreflight` if pre-host keys emerge |
| Runtime draft validation | `DraftValidator` + `DraftSnapshot` (one-pass, all errors) | **out of scope** — stays per-project |
---
## Adoption plan → `ZB.MOM.WW.Configuration`
OtOpcUa is the lightest consumer — adoption is **additive**, not a replacement, and is entirely
optional (no existing validation is wrong, there just isn't any).
**Add startup validation for the bound sections (optional, recommended):**
- For `Ldap`: add an `LdapStartupOptionsValidator : OptionsValidatorBase<LdapOptions>` that calls
`v.Required(o.Server, "Ldap:Server")` and `v.Required(o.SearchBase, "Ldap:SearchBase")`
(mirroring ScadaBridge's `SecurityOptionsValidator` intent), then replace
`Program.cs:99`'s `AddOptions<LdapOptions>().Bind(...)` with
`AddValidatedOptions<LdapOptions, LdapStartupOptionsValidator>(builder.Configuration, "Ldap")`.
- For `OpcUa`: if any field has a fail-fast invariant (e.g. a required endpoint or a port), add an
`OptionsValidatorBase<OpcUaOptions>` and move the `:63` imperative `.Bind` into
`AddValidatedOptions` at composition time. Skip if the section has no hard invariants.
**Keep bespoke (unchanged):**
- `DraftValidator` and `DraftSnapshot`**out of scope**. Runtime draft/snapshot validation,
domain rules, `ValidationError` output, publish-pipeline call site — all stay exactly as they
are. Do **not** fold them into `OptionsValidatorBase`; they are not options validation.
**Status:** OtOpcUa has no validator to migrate today, so its adoption is purely the *new*
guarding work above. It is a **follow-on** (tracked in [`../GAPS.md`](../GAPS.md)), low priority —
the lowest-stakes of the three because there is no drift to correct, only an absence to optionally
fill once the package is referenced.