namespace ZB.MOM.WW.OtOpcUa.Cluster; /// /// Selects and configures the transport carrying central→node commands and node→central /// deployment acks (per-cluster mesh Phase 2). /// /// /// /// selects between a pair-local transport and a cross-mesh /// one. is the default and cannot cross an application-Cluster /// boundary; is the only mode that can, and /// requires it on any node carrying a /// cluster-{ClusterId} role. /// /// /// The DistributedPubSub branch is retained, not scheduled for deletion. An earlier /// comment here promised Phase 6 would delete it along with the single mesh; that was /// deliberately reversed — see docs/Configuration.md § "The compiled transport-mode /// defaults were deliberately NOT flipped". DPS remains the correct and default transport for /// a single-mesh topology, which every integration test and every profile that leaves this /// key unset still runs. /// /// /// Discovery is deliberately asymmetric, matching the sister project. Central /// discovers nodes from the database — enabled, non-maintenance ClusterNode /// rows, rebuilt every . Nodes discover central from /// this section (): static, restart to change. The /// asymmetry follows the rate of change — operators add and retire nodes, but central's own /// address is part of the deployment. /// /// public sealed class MeshTransportOptions { /// Configuration section name. public const string SectionName = "MeshTransport"; /// /// Every command stays on DistributedPubSub — pair-local, and the default. Cannot cross an /// application-Cluster boundary. /// public const string ModeDps = "Dps"; /// /// Commands cross the mesh boundary as unary gRPC calls on the mesh_transport.v1 /// contract — central dials each node's NodeCommandService, a node posts its /// deployment ack to central's CentralCommandService. The command plane is /// authenticated per application Cluster (). /// public const string ModeGrpc = "Grpc"; /// /// (default) or . Any other value fails the host /// at startup rather than silently falling back — a transport that is neither delivers /// nothing, and the symptom would be commands that vanish. That includes the retired /// ClusterClient value: a node still carrying it is refused at boot rather than /// quietly demoted to DPS, which on a split-topology node would deliver nothing at all. /// public string Mode { get; set; } = ModeDps; /// /// How often central re-reads ClusterNode rows to rebuild its dispatch set. /// Refreshing on a timer (rather than only on admin change) means a row edited by any /// process — migration, direct SQL, a second admin node — is picked up without a restart. /// public int ContactRefreshSeconds { get; set; } = 60; /// /// MeshTransport:CentralGrpcEndpoints — node-side only, and only under /// : the gRPC base addresses of the central pair's /// CentralCommandService, e.g. http://central-1:4055. Full URLs /// (scheme://host:port), not bare host:port — http:// is h2c. /// /// /// Tried in order, stopping at the first success, which is what keeps the ack /// one-per-node-per-dispatch: either central node routes it to the same /// ConfigPublishCoordinator singleton proxy, so which one answers does not matter, /// but two answering would double-count. /// public string[] CentralGrpcEndpoints { get; set; } = []; /// /// MeshTransport:GrpcPsk — this node's pre-shared key for the command plane, one per /// application Cluster. Under it is both the bearer this node /// presents when posting an ack to central and the bearer it requires on inbound /// NodeCommandService calls. /// /// /// /// Production form is a secret reference — ${secret:OT-MESH-PSK-<clusterId>}, /// e.g. ${secret:OT-MESH-PSK-SITE-A} — resolved pre-host by the existing /// SecretReferenceExpander, so it works on a driver-only node with no ConfigDb. /// Supply a literal only on a rig, via the environment /// (MeshTransport__GrpcPsk); never commit one. /// /// /// Per-Cluster, not fleet-wide, on purpose. The three existing shared-bearer gRPC /// channels are read/telemetry planes; this one carries RestartDriver, /// AlarmCommand and deployment dispatch, so a single fleet key would let one /// compromised site node command every other site and forge acks fleet-wide. /// /// /// Nullable because unset must be distinguishable from blank at the boundary that /// fails closed: the serve-side interceptor rejects every call when no key is /// configured rather than defaulting to an empty comparison that any empty bearer would /// satisfy. /// /// public string? GrpcPsk { get; set; } /// /// MeshTransport:DispatchTimeoutSeconds — central-side deadline, in seconds, for one /// NodeCommandService.Execute call to one node. Must be greater than 0 under /// . /// /// /// Bounded per node because a fan-out dispatch dials every enabled node in the cluster: an /// unbounded call to one unreachable node would stall the commands queued behind it. Expiry /// is logged and dropped — the transport is fire-and-forget and layers no retry, so the /// deploy path's 2-minute apply deadline stays the only recovery mechanism. /// Validation of this key is scoped to driver-role nodes; that covers the central pair only /// because central nodes are always fused (admin,driver,cluster-*). A pure-admin /// central node would leave this unvalidated on the node that uses it. /// public int DispatchTimeoutSeconds { get; set; } = 10; /// /// MeshTransport:AckTimeoutSeconds — node-side deadline, in seconds, for one /// CentralCommandService.ReportApply call to one central endpoint. Must be greater /// than 0 under . /// /// /// Applied per endpoint, not per ack: the node walks in /// order, so a dead first endpoint costs this deadline before the second is tried. /// public int AckTimeoutSeconds { get; set; } = 10; /// /// MeshTransport:ClusterPsks:{clusterId} — central-side override map of per-cluster /// PSKs, consulted before the Secrets store when central builds a channel to (or /// verifies an ack from) a given Cluster. Keyed case-insensitively. /// /// /// Production leaves this empty and the runtime provider resolves /// OT-MESH-PSK-{clusterId} from the Secrets store instead; the map exists so the /// docker-dev rig and integration tests can supply literals through the environment /// (MeshTransport__ClusterPsks__SITE-A) without standing up a KEK. Central needs a map /// rather than a single key because its cluster set is DB-dynamic — one key per site pair it /// talks to — whereas a node holds exactly its own (). /// public Dictionary ClusterPsks { get; set; } = new(StringComparer.OrdinalIgnoreCase); }