3336ec08c7
Routes the host's secrets registration through a new AddOtOpcUaSecrets extension that gates the ISecretStore implementation on Secrets:Replication:Enabled. Opt-in gate (default FALSE) This call decides which ISecretStore every node resolves — including driver-role nodes with no auth/AdminUI, where a wrong store surfaces as drivers failing to open sessions rather than as a failing test. With the flag false the wiring is the pre-existing AddZbSecrets(config, "Secrets") call, unchanged, so current behavior is byte-identical. With it true, AddZbSecretsAkkaReplication replaces that call (it invokes AddZbSecrets internally; calling both would double-register). Extracted to a named extension specifically so the registration is testable: Program.cs is top-level statements and cannot be exercised by a container test, which is how a "registered but never resolvable" defect ships unnoticed. Serializer HOCON AkkaSecretsReplication.SerializationConfig is merged into the ActorSystem config inside the AddAkka configurator, conditionally on the same gate — a non-replicating node carries no bindings for messages it will never see. Merged via AddHocon(..., HoconAddMode.Append), Akka.Hosting's fallback merge and the same mode the existing base-config merge uses; a raw Config.WithFallback would fight the builder's own assembly. Lazy-actor mitigation The replication actor is created lazily on first ISecretStore resolution, so a node that never touches a secret would never announce a manifest and would silently never converge. SecretReplicationStarter (IHostedService) resolves the store once at startup to make participation unconditional. KNOWN BLOCKER — replication is currently NON-FUNCTIONAL; do not enable ZB.MOM.WW.Secrets.Replicator.AkkaDotNet 0.2.0 never binds its own ISecretReplicator. AddZbSecretsAkkaReplication calls AddZbSecrets FIRST, which does TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>(); the package's own TryAddSingleton<ISecretReplicator>(AkkaSecretReplicator) that follows is therefore a no-op. Verified empirically in a built container: with Enabled=true, ISecretReplicator resolves to NoOpSecretReplicator, so ReplicatingSecretStore publishes into a sink and no actor is ever spawned. Consequence: the startup hook cannot create the actor, and the test asserting it does is committed Skipped with the evidence. Not worked around here — the fix belongs upstream (AddSingleton, or register before calling AddZbSecrets). Because the flag defaults false, this commit is inert in production. Tests: SecretsReplicationRegistrationTests (new) — disabled path resolves plain SqliteSecretStore and needs no ActorSystem; enabled path resolves ReplicatingSecretStore AND the undecorated concrete SqliteSecretStore the decorator is built from (the exact registration gap that shipped once); startup hook registered only when enabled. Red before wiring (4 assertion failures), green after: 6 pass, 1 skipped (blocker above). Build: 861 warnings / 0 errors, unchanged from baseline (full --no-incremental A/B). Host.IntegrationTests: 123 pass, 6 skip, 1 fail — AbCip_Green_AgainstSim, verified pre-existing on the stashed tree (fixture-gated). Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
83 lines
4.0 KiB
C#
83 lines
4.0 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using ZB.MOM.WW.Secrets.DependencyInjection;
|
|
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
|
|
/// <summary>
|
|
/// Single registration point for the secrets subsystem on every OtOpcUa node, admin- and
|
|
/// driver-role alike. Exists as a named extension rather than an inline <c>AddZbSecrets</c> call
|
|
/// in <c>Program.cs</c> so the choice of <c>ISecretStore</c> implementation is covered by a DI
|
|
/// resolution test — <c>Program.cs</c> is top-level statements and cannot be exercised directly,
|
|
/// which is exactly how a "registered but never resolvable" wiring defect ships unnoticed.
|
|
/// </summary>
|
|
public static class SecretsRegistration
|
|
{
|
|
/// <summary>Configuration key gating cluster secret replication. Absent or false = off.</summary>
|
|
public const string ReplicationEnabledKey = "Secrets:Replication:Enabled";
|
|
|
|
/// <summary>Configuration section holding <c>AkkaSecretsReplicationOptions</c>.</summary>
|
|
public const string ReplicationSectionPath = "Secrets:Replication";
|
|
|
|
/// <summary>Configuration section holding the core secrets options.</summary>
|
|
public const string SecretsSectionPath = "Secrets";
|
|
|
|
/// <summary>
|
|
/// True when cluster secret replication is switched on in configuration. Defaults to false —
|
|
/// absent configuration must mean "off".
|
|
/// </summary>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <returns><c>true</c> when replication is enabled.</returns>
|
|
public static bool IsReplicationEnabled(IConfiguration configuration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(configuration);
|
|
return configuration.GetValue(ReplicationEnabledKey, defaultValue: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the secrets subsystem. Replication is <b>opt-in</b>: unless
|
|
/// <see cref="ReplicationEnabledKey"/> is true this is exactly the plain local SQLite wiring
|
|
/// the host has always used.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The gate is deliberately default-deny. This call decides which <c>ISecretStore</c> the
|
|
/// container hands to <b>every</b> node, including driver-role nodes with no auth or
|
|
/// AdminUI surface, where a bad store surfaces as drivers failing to open sessions rather
|
|
/// than as a failing test. Enabling replication by default would change secret resolution
|
|
/// across a production fleet with nobody having asked for it.
|
|
/// </para>
|
|
/// <para>
|
|
/// <c>AddZbSecretsAkkaReplication</c> calls <c>AddZbSecrets</c> internally, so the two
|
|
/// branches are alternatives, never additive — calling both would double-register.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">The service collection to add to.</param>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
|
|
public static IServiceCollection AddOtOpcUaSecrets(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(configuration);
|
|
|
|
if (!IsReplicationEnabled(configuration))
|
|
{
|
|
services.AddZbSecrets(configuration, SecretsSectionPath);
|
|
return services;
|
|
}
|
|
|
|
services.AddZbSecretsAkkaReplication(configuration, SecretsSectionPath, ReplicationSectionPath);
|
|
|
|
// Anti-entropy participation must not hinge on this node happening to touch a secret — see
|
|
// SecretReplicationStarter for why the library's lazy actor creation is insufficient here.
|
|
services.AddSingleton<SecretReplicationStarter>();
|
|
services.AddHostedService(sp => sp.GetRequiredService<SecretReplicationStarter>());
|
|
|
|
return services;
|
|
}
|
|
}
|