diff --git a/docs/plans/2026-07-22-clusterclient-to-grpc-live-gate.md b/docs/plans/2026-07-22-clusterclient-to-grpc-live-gate.md index 43505171..6bbed433 100644 --- a/docs/plans/2026-07-22-clusterclient-to-grpc-live-gate.md +++ b/docs/plans/2026-07-22-clusterclient-to-grpc-live-gate.md @@ -475,5 +475,68 @@ default (Phase 4 flipped the shipped appsettings to `CentralGrpcEndpoints` — u redeploy from `main` now yields all-gRPC, not all-Akka). No ClusterClient remains anywhere in the running system or the source tree. Deferred, unchanged from earlier phases: the instance-dependent central→site RPC matrix (`ExecuteOpcUa`/`ExecuteRoute`/standby parked retry) and live -`TriggerFailover`, all unit-proven; PSK **rotation** on a live pair; `docker-env2` was updated with -its own key but not gated here. +`TriggerFailover`, all unit-proven; PSK **rotation** on a live pair. `docker-env2` is now gated — +see below. + +--- + +## Env2 gate — `docker-env2` on the gRPC PSK build — **PASS** (2026-07-23, Gitea #31) + +The secondary Transport-testing topology (`docker-env2/`: 2 central + 1 site-x × 2 nodes, host +ports 91XX, shared infra) carried its `GrpcPsk` in config since Phase 0 but had never been +redeployed or gated against the migration build. Rebuilt `scadabridge:latest` from `main` @ +`8524a7f7` (includes the #28 Transport execution-strategy fix) and recreated only the env2 +containers (`bash docker-env2/deploy.sh`; the primary rig's running image was untouched). Site-x +PSK `dev-grpc-psk-docker-env2-site-x` on both site nodes matches central's +`ScadaBridge__Communication__SitePsks__site-x`. + +| # | Check | Result | +|---|-------|--------| +| 1 | Both site nodes boot with the key (StartupValidator passes) | **PASS** | +| 2 | Unauthenticated control-plane call ⇒ PermissionDenied; correct key ⇒ success | **PASS** | +| 3 | LocalDb unaffected | **PASS** | + +### Check 1 — both site nodes boot + +`scadabridge-env2-site-x-a` and `-b` both reached `Application started` with `Now listening on: +http://[::]:8083` (the gRPC control/data listener). `StartupValidator` is fail-closed on a missing +`ScadaBridge:Communication:GrpcPsk` — a site node without it refuses to boot — so reaching +`Application started` is itself the proof the key is present and valid. Each node also logged a clean +`Reconcile pass for site site-x node node-{a,b} complete: 0 fetched, 0 failed, 0 orphan(s)`, i.e. +the site→central gRPC path was already carrying traffic. + +### Check 2 — PSK auth on the control plane + +`grpcurl` against the site-hosted `SiteStreamService/PullAuditEvents` on both nodes +(`localhost:9123` = site-x-a, `localhost:9124` = site-x-b): + +- **No auth header** ⇒ `PermissionDenied: Control plane authentication failed.` +- **Wrong bearer key** (`Bearer WRONG-KEY`, `x-scadabridge-site: site-x`) ⇒ `PermissionDenied`. +- **Correct key** (`Bearer dev-grpc-psk-docker-env2-site-x`) ⇒ `{}` (empty audit set — the env has + no S&F driver seeded — but **not** a permission error: auth accepted). Identical on both nodes. + +(Same contract clarification as the primary rig: the site interceptor authenticates the node's single +`GrpcPsk` and does not require `x-scadabridge-site`; per-site isolation is proven by the wrong-key +rejection.) + +### Check 3 — LocalDb unaffected + +Env2 site-x runs LocalDb **local-only** (no `LocalDb:Replication:PeerAddress` configured — the rig's +replication posture is proven on the primary `docker/` stack, not here), so "unaffected" means the +consolidated site DB stays operational on the gRPC build. Site logs hold **0** LocalDb +errors/exceptions and both nodes boot healthy with the LocalDb at `/app/data/site-localdb.db`; the +clean reconcile passes above confirm the site DB + site→central path work end to end. + +### Bonus / observations + +- **No real ClusterClient.** Central logs hold **0** ClusterClient/receptionist references; the site + logs' only match is the benign string label `client=ClusterClientSiteAuditClient` in a + `SiteAuditTelemetryActor created` line (a legacy identifier, **not** an Akka `ClusterClient`) — + identical to the primary rig's site nodes. No real receptionist/ClusterClient actor path exists. +- **Central registers site-x over gRPC.** `Site site-x registered online via heartbeat` — the + site→central gRPC command/control path is live. +- **Seed-data note (not a migration issue).** `ScadaBridgeConfig2.dbo.Sites` has **0 rows**, so + central logs `Reconcile request from unknown site 'site-x' … replying with empty gap` and evicts + it from the health aggregator as "no longer configured." This is a first-time-setup seeding gap + (`docker-env2/seed-sites.sh` not yet run on the fresh DB), orthogonal to the transport — which + demonstrably carries the heartbeat and reconcile regardless.