Files
scadaproj/components/configuration/current-state/otopcua/CURRENT-STATE.md
T
Joseph Doherty fbf0f23e76 docs(config): correct OtOpcUa draft-validation description
The C# DraftValidator/DraftSnapshot has NO live caller in OtOpcUa src/ (verified
repo-wide) — it is dormant complement code. The enforced pre-publish draft
validation runs DB-side in the sp_ValidateDraft stored procedure (Status='Draft'
-> sp_PublishGeneration lifecycle). Reframe across current-state/SPEC/GAPS/README/
CLAUDE.md from 'runtime draft validation' + a false publish-pipeline caller to
'dormant managed validator; enforcement is DB-side'. Out-of-scope conclusion
for ZB.MOM.WW.Configuration is unchanged.
2026-06-01 10:13:29 -04:00

108 lines
7.1 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-shaped type in the configuration
namespace is the C# `DraftValidator` — but it is **dormant (no live caller in `src/`)** and, by
design, concerns config-*generation content*, not `IConfiguration`/options. The enforced pre-publish
draft validation actually runs **DB-side** in the `sp_ValidateDraft` stored procedure. Either way,
draft/generation validation is **out of scope** for the shared options-validation 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` — dormant managed draft validator (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`).
In the current tree that complement is **not wired in** — see the no-caller note below.
- `:24``public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)`*would* run
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`) — a repo-wide search
finds **no live caller in `src/`** (nothing constructs a `DraftSnapshot` or calls
`DraftValidator.Validate`/`ValidateClusterTopology`), and it is never registered in DI or options.
The enforced pre-publish validation lives **DB-side** in the `sp_ValidateDraft` stored procedure
(`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260417215224_StoredProcedures.cs:157+`,
called as part of the `Status='Draft' → sp_PublishGeneration` generation lifecycle); the managed
`DraftValidator` is currently **dormant complement code**. When it does run it produces
`ValidationError` (`.../Validation/ValidationError.cs`), a domain record, not `ValidateOptionsResult`.
**Why it stays per-project:** it (and its live DB counterpart `sp_ValidateDraft`) 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 |
| Draft/generation validation | DB `sp_ValidateDraft` (live, in the publish lifecycle) + C# `DraftValidator` (dormant, no `src/` caller) | **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**. Draft/generation content validation
(enforced DB-side by `sp_ValidateDraft`, with the managed `DraftValidator` as dormant complement
code), domain rules, `ValidationError` output — all stay exactly as they are. Do **not** fold them
into `OptionsValidatorBase`; they are not options validation. (Whether the unused C# `DraftValidator`
should be revived or removed is an OtOpcUa housekeeping question, unrelated to this component.)
**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.