feat(secrets): central's Grpc-mode store is the SHARED SQL-Server store (scadaproj#4)
In Secrets:Replication:Mode=Grpc a central node's ISecretStore is now the
shared SQL-Server store (AddZbSecretsSqlServerStore) instead of a per-node
local SQLite store. Both central hubs read and write ONE copy of every row,
so they serve identical manifests by construction — the 2026-08-07 live gate
observed central-b answering an authenticated GetManifest with an EMPTY
manifest while central-a held every secret, which would turn site-side hub
failover into a silent convergence stop.
- SecretsRegistration: two fail-closed pre-checks before any registration on
the central+Grpc path — a blank Secrets:SqlServer:ConnectionString throws
naming the key (an independent store per central node is the recorded
defect), and a value containing ${secret: throws naming the bootstrap
circularity (the expander needs this store to resolve references). Site
registrations are byte-identical to before; SqlServer mode and
replication-off are untouched.
- Program.cs Layer-A expander follows the store swap: central+Grpc with a
non-blank connection string migrates and resolves pre-host ${secret:}
references through the shared SQL store, so expanded values can never
diverge from what the running node serves. Every other case keeps the
SQLite path unchanged; blank-connstr central deliberately falls through so
the clear AddScadaBridgeSecrets message is the one that fails the boot.
- appsettings.json: Secrets:SqlServer _comment now documents the Grpc-mode
central requirement (literal/env value only, sites leave it empty).
- SecretsReplicationWiringTests: +5 pins (shared store resolves, blank and
${secret:} connstrings fail naming the key, sites-have-no-SqlServer-types
descriptor sweep), central fixtures carry the now-required connstr.
Full suite green (7,474 passed across 30 projects, 0 warnings); the two
failures are pre-existing and unrelated: the Playwright live-rig suite fails
identically on unmodified main (cluster not running), and
GrpcCentralTransportTests.DeadlineExceeded_IsNotRetriedOnThePeer is a timing
flake that passes 3/3 in isolation and 470/470 on the first run of this code.
Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
@@ -32,6 +32,8 @@ using ZB.MOM.WW.ScadaBridge.Transport;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
using ZB.MOM.WW.Secrets.Configuration;
|
||||
using ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
using ZB.MOM.WW.Secrets.Replicator.SqlServer;
|
||||
using ZB.MOM.WW.Secrets.Replicator.SqlServer.DependencyInjection;
|
||||
using ZB.MOM.WW.Secrets.Sqlite;
|
||||
using ZB.MOM.WW.Secrets.Ui;
|
||||
using ZB.MOM.WW.Telemetry;
|
||||
@@ -52,13 +54,46 @@ var configuration = new ConfigurationBuilder()
|
||||
|
||||
// Expand ${secret:...} config references before any validator/binder sees them (Layer A).
|
||||
// Throwaway provider — disposed here, shares no singletons with the host container.
|
||||
//
|
||||
// The expander must read the SAME store the running node will serve. On a central node in Grpc
|
||||
// replication mode that store is the SHARED SQL-Server store (see SecretsRegistration,
|
||||
// scadaproj#4) — an expander left on SQLite there would resolve pre-host ${secret:} references
|
||||
// from a stale/empty local store, silently diverging from what the node's own hub serves. Every
|
||||
// other case (sites, SqlServer mode, replication off) keeps the local SQLite path exactly as it
|
||||
// always was. Central+Grpc with a BLANK connection string deliberately falls through to the
|
||||
// SQLite path too: that boot is about to fail in AddScadaBridgeSecrets with the message naming
|
||||
// Secrets:SqlServer:ConnectionString, so the throw is not duplicated here.
|
||||
var expanderUsesSharedSqlStore =
|
||||
string.Equals(
|
||||
configuration["ScadaBridge:Node:Role"], "Central", StringComparison.OrdinalIgnoreCase)
|
||||
&& SecretsRegistration.UsesGrpcHub(configuration)
|
||||
&& !string.IsNullOrWhiteSpace(configuration[SecretsRegistration.HubConnectionStringKey]);
|
||||
|
||||
var expanderServices = new ServiceCollection();
|
||||
if (expanderUsesSharedSqlStore)
|
||||
{
|
||||
expanderServices.AddZbSecretsSqlServerStore(configuration, "Secrets");
|
||||
}
|
||||
else
|
||||
{
|
||||
expanderServices.AddZbSecrets(configuration, "Secrets");
|
||||
}
|
||||
|
||||
#pragma warning disable ASP0000 // deliberate throwaway container
|
||||
await using (var secretsProvider = new ServiceCollection()
|
||||
.AddZbSecrets(configuration, "Secrets")
|
||||
.BuildServiceProvider())
|
||||
await using (var secretsProvider = expanderServices.BuildServiceProvider())
|
||||
#pragma warning restore ASP0000
|
||||
{
|
||||
await secretsProvider.GetRequiredService<SqliteSecretsStoreMigrator>().MigrateAsync(default);
|
||||
if (expanderUsesSharedSqlStore)
|
||||
{
|
||||
await secretsProvider.GetRequiredService<SqlServerSecretsStoreMigrator>()
|
||||
.MigrateAsync(default);
|
||||
}
|
||||
else
|
||||
{
|
||||
await secretsProvider.GetRequiredService<SqliteSecretsStoreMigrator>()
|
||||
.MigrateAsync(default);
|
||||
}
|
||||
|
||||
var resolver = secretsProvider.GetRequiredService<ISecretResolver>();
|
||||
await new SecretReferenceExpander(resolver)
|
||||
.ExpandConfigurationAsync(configuration, default);
|
||||
|
||||
Reference in New Issue
Block a user