using Akka.Actor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Replication; using ZB.MOM.WW.Secrets.DependencyInjection; using ZB.MOM.WW.Secrets.Sqlite; namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection; /// /// Dependency-injection entry point for peer-to-peer secret replication over an Akka.NET cluster. /// public static class AkkaSecretsServiceCollectionExtensions { /// /// Registers secret replication over the cluster: each node keeps its own local store, local /// writes broadcast to peers, and a periodic manifest exchange repairs any divergence in both /// directions. /// /// /// /// Choose this transport when a node must keep resolving secrets while partitioned from /// the rest of the cluster — reads never leave the node, so an isolated site keeps running on its /// last-known-good state and re-converges when the partition heals. If every node can always /// reach a shared database, the SQL-Server package's shared-store mode is simpler and has no /// distributed failure modes at all; prefer it unless partition tolerance is a stated requirement. /// /// /// Requirements. The application must (1) register its in DI /// before the first secret resolve, and (2) give every node the same KEK — rows carry /// ciphertext only, so a node with a different master key fails closed on a kek_id /// mismatch. Merging into the actor /// system's configuration is recommended. /// /// /// KEK rotation does not replicate. A re-wrap deliberately leaves the revision untouched /// so it stays invisible to last-writer-wins; run rewrap-all against every node's local /// store. /// /// /// The service collection to add to. /// The application configuration. /// Configuration section holding the core secrets options. /// Configuration section holding the replication options. /// /// The application content root, forwarded to AddZbSecrets so the store-path guard can /// reject a Secrets:SqlitePath inside it. Pass it from every pre-host composition (a /// container with no IHostEnvironment, such as a ${secret:} expander's), exactly as /// for the 4-argument AddZbSecrets. The last explicitly supplied root wins across calls. /// /// The same instance, for chaining. public static IServiceCollection AddZbSecretsAkkaReplication( this IServiceCollection services, IConfiguration config, string secretsSectionPath = "Secrets", string replicationSectionPath = "Secrets:Replication", string? contentRootPath = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(config); ArgumentException.ThrowIfNullOrWhiteSpace(secretsSectionPath); ArgumentException.ThrowIfNullOrWhiteSpace(replicationSectionPath); IConfigurationSection section = config.GetSection(replicationSectionPath); services.Configure(section); // Validate eagerly so a bad interval fails at startup, not on the first announce tick. var eager = new AkkaSecretsReplicationOptions(); section.Bind(eager); eager.Validate(); // The actor is created lazily on first use rather than at registration: the ActorSystem is // typically registered by the application AFTER this call, and a cluster node may also not be // ready to join at the moment the container is built. // // The invalidator MUST stay deferred (scadaproj#1): it is the resolver, the resolver reads // ISecretStore, and ISecretStore is the ReplicatingSecretStore decorator registered below — // resolving it here closes a singleton cycle the container cannot detect through factory // lambdas, and every hosted process then deadlocks at startup inside the container's // resolution locks. DeferredSecretCacheInvalidator waits until the first eviction, which // can only happen after the graph has finished resolving. services.TryAddSingleton(sp => new SecretReplicationActorProvider( sp.GetRequiredService(), sp.GetRequiredService(), new DeferredSecretCacheInvalidator(sp.GetService), sp.GetRequiredService>().Value)); // The local store + migrator come from AddZbSecrets (SQLite by default). services.AddZbSecrets(config, secretsSectionPath, contentRootPath); // REPLACED after the core call rather than TryAdd-ed before it (scadaproj#30). AddZbSecrets // does TryAddSingleton(); a TryAdd here only won when // this package ran first, so an application that had already called AddZbSecrets got // replication bound to the no-op sink — publishing nothing, no actor ever created, no error // (the 0.2.0 shape, and still reachable from the application's side until 0.6.6). services.RemoveAll(); services.AddSingleton(sp => new AkkaSecretReplicator(sp.GetRequiredService().ActorRef)); // Decorate the local store so every write publishes. Resolved from the concrete SQLite store, // not ISecretStore — asking the container for ISecretStore here would resolve this very // decorator and recurse forever. The core's ISecretStore forward is removed first so the // decorator is the only registration in either call order. services.RemoveAll(); services.AddSingleton(sp => new ReplicatingSecretStore( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); return services; } } /// /// Creates this node's on first access and hands out its ref. /// /// /// The indirection exists because the actor must be created lazily — the application usually /// registers its after AddZbSecretsAkkaReplication, so eagerly /// resolving one at registration time would fail. Creation is guarded by /// with so /// concurrent first writes cannot spawn two actors on the same node. /// /// The application's actor system. /// The node's local secret store — the undecorated one. /// Resolver-cache seam. /// Replication options. public sealed class SecretReplicationActorProvider( ActorSystem system, ISecretStore store, ISecretCacheInvalidator? cacheInvalidator, AkkaSecretsReplicationOptions options) { private readonly Lazy _actorRef = new( () => system.ActorOf( SecretReplicationActor.Props(store, cacheInvalidator, options.AnnounceInterval), options.ActorName), LazyThreadSafetyMode.ExecutionAndPublication); /// This node's replication actor, created on first access. public IActorRef ActorRef => _actorRef.Value; }