1219b7b13e
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
104 lines
5.9 KiB
C#
104 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
|
|
|
/// <summary>
|
|
/// Registers the OPC UA Client driver with the <see cref="DriverFactoryRegistry"/>. The driver's
|
|
/// <c>DriverConfig</c> JSON deserialises directly into <see cref="OpcUaClientDriverOptions"/>
|
|
/// (the same shape <see cref="OpcUaClientDriverProbe"/> reads), so no separate DTO is needed —
|
|
/// unlike the protocol drivers, this driver has no pre-declared "Tags" DTO path to retire.
|
|
/// v3: the deploy artifact's authored raw tags bind straight onto
|
|
/// <see cref="OpcUaClientDriverOptions.RawTags"/> (a <c>RawTagEntry</c> list) through the same
|
|
/// deserialiser; the <c>EndpointUrl</c> binding is unchanged (endpoint→DeviceConfig is Wave C).
|
|
/// Mirrors <c>ModbusDriverFactoryExtensions</c> / <c>GalaxyDriverFactoryExtensions</c>.
|
|
/// </summary>
|
|
public static class OpcUaClientDriverFactoryExtensions
|
|
{
|
|
/// <summary>Driver type name — matches <c>DriverInstance.DriverType</c> values.</summary>
|
|
public const string DriverTypeName = "OpcUaClient";
|
|
|
|
// Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
|
|
// The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
|
|
// AuthType / TargetNamespaceKind) be authored as their string names — the natural form
|
|
// for human-edited + AdminUI-emitted DriverConfig JSON.
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
|
Converters = { new JsonStringEnumConverter() },
|
|
};
|
|
|
|
/// <summary>Register the OpcUaClient factory with the driver registry, threading the shared secret resolver.</summary>
|
|
/// <param name="registry">The driver factory registry to register with.</param>
|
|
/// <param name="loggerFactory">Optional logger factory used to create per-instance loggers.</param>
|
|
/// <param name="secretResolver">
|
|
/// The shared secret resolver (from DI) used to resolve the <c>secret:</c> arm of each
|
|
/// instance's <c>Password</c> / <c>UserCertificatePassword</c> at session-open. Defaults to
|
|
/// the <see cref="NullSecretResolver"/> null-object for the test/standalone path — callers
|
|
/// that need a working <c>secret:</c> credential ref must thread the DI resolver.
|
|
/// </param>
|
|
public static void Register(
|
|
DriverFactoryRegistry registry,
|
|
ILoggerFactory? loggerFactory = null,
|
|
ISecretResolver? secretResolver = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
var resolver = secretResolver ?? NullSecretResolver.Instance;
|
|
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, resolver));
|
|
}
|
|
|
|
/// <summary>Public for the Server-side bootstrapper + test consumers.</summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The JSON configuration string for the driver.</param>
|
|
/// <param name="loggerFactory">Optional logger factory for the per-instance logger.</param>
|
|
/// <param name="secretResolver">
|
|
/// Optional shared secret resolver for the driver's <c>secret:</c> credential arm; defaults
|
|
/// to the <see cref="NullSecretResolver"/> null-object (fail-closed on a <c>secret:</c> ref).
|
|
/// </param>
|
|
/// <returns>A configured <see cref="OpcUaClientDriver"/>.</returns>
|
|
public static OpcUaClientDriver CreateInstance(
|
|
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null,
|
|
ISecretResolver? secretResolver = null)
|
|
{
|
|
return new OpcUaClientDriver(
|
|
ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId,
|
|
loggerFactory?.CreateLogger<OpcUaClientDriver>(),
|
|
secretResolver ?? NullSecretResolver.Instance);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an OpcUaClient <c>DriverConfig</c> JSON document into typed options. Extracted so
|
|
/// <see cref="OpcUaClientDriver.InitializeAsync"/> can re-parse a CHANGED config on reinitialize
|
|
/// (Gitea #516) instead of serving the options it was constructed with. Note the driver's own
|
|
/// doc-comment previously claimed "resolving on every InitializeAsync picks up rotations" — that was
|
|
/// true of SECRET rotation only; the endpoint, security policy and tag set were all frozen at
|
|
/// construction.
|
|
/// </summary>
|
|
/// <param name="driverInstanceId">The unique driver instance identifier.</param>
|
|
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
|
|
/// <returns>The parsed <see cref="OpcUaClientDriverOptions"/>.</returns>
|
|
internal static OpcUaClientDriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
|
|
|
var options = JsonSerializer.Deserialize<OpcUaClientDriverOptions>(driverConfigJson, JsonOptions)
|
|
?? throw new InvalidOperationException(
|
|
$"OpcUaClient driver config for '{driverInstanceId}' deserialised to null");
|
|
|
|
// #273/#278: nothing runs DataAnnotations on driver options, so range-check here — at options load —
|
|
// and fail with a clear message instead of a driver that faults and retries forever.
|
|
try { OpcUaClientDriver.ValidateTuning(options); }
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"OpcUaClient driver config for '{driverInstanceId}' is invalid: {ex.Message}", ex);
|
|
}
|
|
return options;
|
|
}
|
|
}
|