9bb237b794
Resolve secret:-prefixed Password + UserCertificatePassword through the shared ISecretResolver, fail-closed on absent, retiring the cleartext-in-DB path. The driver-registry factory is synchronous (Func<string,string,IDriver>), so resolution is done lazily in the async session-open (InitializeAsync, before BuildUserIdentity) rather than at deserialize — mirroring Task 7's Galaxy pattern and matching its re-resolve-on-reconnect behavior. Both consumers (username Password and the certificate-password LoadPkcs12 path via BuildCertificateIdentity) see the resolved connect-scoped options; _options stays raw (secret: refs intact), no long-lived plaintext field. Scope corrections vs the plan (verified against v3): the probe is unauthenticated GetEndpoints-only and never reads either credential, so it is NOT a resolution site (comment added, no dead code); OpcUaClientDriverOptions was a sealed class, converted to sealed record for the with-expression (no positional params → identical JSON; no reference-equality/dict-key/ToString-log usages → no behavior/leak change). ISecretResolver threaded via factory Register/CreateInstance + DriverFactoryBootstrap (real resolver from DI); NullSecretResolver null-object backs test/parse paths only, fail-closed on secret: refs. TDD: 4 helper tests RED→GREEN; 142 OpcUaClient tests pass.
152 lines
9.2 KiB
C#
152 lines
9.2 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
|
|
|
// Probe type aliases — keep the using list concise.
|
|
using ModbusProbe = Driver.Modbus.ModbusDriverProbe;
|
|
using AbCipProbe = Driver.AbCip.AbCipDriverProbe;
|
|
using AbLegacyProbe = Driver.AbLegacy.AbLegacyDriverProbe;
|
|
using S7Probe = Driver.S7.S7DriverProbe;
|
|
using TwinCATProbe = Driver.TwinCAT.TwinCATDriverProbe;
|
|
using FocasProbe = Driver.FOCAS.FocasDriverProbe;
|
|
using OpcUaProbe = Driver.OpcUaClient.OpcUaClientDriverProbe;
|
|
using GalaxyProbe = Driver.Galaxy.GalaxyDriverProbe;
|
|
using CalculationProbe = Driver.Calculation.CalculationDriverProbe;
|
|
|
|
/// <summary>
|
|
/// Wires every cross-platform driver assembly's <c>Register(registry, loggerFactory)</c>
|
|
/// extension into a single <see cref="DriverFactoryRegistry"/> singleton and binds the
|
|
/// v2 <see cref="IDriverFactory"/> abstraction to a <see cref="DriverFactoryRegistryAdapter"/>
|
|
/// over it. Replaces the F7 seam's <c>NullDriverFactory</c> default so deploys actually
|
|
/// materialise real <see cref="IDriver"/> instances on driver-role nodes.
|
|
///
|
|
/// The factory registry is skipped on admin-only nodes — they never run drivers, so it doesn't
|
|
/// need to exist (Program.cs guards via the <c>hasDriver</c> flag). The driver <em>probe</em>
|
|
/// set is the exception: it backs the AdminUI Test Connect button and so must also be wired on
|
|
/// admin nodes — see <see cref="AddOtOpcUaDriverProbes"/>.
|
|
/// </summary>
|
|
public static class DriverFactoryBootstrap
|
|
{
|
|
/// <summary>
|
|
/// Register the cross-platform driver factories + bind <see cref="IDriverFactory"/>.
|
|
/// Must be called BEFORE <c>services.AddAkka</c> so the runtime extension can resolve
|
|
/// <see cref="IDriverFactory"/> from DI when spawning <c>DriverHostActor</c>.
|
|
/// </summary>
|
|
/// <param name="services">The service collection to register driver factories with.</param>
|
|
/// <returns>The same service collection, for chaining.</returns>
|
|
public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services)
|
|
{
|
|
services.AddSingleton<DriverFactoryRegistry>(sp =>
|
|
{
|
|
var registry = new DriverFactoryRegistry();
|
|
var loggerFactory = sp.GetService<ILoggerFactory>();
|
|
// The calc driver needs the root script logger so a script failure fans out onto the
|
|
// script-logs topic; resolve it here (null on nodes without the script pipeline wired).
|
|
var scriptRoot = sp.GetService<ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger>();
|
|
// The Galaxy driver resolves its Gateway.ApiKeySecretRef secret: arm through the
|
|
// shared ISecretResolver (registered unconditionally by AddZbSecrets on every node).
|
|
var secretResolver = sp.GetRequiredService<ISecretResolver>();
|
|
Register(registry, loggerFactory, secretResolver, scriptRoot);
|
|
return registry;
|
|
});
|
|
services.AddSingleton<IDriverFactory>(sp =>
|
|
new DriverFactoryRegistryAdapter(sp.GetRequiredService<DriverFactoryRegistry>()));
|
|
|
|
// Phase 6.1 resilience infrastructure — the process-singleton tracker + Polly pipeline builder,
|
|
// and the per-driver-instance invoker factory the DriverHostActor uses to wrap every capability
|
|
// call. Registered here (driver nodes) because the factory needs DriverFactoryRegistry.GetTier;
|
|
// admin-only nodes skip this and the runtime falls back to the pass-through factory.
|
|
services.AddSingleton<DriverResilienceStatusTracker>();
|
|
services.AddSingleton<DriverResiliencePipelineBuilder>(sp =>
|
|
new DriverResiliencePipelineBuilder(
|
|
timeProvider: null, // TimeProvider.System
|
|
statusTracker: sp.GetRequiredService<DriverResilienceStatusTracker>(),
|
|
logger: sp.GetService<ILoggerFactory>()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverResiliencePipelineBuilder")));
|
|
services.AddSingleton<IDriverCapabilityInvokerFactory>(sp =>
|
|
{
|
|
var registry = sp.GetRequiredService<DriverFactoryRegistry>();
|
|
return new DriverCapabilityInvokerFactory(
|
|
sp.GetRequiredService<DriverResiliencePipelineBuilder>(),
|
|
sp.GetRequiredService<DriverResilienceStatusTracker>(),
|
|
registry.GetTier,
|
|
logger: sp.GetService<ILoggerFactory>()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverCapabilityInvokerFactory"));
|
|
});
|
|
|
|
// The tracker's operator-facing reader: a periodic DPS publisher that mirrors each snapshot to the
|
|
// AdminUI resilience panel (R2-10 — closes the "tracker fed by everything, read by nothing" gap).
|
|
// Lives on driver nodes exactly where the tracker exists. The lazy Func<ActorSystem> is idempotent
|
|
// with the one Program.cs registers for the alarm-command router.
|
|
services.TryAddSingleton<Func<Akka.Actor.ActorSystem>>(sp => () => sp.GetRequiredService<Akka.Actor.ActorSystem>());
|
|
services.AddHostedService<DriverResilienceStatusPublisherService>();
|
|
|
|
// Driver nodes also carry the probe set so a fused admin,driver node has it; the admin-only
|
|
// case is covered by Program.cs calling AddOtOpcUaDriverProbes() in the hasAdmin block.
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register one <see cref="IDriverProbe"/> per driver type. These back the AdminUI's
|
|
/// "Test Connect" button: the <c>admin-operations</c> cluster singleton resolves
|
|
/// <see cref="IEnumerable{T}"/> of <see cref="IDriverProbe"/> and dispatches by DriverType.
|
|
/// <para>
|
|
/// That singleton is role-pinned to <c>admin</c>, so this MUST be wired on admin nodes —
|
|
/// including admin-only nodes that lack the <c>driver</c> role (e.g. the MAIN cluster's
|
|
/// admin-a/admin-b). Probes are lightweight (cheap connect, no persistent state) and don't
|
|
/// need the driver-factory registry, so they register independently of
|
|
/// <see cref="AddOtOpcUaDriverFactories"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// Uses <c>TryAddEnumerable</c> so a fused admin,driver node — which reaches this from both
|
|
/// the driver-factory path and the admin path — registers each probe exactly once. A
|
|
/// duplicate would make the singleton's <c>ToDictionary(p => p.DriverType)</c> throw.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="services">The service collection to register driver probes with.</param>
|
|
/// <returns>The same service collection, for chaining.</returns>
|
|
public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services)
|
|
{
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, ModbusProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbCipProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbLegacyProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, S7Probe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, TwinCATProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, FocasProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, OpcUaProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, GalaxyProbe>());
|
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, CalculationProbe>());
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoke every cross-platform driver's <c>Register</c> extension. New driver assemblies
|
|
/// get added here — one line per type. ShouldStub() in <c>DriverInstanceActor</c> still
|
|
/// handles platform/role-dependent stubbing (e.g. Galaxy on macOS), so registering a
|
|
/// factory here doesn't mean it always runs in production.
|
|
/// </summary>
|
|
private static void Register(
|
|
DriverFactoryRegistry registry,
|
|
ILoggerFactory? loggerFactory,
|
|
ISecretResolver secretResolver,
|
|
ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger? scriptRoot = null)
|
|
{
|
|
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry);
|
|
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
|
|
Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot);
|
|
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
|
|
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory);
|
|
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
|
|
Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory, secretResolver);
|
|
Driver.S7.S7DriverFactoryExtensions.Register(registry);
|
|
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
|
|
}
|
|
}
|