348 lines
31 KiB
Markdown
348 lines
31 KiB
Markdown
# Component: Host
|
||
|
||
## Purpose
|
||
|
||
The Host component is the single deployable executable for the entire ScadaBridge system. The same binary runs on every node — central and site alike. The node's role is determined entirely by configuration (`appsettings.json`), not by which binary is deployed. On central nodes the Host additionally bootstraps ASP.NET Core to serve the Central UI and Inbound API web endpoints.
|
||
|
||
## Location
|
||
|
||
All nodes (central and site).
|
||
|
||
## Responsibilities
|
||
|
||
- Serve as the single entry point (`Program.cs`) for the ScadaBridge process.
|
||
- Read and validate node configuration at startup before any actor system is created.
|
||
- Register the correct set of component services and actors based on the configured node role.
|
||
- Bootstrap the Akka.NET actor system with Remoting, Clustering, and split-brain resolution from a hand-assembled, injection-safe HOCON document (`AkkaHostedService.BuildHocon`; see REQ-HOST-6).
|
||
- Host ASP.NET Core web endpoints on central nodes only.
|
||
- Configure structured logging (Serilog) with environment-specific enrichment.
|
||
- Support running as a Windows Service in production and as a console application during development.
|
||
- Perform graceful shutdown via Akka.NET CoordinatedShutdown when the service is stopped.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
### REQ-HOST-1: Single Binary Deployment
|
||
|
||
The same compiled binary must be deployable to both central and site nodes. The node's role (Central or Site) is determined solely by configuration values in `appsettings.json` (or environment-specific overrides). There must be no separate build targets, projects, or conditional compilation symbols for central vs. site.
|
||
|
||
### REQ-HOST-2: Role-Based Service Registration
|
||
|
||
At startup the Host must inspect the configured node role and register only the component services appropriate for that role:
|
||
|
||
- **Shared** (both Central and Site): ClusterInfrastructure, Communication, HealthMonitoring, ExternalSystemGateway.
|
||
- **Central only**: TemplateEngine, DeploymentManager, Security, AuditLogging, CentralUI, InboundAPI, ManagementService, NotificationService, NotificationOutbox, SiteCallAudit.
|
||
- **Site only**: SiteRuntime, DataConnectionLayer, StoreAndForward, SiteEventLogging.
|
||
|
||
Components not applicable to the current role must not be registered in the DI container or the Akka.NET actor system.
|
||
|
||
### REQ-HOST-3: Configuration Binding
|
||
|
||
The Host must bind configuration sections from `appsettings.json` to strongly-typed options classes using the .NET **Options pattern** (`IOptions<T>` / `IOptionsSnapshot<T>`). Each component has its own configuration section under `ScadaBridge`, mapped to a dedicated configuration class owned by that component.
|
||
|
||
#### Infrastructure Sections
|
||
|
||
| Section | Options Class | Owner | Contents |
|
||
|---------|--------------|-------|----------|
|
||
| `ScadaBridge:Node` | `NodeOptions` | Host | Role, NodeHostname, SiteId, RemotingPort, GrpcPort (site: hosts `SiteCommandService` + `SiteStreamGrpcServer`, default 8083), CentralGrpcPort (central: hosts `CentralControlService`, default 8083) |
|
||
| `ScadaBridge:Cluster` | `ClusterOptions` | ClusterInfrastructure | SeedNodes, SplitBrainResolverStrategy, StableAfter, HeartbeatInterval, FailureDetectionThreshold, MinNrOfMembers |
|
||
| `ScadaBridge:Database` | `DatabaseOptions` | Host | Central: ConfigurationDb, MachineDataDb connection strings; Site: SQLite paths |
|
||
|
||
#### Per-Component Sections
|
||
|
||
| Section | Options Class | Owner | Contents |
|
||
|---------|--------------|-------|----------|
|
||
| `ScadaBridge:DataConnection` | `DataConnectionOptions` | Data Connection Layer | ReconnectInterval, TagResolutionRetryInterval, WriteTimeout |
|
||
| `ScadaBridge:StoreAndForward` | `StoreAndForwardOptions` | Store-and-Forward | SqliteDbPath, ReplicationEnabled |
|
||
| `ScadaBridge:HealthMonitoring` | `HealthMonitoringOptions` | Health Monitoring | ReportInterval, OfflineTimeout |
|
||
| `ScadaBridge:SiteEventLog` | `SiteEventLogOptions` | Site Event Logging | RetentionDays, MaxStorageMb, PurgeScheduleCron |
|
||
| `ScadaBridge:Communication` | `CommunicationOptions` | Communication | DeploymentTimeout, LifecycleTimeout, QueryTimeout, TransportHeartbeatInterval, TransportFailureThreshold |
|
||
| `ScadaBridge:Security` | `SecurityOptions` | Security & Auth | LdapServer, LdapPort, LdapUseTls, JwtSigningKey, JwtExpiryMinutes, IdleTimeoutMinutes |
|
||
| `ScadaBridge:InboundApi` | `InboundApiOptions` | Inbound API | DefaultMethodTimeout, MaxRequestBodyBytes, ApiMethodCacheTtl, StandbyForwardPeerEndpoints (HTTP base URLs of the other central node — a standby relays `POST /api/*` there instead of answering 503), StandbyForwardTimeout |
|
||
| `ScadaBridge:Notification` | `NotificationOptions` | Notification Service | (SMTP config is stored in the central config DB, not in appsettings) |
|
||
| `ScadaBridge:NotificationOutbox` | `NotificationOutboxOptions` | Notification Outbox | Dispatcher poll interval, stuck-age threshold, retention window (delivery retry settings reuse the central SMTP configuration) |
|
||
| `ScadaBridge:SiteCallAudit` | `SiteCallAuditOptions` | Site Call Audit | Reconciliation pull interval, stuck-age threshold, retention window |
|
||
| `ScadaBridge:ManagementService` | `ManagementServiceOptions` | Management Service | (Reserved for future configuration) |
|
||
| `ScadaBridge:Logging` | `LoggingOptions` | Host | Serilog sink configuration, log level overrides |
|
||
|
||
#### Convention
|
||
|
||
- Each component defines its own options class (e.g., `DataConnectionOptions`) in its own project. The class is a plain POCO with properties matching the JSON section keys.
|
||
- The Host binds each section during startup via `services.Configure<T>(configuration.GetSection("ScadaBridge:<ComponentName>"))`.
|
||
- Each component's `AddXxx()` extension method accepts `IServiceCollection` and reads its options via `IOptions<T>` — the component never reads `IConfiguration` directly.
|
||
- Options classes live in the component project, not in Commons, because they are component-specific configuration — not shared contracts.
|
||
- Startup validation (REQ-HOST-4) validates all required options before the actor system starts.
|
||
|
||
### REQ-HOST-4: Startup Validation
|
||
|
||
Before the Akka.NET actor system is created, the Host must validate all required configuration values and fail fast with a clear error message if any are missing or invalid. Validation rules include:
|
||
|
||
- `NodeConfiguration.Role` must be a valid `NodeRole` value.
|
||
- `NodeConfiguration.NodeHostname` must not be null or empty.
|
||
- `NodeConfiguration.RemotingPort` must be in valid port range (1–65535).
|
||
- Site nodes must have `GrpcPort` in valid port range (1–65535) and different from `RemotingPort`.
|
||
- Site nodes must have a non-empty `SiteId`.
|
||
- Central nodes must have non-empty `ConfigurationDb` and `MachineDataDb` connection strings.
|
||
- Site nodes must have non-empty SQLite path values. Site nodes do **not** require a `ConfigurationDb` connection string — all configuration is received via artifact deployment and read from local SQLite.
|
||
- At least two seed nodes must be configured.
|
||
|
||
### REQ-HOST-4a: Readiness Gating
|
||
|
||
On central nodes, the ASP.NET Core web endpoints (Central UI, Inbound API) must **not accept traffic** until the node is fully operational:
|
||
|
||
- Akka.NET cluster membership is established.
|
||
- Database connectivity (MS SQL) is verified.
|
||
- Required cluster singletons are running (if applicable).
|
||
|
||
These are implemented as three `Ready`-tagged health checks registered in the Central-role branch of `Program.cs` (so they are naturally role-scoped — site nodes do not run them): `database` (`DatabaseHealthCheck<ScadaBridgeDbContext>`), `akka-cluster` (`AkkaClusterHealthCheck`), and `required-singletons` (`RequiredSingletonsHealthCheck`). The last verifies each *required-always* central singleton is reachable by Asking its local `ClusterSingletonProxy` an `Identify` with a short bounded timeout (~2s, probes run concurrently) and treating a non-null `ActorIdentity.Subject` as reachable; any unreachable required singleton degrades the check to **Unhealthy**, naming it. The required-always set is the five unconditional central singletons: notification-outbox, audit-log-ingest, site-call-audit, audit-log-purge, and site-audit-reconciliation. Feature-gated singletons are the "if applicable" case and are not probed when their feature is off. The check is leadership-agnostic — the proxy reaches the singleton from either central node, so a ready standby still reports ready (readiness must NOT require cluster leadership; that is the `Active` tier's job). During a brief singleton handover the probe may momentarily time out and the node may flap to not-ready, which is correct: a node mid-handover is legitimately not fully ready (no retries are used, to keep readiness polling fast).
|
||
|
||
A standard ASP.NET Core health check endpoint (`/health/ready`) reports readiness status. The load balancer uses this endpoint to determine when to route traffic to the node. During startup or failover, the node returns `503 Service Unavailable` until ready.
|
||
|
||
### REQ-HOST-5: Windows Service Hosting
|
||
|
||
The Host must support running as a Windows Service via `UseWindowsService()`. When launched outside of a Windows Service context (e.g., during development), it must run as a standard console application. No code changes or conditional compilation are required to switch between the two modes.
|
||
|
||
### REQ-HOST-6: Akka.NET Bootstrap
|
||
|
||
The Host bootstraps the Akka.NET actor system from a **hand-assembled, injection-safe HOCON document** (`AkkaHostedService.BuildHocon`) passed to `ActorSystem.Create` — **not** Akka.Hosting's `AkkaConfigurationBuilder`/`AddAkka()`. Every interpolated value is routed through `QuoteHocon`/`DurationHocon` so a hostname, seed URI or strategy containing a quote, backslash or whitespace cannot corrupt the document, and each timing is rendered in milliseconds so sub-second values survive. The bound `ClusterOptions`/`NodeOptions`/`CommunicationOptions` are startup-validated (`ClusterOptionsValidator`, `StartupValidator`) so a misconfiguration fails fast with a key-naming message. The document configures:
|
||
|
||
- **Remoting**: the node's hostname and port from `NodeOptions`.
|
||
- **Clustering**: seed nodes and the node's cluster role(s) from configuration.
|
||
- **Split-Brain Resolver**: the strategy, stable-after and `down-if-alone` from `ClusterOptions`, **with the downing provider named explicitly** (`akka.cluster.downing-provider-class = "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster"`). Akka's default is `NoDowning`, under which the entire SBR section is inert and singletons never migrate on a crash or partition — omitting this one line silently disables failover. This exact omission was arch-review 01's Critical finding; it is now guarded by tests asserting the parsed HOCON key **and** a two-node kill test asserting the behavior (downing + singleton migration).
|
||
- **Coordinated shutdown**: `run-coordinated-shutdown-when-down = on` plus a cluster-leave phase timeout above the singleton drain budget (see REQ-HOST-4a / the down-if-alone recovery contract in Component-ClusterInfrastructure.md).
|
||
- **Actor registration**: each component's actors registered conditional on the node's role.
|
||
|
||
> **Bootstrap-mechanism note.** The `Akka.Hosting` / `Akka.Cluster.Hosting` / `Akka.Remote.Hosting` typed-builder packages were referenced but unused and were dropped (arch-review 01 Task 22). The hand-rolled HOCON path is a single hardened, production-shape code path with full test coverage; migrating to Akka.Hosting's typed options is deliberately **not** done here and is tracked as possible future work rather than carried as an unused dependency. Only `Akka.Cluster.Tools` (ClusterSingleton, transitively Akka.Cluster) remains — its ClusterClient/ClusterClientReceptionist surface is no longer used since Phase 4 moved cross-cluster messaging to gRPC.
|
||
|
||
> **Persistence note.** ScadaBridge does not use Akka.Persistence. Durable state
|
||
> (store-and-forward buffers, site event logs, static attribute writes,
|
||
> deployment records, configuration) is owned by the individual components and
|
||
> persisted through component-owned stores — SQLite at sites, MS SQL centrally —
|
||
> not through an Akka journal/snapshot store. The Host therefore configures no
|
||
> `akka.persistence` section and references no persistence plugin. There are no
|
||
> `PersistentActor` subclasses in the system by design.
|
||
|
||
### REQ-HOST-6a: gRPC command/control services (no ClusterClientReceptionist)
|
||
|
||
As of Phase 4 of the ClusterClient→gRPC migration (2026-07-23), the Host configures **no** `ClusterClientReceptionist` and registers **no** actor with one. Both remaining receptionist registrations were removed:
|
||
|
||
- The central **CentralCommunicationActor** registration is gone. Sites now reach central command/control over gRPC by dialling the central-hosted **`CentralControlService`** (`GrpcCentralTransport`, sticky central-a→central-b failover), not via a ClusterClient locating a receptionist.
|
||
- The site **SiteCommunicationActor** registration is gone. Central now reaches site command/control over gRPC by dialling the site-hosted **`SiteCommandService`** (`GrpcSiteTransport`, per-site NodeA→NodeB failover).
|
||
|
||
Central hosts `CentralControlService` (site dials in); each site hosts `SiteCommandService` and `SiteStreamGrpcServer` (central dials in). Endpoints are dialled directly from configuration/DB, so there is no cross-cluster actor discovery.
|
||
|
||
**The ManagementActor was never reachable over the receptionist either** (its registration was removed 2026-07-22, ahead of Phase 4). That registration was written for an out-of-cluster CLI that was never built: the shipped CLI speaks HTTP Basic to the central `/management` endpoints, which ask the ManagementActor **in-process** via `ManagementActorHolder` (`ManagementEndpoints.cs`). The actor itself still runs at `/user/management`.
|
||
|
||
> **Migration note.** `Akka.Cluster.Tools` stays a Host dependency for ClusterSingleton; only its ClusterClient/ClusterClientReceptionist surface is now unused. See `docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`.
|
||
|
||
### REQ-HOST-7: ASP.NET Web Endpoints
|
||
|
||
On central nodes, the Host must use `WebApplication.CreateBuilder` to produce a full ASP.NET Core host with Kestrel, and must map web endpoints for:
|
||
|
||
- Central UI (via `MapCentralUI()` extension method).
|
||
- Inbound API (via `MapInboundAPI()` extension method).
|
||
|
||
On site nodes, the Host must also use `WebApplication.CreateBuilder` (not `Host.CreateDefaultBuilder`) to host the **SiteStreamGrpcServer** and the **SiteCommandService** (central→site command/control, added in Phase 4) via Kestrel HTTP/2 on the configured `GrpcPort` (default 8083). Kestrel is configured with `HttpProtocols.Http2` on the gRPC port only — no HTTP/1.1 web endpoints are exposed. On central nodes, the Host additionally hosts the **CentralControlService** (site→central command/control) via Kestrel HTTP/2 on the configured `CentralGrpcPort` (default 8083), alongside the HTTP/1.1 Central UI / Inbound API endpoints. The gRPC services are mapped via `MapGrpcService<>()`.
|
||
|
||
**Startup ordering (site nodes)**:
|
||
1. Actor system and SiteStreamManager must be initialized before gRPC begins accepting connections.
|
||
2. The gRPC server rejects streams with `StatusCode.Unavailable` until the actor system is ready.
|
||
|
||
**Shutdown ordering (site nodes)**:
|
||
1. On `CoordinatedShutdown`, stop accepting new gRPC streams first.
|
||
2. Cancel all active gRPC streams (triggering client-side reconnect).
|
||
3. Tear down actors.
|
||
4. Use `IHostApplicationLifetime.ApplicationStopping` to signal the gRPC server.
|
||
|
||
### REQ-HOST-8: Structured Logging
|
||
|
||
The Host must configure Serilog as the logging provider with:
|
||
|
||
- Configuration-driven sink setup (console and file sinks at minimum).
|
||
- Automatic enrichment of every log entry with `SiteId`, `NodeHostname`, and `NodeRole` properties sourced from `NodeConfiguration`.
|
||
- Structured (machine-parseable) output format.
|
||
|
||
### REQ-HOST-8a: Dead Letter Monitoring
|
||
|
||
The Host must subscribe to the Akka.NET `DeadLetter` event stream and log dead letters at Warning level. Dead letters indicate messages sent to actors that no longer exist — a common symptom of failover timing issues, stale actor references, or race conditions during instance lifecycle transitions. The dead letter count is reported as a health metric (see Health Monitoring).
|
||
|
||
### REQ-HOST-9: Graceful Shutdown
|
||
|
||
When the Host process receives a stop signal (Windows Service stop, `Ctrl+C`, or SIGTERM), it must trigger Akka.NET CoordinatedShutdown to allow actors to drain in-flight work before the process exits. The Host must not call `Environment.Exit()` or forcibly terminate the actor system without coordinated shutdown.
|
||
|
||
### REQ-HOST-10: Extension Method Convention
|
||
|
||
Each component library must expose its services to the Host via a consistent set of extension methods:
|
||
|
||
- `IServiceCollection.AddXxx()` — registers the component's DI services.
|
||
- `AkkaConfigurationBuilder.AddXxxActors()` — registers the component's actors with the Akka.NET actor system (for components that have actors).
|
||
- `WebApplication.MapXxx()` — maps the component's web endpoints (only for CentralUI and InboundAPI).
|
||
|
||
The Host's `Program.cs` calls these extension methods; the component libraries own the registration logic. This keeps the Host thin and each component self-contained. (The ManagementService component no longer registers the ManagementActor with ClusterClientReceptionist — that registration was removed 2026-07-22, and the whole receptionist surface went in Phase 4.)
|
||
|
||
### REQ-HOST-11: Dependency-Injection Lifetime Discipline
|
||
|
||
Akka actors are constructed by the actor system, not by the DI container, so the container cannot enforce lifetimes on them. Three rules therefore apply, and each has a mechanical guard.
|
||
|
||
**1. An actor may hold the root `IServiceProvider` (or an `IServiceScopeFactory`) for its lifetime — never a scope or anything resolved from one.** Every operation that needs a scoped service opens its own scope (`CreateScope` / `CreateAsyncScope`, disposed in the same handler) and resolves inside it. Caching the resolved instance in a field is a **captive dependency**: the actor keeps one EF `DbContext` alive for its whole lifetime and any concurrency the actor allows then collides on it. This is not hypothetical — it shipped as register row 45, where `NotificationOutboxActor` cached delivery adapters from an actor-lifetime scope and `MaxParallelDeliveries` concurrent deliveries drove EF's "a second operation was started on this context instance" continuously on the rig.
|
||
|
||
*Guard:* `CaptiveDependencyGuard` (Host test suite) reflects over every actor class in the shipped assemblies and fails on any instance field — auto-property backing fields included — typed as `IServiceScope`, as a `DbContext`, or as a service the real composition roots register `Scoped`. The scoped set is derived from the actual registrations of the built Central and Site containers rather than hand-listed, so changing a registration's lifetime automatically re-scopes the check. `IServiceProvider` / `IServiceScopeFactory` fields are explicitly permitted. Exemptions live in a small allowlist inside the guard, each carrying its safety argument, and a self-test fails any allowlist entry that has stopped matching a real field.
|
||
|
||
**2. Scope validation is on in Development, for both roles.** `Program.cs` applies `ValidateScopes` + `ValidateOnBuild` via `ServiceProviderValidation` in both the Central and Site branches. This is the container-visible half of the same concern — it catches a singleton whose *constructor* takes a scoped service, which is the ordinary form of the bug in non-actor code. It cannot see rule 1's hand-rolled `GetRequiredService` calls, and rule 1's guard cannot see constructor injection; neither subsumes the other. Production runs unvalidated deliberately: a validation failure is fail-closed, and a defect must not take a production node down at boot when Development and CI give the same signal.
|
||
|
||
**3. A registration must be constructible on every role that registers it.** A service whose dependencies exist only on the other role belongs in a role-scoped extension method (`AddCentralCommunication`, `AddAuditLogCentralKpi`), not in a shared `AddXxx()` on the argument that "only the central actor ever resolves it". An unconstructible descriptor is not inert: it fails `ValidateOnBuild` for the *entire* graph, which is what previously kept rule 2 switched off on the Site branch and would have masked any genuine defect the validator found there.
|
||
|
||
---
|
||
|
||
### Boot-time facts the Host supplies to Health Monitoring
|
||
|
||
Health Monitoring reports two things the Host alone knows, and in both cases the Host reads the
|
||
configuration and hands over a captured value rather than teaching the component to read it. This
|
||
is the `SiteEventLogReplicationCheck` pattern generalised: configuration-reading rules live in one
|
||
place, and a component that merely *reports* a fact never grows a dependency on where it comes
|
||
from.
|
||
|
||
**LocalDb replication configured-ness** (`SiteServiceRegistration`, register row 80). The
|
||
replication engine is registered unconditionally and idles without a peer, so `ISyncStatus` answers
|
||
`Connected = false` with a real `0` backlog on a node that was never meant to replicate. Passing
|
||
that through made every deliberately-unreplicated site publish "disconnected, empty backlog". The
|
||
Host now evaluates `SiteLocalDbSetup.ReplicationIsConfigured(config)` **once** and passes it into
|
||
`AddLocalDbReplicationHealthBridge` as a third provider; an unconfigured node publishes `null` for
|
||
both LocalDb health fields. The same captured value feeds `SiteEventLogReplicationCheck`, so the
|
||
two surfaces can never disagree about whether this node replicates. Note the rule is `PeerAddress`
|
||
**OR** `ApiKey` — only the dialling half sets a peer address.
|
||
|
||
**Startup reconciler presence** (`AkkaHostedService`, register row 73). The Host decides whether to
|
||
spawn `SiteReconciliationActor` — it needs an `IDeploymentConfigFetcher`, and skips the actor when
|
||
none is resolvable. That skip was previously a lone boot Warning, so a node with no self-heal path
|
||
at all reported itself perfectly healthy. Both branches now call
|
||
`ISiteHealthCollector.SetStartupReconcilerActive(...)`, latching the fact onto every subsequent
|
||
health report as tri-state `StartupReconcilerActive`; the skip branch additionally logs at **Error**
|
||
(raised from Warning — no configuration makes the fetcher legitimately absent on a site node) and
|
||
writes a best-effort `deployment`/`Error` site event so the evidence outlives the boot log. Central
|
||
renders it as a *No self-heal* warning badge. See
|
||
[Component-HealthMonitoring.md](Component-HealthMonitoring.md) → *Startup Reconciler Presence*.
|
||
|
||
### Standby health-check log noise
|
||
|
||
`LoggerConfigurationFactory` installs one Serilog exclusion filter, for the by-design standby
|
||
verdict of the `active-node` probe (register row 71).
|
||
|
||
`DefaultHealthCheckService` logs one line per check per probe and picks the level from the verdict —
|
||
Debug / Warning / **Error** for Healthy / Degraded / Unhealthy. Both active-node checks
|
||
(`SitePairActiveNodeHealthCheck` on a site, the shared `ActiveNodeHealthCheck` on central) answer
|
||
Unhealthy on the standby, because 503 is exactly the contract `/health/active` publishes to Traefik
|
||
and the family dashboard. A perfectly healthy standby therefore logged ERR on every probe — every
|
||
10 s on a site node, every 5 s on a central node behind Traefik's active-node probe — which floods
|
||
the log and trains operators to ignore ERR.
|
||
|
||
Serilog's `MinimumLevel.Override` can only *raise* a per-source floor and a `LogEvent`'s level is
|
||
immutable, so neither an override nor an enricher can rewrite the event; excluding it is the only
|
||
surgical option. The predicate matches on `Level == Error` **and** the health-check logger category
|
||
**and** `HealthCheckName == "active-node"`. Keying on the check name rather than the category is
|
||
what preserves the constraint: a failing `localdb`, `akka-cluster`, `database` or
|
||
`required-singletons` check still logs at Error, and the active-node check has no error path of its
|
||
own (the active node always answers Healthy), so the only event this can ever suppress is a standby
|
||
reporting that it is a standby. The verdict remains fully observable through the HTTP status of
|
||
`/health/active` itself, the readiness probe's cluster view, and the health dashboard.
|
||
|
||
---
|
||
|
||
## Secrets (2026-09-28)
|
||
|
||
The Host composes `ZB.MOM.WW.Secrets` (a `scadaproj` shared library, not a ScadaBridge component —
|
||
hence it has no row in the Component Registration Matrix below) via `SecretsRegistration.AddScadaBridgeSecrets`,
|
||
called on both roles from their respective composition-root `Configure` methods. Design:
|
||
`docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md` (see its *Implementation notes (as
|
||
built)* section for where the shipped code deviates from the original design).
|
||
|
||
- **Site role — always a local, undecorated store.** A site node's `ISecretStore`/`ISecretResolver`
|
||
is unconditionally the plain node-local SQLite store (`AddZbSecrets`), regardless of any
|
||
`Secrets:Store:Provider` value — a Site never reads that key. This is a hard invariant, not
|
||
just a default: `DeployedSecretsMirror` (Component-SiteRuntime.md) calls `ISecretStore.DeleteAsync`
|
||
directly on the assumption that a delete is node-local; a replication-decorated store would have
|
||
published that delete fleet-wide. Secrets reach a site only via Deploy Artifacts (see
|
||
Component-DeploymentManager.md → *Site secrets*) — there is no other channel.
|
||
- **Central role — `Secrets:Store:Provider`** (`Sqlite` default | `SqlServer`), read **only** on the
|
||
Central role:
|
||
- `Sqlite` (default): each central node keeps its own per-node SQLite secret store. On a
|
||
**clustered** central (≥ 2 seed nodes) this is a documented misconfiguration — `StartupValidator`
|
||
logs a boot-time Warning (same shape as the inbound-API-key-store precedent, register row 108),
|
||
because the two nodes would deploy different secret sets to sites depending on which is active.
|
||
- `SqlServer`: both central nodes read and write **one shared SQL Server store**
|
||
(`Secrets:SqlServer:ConnectionString`, a literal/env value — never a `${secret:}` reference,
|
||
the same bootstrap-circularity rule the inbound-API-key shared store follows) — the `/admin/secrets`
|
||
page edits it, and it is the store `SiteSecretsCollector` resolves from. The pre-host `${secret:}`
|
||
expander (the Layer-A pass that resolves `${secret:X}` tokens inside `appsettings` itself, before
|
||
the DI container is built) reads the **same** shared store when `Provider = SqlServer`, so a
|
||
central node's boot-time config tokens and its running `ISecretResolver` never disagree about
|
||
what a token resolves to.
|
||
- A connection string that is blank, or is itself a `${secret:}` reference (a bootstrap circularity),
|
||
fails fast naming the offending key.
|
||
- **Bootstrap secrets are never deployed.** The site's gRPC PSK (`SB-GRPC-PSK-{siteId}`), the LocalDb
|
||
replication key, and the secrets master key (KEK) are never data-connection-referenced and are
|
||
never shipped through Deploy Artifacts — a site needs them *before* it can receive a deploy at all,
|
||
and pushing a rotated one through the channel it authenticates would sever that channel. They stay
|
||
out-of-band: service environment variables, a hand-seeded store, or a master-key file, exactly as
|
||
before this feature. Every node — every central and every site — **must share the same KEK**;
|
||
see `docs/deployment/topology-guide.md` → *Secrets: shared store, per-site deploy, and the KEK*
|
||
for where that is (and is not) checked.
|
||
- **The gRPC secrets hub is retired in ScadaBridge (2026-09-28).** ScadaBridge no longer composes
|
||
`ZB.MOM.WW.Secrets.Replicator.Grpc` on either role, and the historical SQL-Server *replication*
|
||
mode (`Secrets:Replication:Mode=SqlServer` — distinct from `Secrets:Store:Provider=SqlServer`
|
||
above, which shares one store rather than replicating between two) is retired too. Setting
|
||
`Secrets:Replication:Enabled` to anything other than absent/empty/`false` now fails
|
||
`StartupValidator.Validate` outright, on **either** role, with a message pointing at Deploy
|
||
Artifacts (sites) and `Secrets:Store:Provider=SqlServer` (a clustered central). A leftover
|
||
`Secrets:Replication:Mode` or any `Secrets:GrpcHub:*` key (harmless once `Enabled` is unset, but
|
||
dead configuration) logs a startup Warning naming both sections so they can be cleaned up. The
|
||
library itself keeps the hub for other consumers; only ScadaBridge stops using it.
|
||
|
||
---
|
||
|
||
## Component Registration Matrix
|
||
|
||
| Component | Central | Site | DI (`AddXxx`) | Actors (`AddXxxActors`) | Endpoints (`MapXxx`) |
|
||
|---|---|---|---|---|---|
|
||
| ClusterInfrastructure | Yes | Yes | Yes | Yes | No |
|
||
| Communication | Yes | Yes | Yes | Yes | No |
|
||
| HealthMonitoring | Yes | Yes | Yes | Yes | No |
|
||
| ExternalSystemGateway | Yes | Yes | Yes | Yes | No |
|
||
| AuditLog | Yes | Yes | Yes | Yes | No |
|
||
| NotificationService | Yes | No | Yes | Yes | No |
|
||
| NotificationOutbox | Yes | No | Yes | Yes | No |
|
||
| SiteCallAudit | Yes | No | Yes | Yes | No |
|
||
| KpiHistory | Yes | No | Yes | Yes | No |
|
||
| Transport | Yes | No | Yes | No | No |
|
||
| TemplateEngine | Yes | No | Yes | Yes | No |
|
||
| DeploymentManager | Yes | No | Yes | Yes | No |
|
||
| Security | Yes | No | Yes | Yes | No |
|
||
| CentralUI | Yes | No | Yes | No | Yes |
|
||
| InboundAPI | Yes | No | Yes | No | Yes |
|
||
| ManagementService | Yes | No | Yes | Yes | No |
|
||
| SiteRuntime | No | Yes | Yes | Yes | No |
|
||
| DataConnectionLayer | No | Yes | Yes | Yes | No |
|
||
| StoreAndForward | No | Yes | Yes | Yes | No |
|
||
| SiteEventLogging | No | Yes | Yes | Yes | No |
|
||
| ConfigurationDatabase | Yes | No | Yes | No | No |
|
||
|
||
---
|
||
|
||
## Dependencies
|
||
|
||
- **All 19 component libraries**: The Host references every component project to call their extension methods (excludes CLI, which is a separate executable). Audit Log (#23) ships its central+site code in `ZB.MOM.WW.ScadaBridge.AuditLog`; the Host calls `AddAuditLog()` on both roles, M2+ will add `AddAuditLogActors()`.
|
||
- **Akka.Cluster.Tools**: ClusterSingleton (singleton manager/proxy); transitively pulls Akka.Cluster (SBR provider) and Akka.Remote. Its ClusterClient/ClusterClientReceptionist surface is no longer used — cross-cluster messaging is gRPC as of Phase 4. The actor system is built from hand-assembled HOCON (REQ-HOST-6), so the `Akka.Hosting`/`Akka.Remote.Hosting`/`Akka.Cluster.Hosting` typed-builder packages are **not** referenced (dropped in arch-review 01 Task 22). No Akka.Persistence plugin — see the Persistence note under REQ-HOST-6.
|
||
- **Serilog.AspNetCore**: For structured logging integration.
|
||
- **Microsoft.Extensions.Hosting.WindowsServices**: For Windows Service support.
|
||
- **ASP.NET Core** (central only): For web endpoint hosting.
|
||
|
||
## Interactions
|
||
|
||
- **All components**: The Host is the composition root — it wires every component into the DI container and actor system.
|
||
- **Configuration Database**: The Host registers the DbContext and wires repository implementations to their interfaces. In development, triggers auto-migration; in production, validates schema version.
|
||
- **ClusterInfrastructure**: The Host configures the underlying Akka.NET cluster that ClusterInfrastructure manages at runtime.
|
||
- **CentralUI / InboundAPI**: The Host maps their web endpoints into the ASP.NET Core pipeline on central nodes.
|
||
- **ManagementService**: The Host registers the ManagementActor on central nodes; the CLI reaches it over the HTTP `/management` endpoints (in-process via `ManagementActorHolder`), not via any cluster transport. No ClusterClientReceptionist is configured (removed in Phase 4).
|
||
- **HealthMonitoring**: The Host's startup validation and logging configuration provide the foundation for health reporting.
|