diff --git a/docker-dev/docker-compose.yml b/docker-dev/docker-compose.yml index 9db7aac3..5330a628 100644 --- a/docker-dev/docker-compose.yml +++ b/docker-dev/docker-compose.yml @@ -40,6 +40,23 @@ # + seeded clusters across `docker compose up` cycles; without it a recreate # silently drops the OtOpcUa database. # +# Per-cluster mesh Phase 5 (gRPC live-telemetry stream): every driver-role node (all six, +# including the fused central pair) carries Telemetry__GrpcListenPort=4056 (dedicated h2c +# listener, verified free against every other per-node port: Akka 4053, ConfigServe 4055 on +# central, LocalDb sync 9001 on site-a, OPC UA 4840, HTTP 9000) + Telemetry__ApiKey +# (committed dev key, matches the SQL password / secrets KEK / ConfigServe key exception). +# central-1/central-2 also carry the matching TelemetryDial__ApiKey. Both Telemetry__Mode and +# TelemetryDial__Mode are left UNSET here, so every node defaults to "Dps" (dark — telemetry +# keeps riding the existing DistributedPubSub topic). The ClusterNode.GrpcPort column (seeded +# by docker-dev/seed/seed-clusters.sql) points central's dial supervisor at each node's :4056. +# Unlike MeshTransport/ConfigSource, no `${VAR:-Dps}` interpolation is wired for either Mode key +# (a bare shell export does nothing here — these keys aren't referenced anywhere in this file, so +# there is nothing for it to substitute into). To run the Phase 5 live gate, manually add +# `Telemetry__Mode: "Grpc"` to the `environment:` block of all six driver nodes (central-1, +# central-2, site-a-1, site-a-2, site-b-1, site-b-2) and `TelemetryDial__Mode: "Grpc"` to +# central-1's and central-2's blocks — or supply both via a docker-compose.override.yml carrying +# the same keys — then `docker compose -f docker-dev/docker-compose.yml up -d` to recreate. +# # Usage: # docker compose -f docker-dev/docker-compose.yml up -d --build # open http://localhost:9200 # central Blazor admin UI @@ -186,6 +203,16 @@ services: ConfigServe__GrpcListenPort: "4055" ConfigServe__ApiKey: "configserve-docker-dev-key" ConfigSource__Mode: "Direct" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream). DARK SWITCH — Telemetry:Mode is left + # unset here (defaults to "Dps"), so the node keeps publishing telemetry on the existing + # DistributedPubSub topic; the dedicated gRPC listener below binds regardless (the node "always + # hosts" per docs/Telemetry.md) so flipping the mode later is a config change, not a redeploy. + # Port 4056 checked free against every other port this node binds: Akka 4053, ConfigServe 4055, + # OPC UA 4840 (container-internal), HTTP 9000. TelemetryDial__ApiKey (below) is central's + # dial-side key and must equal every node's Telemetry:ApiKey (fail-closed interceptor). + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" + TelemetryDial__ApiKey: "telemetry-docker-dev-key" # Both redundancy peers are seeds (#459 / ScadaBridge parity) so a restarted node can # re-join the mesh via EITHER peer, not only central-1. # @@ -282,6 +309,11 @@ services: ConfigServe__GrpcListenPort: "4055" ConfigServe__ApiKey: "configserve-docker-dev-key" ConfigSource__Mode: "Direct" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream) — see central-1. DARK SWITCH: Mode + # stays unset (⇒ Dps); the dedicated :4056 listener binds regardless. + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" + TelemetryDial__ApiKey: "telemetry-docker-dev-key" # Both redundancy peers are seeds (#459 / ScadaBridge parity) — see central-1. SELF FIRST: # central-2 lists itself as seed-nodes[0], which is what lets it cold-start while central-1 # is down (it previously listed central-1 first and simply never came Up in that case). @@ -374,6 +406,13 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream) — see central-1/central-2. DARK + # SWITCH: Telemetry:Mode stays unset (⇒ Dps); the dedicated :4056 listener binds regardless + # (the node "always hosts" per docs/Telemetry.md). Port checked free against this node's + # other ports: Akka 4053, OPC UA 4840, HTTP 9000, LocalDb sync 9001 (below). ApiKey must + # equal central's TelemetryDial:ApiKey (fail-closed interceptor). + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" # Site nodes are deliberately NOT seeds — they join the central pair's mesh. The self-first # seed rule is conditional for exactly this reason (it binds only when a node's own address # is in its own seed list), so these configs are exempt rather than broken. @@ -462,6 +501,10 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream) — see central-1/site-a-1. DARK + # SWITCH: Telemetry:Mode stays unset (⇒ Dps); the dedicated :4056 listener binds regardless. + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053" Cluster__Roles__0: "driver" # Mesh command transport (per-cluster mesh Phase 2). DARK SWITCH: the rig comes up on "Dps", @@ -539,6 +582,10 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream) — see central-1/site-a-1. DARK + # SWITCH: Telemetry:Mode stays unset (⇒ Dps); the dedicated :4056 listener binds regardless. + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053" Cluster__Roles__0: "driver" # Mesh command transport (per-cluster mesh Phase 2). DARK SWITCH: the rig comes up on "Dps", @@ -599,6 +646,10 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # Per-cluster mesh Phase 5 (gRPC live-telemetry stream) — see central-1/site-a-1. DARK + # SWITCH: Telemetry:Mode stays unset (⇒ Dps); the dedicated :4056 listener binds regardless. + Telemetry__GrpcListenPort: "4056" + Telemetry__ApiKey: "telemetry-docker-dev-key" Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053" Cluster__Roles__0: "driver" # Mesh command transport (per-cluster mesh Phase 2). DARK SWITCH: the rig comes up on "Dps", diff --git a/docker-dev/seed/seed-clusters.sql b/docker-dev/seed/seed-clusters.sql index a2edf468..62bfa596 100644 --- a/docker-dev/seed/seed-clusters.sql +++ b/docker-dev/seed/seed-clusters.sql @@ -16,6 +16,12 @@ -- Host = Compose service name (resolves inside the otopcua-dev network). -- OpcUaPort stays at the container-internal 4840; the host-side port mapping is in -- docker-compose.yml ports: blocks and is irrelevant to ClusterNode rows. +-- +-- GrpcPort (per-cluster mesh Phase 5): every node's dedicated live-telemetry gRPC listener, +-- matching Telemetry__GrpcListenPort=4056 set on all six host nodes in docker-compose.yml. +-- TelemetryNodeSource (central) reads Host + GrpcPort to build its dial-target set +-- (http://{Host}:{GrpcPort}); a NULL GrpcPort is skipped with a Warning. Mirrors how AkkaPort +-- was seeded for Phase 1's ClusterNodeAddressReconcilerActor. SET NOCOUNT ON; SET XACT_ABORT ON; @@ -64,13 +70,13 @@ IF NOT EXISTS (SELECT 1 FROM dbo.ServerCluster WHERE ClusterId = 'SITE-B') IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'central-1:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('central-1:4053', 'MAIN', 'central-1', 4840, 8081, 4053, 'urn:OtOpcUa:central-1', 200, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('central-1:4053', 'MAIN', 'central-1', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:central-1', 200, 1, 'docker-dev-seed'); IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'central-2:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('central-2:4053', 'MAIN', 'central-2', 4840, 8081, 4053, 'urn:OtOpcUa:central-2', 150, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('central-2:4053', 'MAIN', 'central-2', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:central-2', 150, 1, 'docker-dev-seed'); ------------------------------------------------------------------------------ -- ClusterNode — site A @@ -78,13 +84,13 @@ IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'central-2:4053') IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-1:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('site-a-1:4053', 'SITE-A', 'site-a-1', 4840, 8081, 4053, 'urn:OtOpcUa:site-a-1', 200, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('site-a-1:4053', 'SITE-A', 'site-a-1', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:site-a-1', 200, 1, 'docker-dev-seed'); IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-2:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('site-a-2:4053', 'SITE-A', 'site-a-2', 4840, 8081, 4053, 'urn:OtOpcUa:site-a-2', 150, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('site-a-2:4053', 'SITE-A', 'site-a-2', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:site-a-2', 150, 1, 'docker-dev-seed'); ------------------------------------------------------------------------------ -- ClusterNode — site B @@ -92,13 +98,13 @@ IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-2:4053') IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-1:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('site-b-1:4053', 'SITE-B', 'site-b-1', 4840, 8081, 4053, 'urn:OtOpcUa:site-b-1', 200, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('site-b-1:4053', 'SITE-B', 'site-b-1', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:site-b-1', 200, 1, 'docker-dev-seed'); IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-2:4053') INSERT INTO dbo.ClusterNode - (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) - VALUES ('site-b-2:4053', 'SITE-B', 'site-b-2', 4840, 8081, 4053, 'urn:OtOpcUa:site-b-2', 150, 1, 'docker-dev-seed'); + (NodeId, ClusterId, Host, OpcUaPort, DashboardPort, AkkaPort, GrpcPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy) + VALUES ('site-b-2:4053', 'SITE-B', 'site-b-2', 4840, 8081, 4053, 4056, 'urn:OtOpcUa:site-b-2', 150, 1, 'docker-dev-seed'); ------------------------------------------------------------------------------ -- Galaxy MxAccess gateway — INTENTIONALLY NOT SEEDED (retired 2026-06-15)