d32d89c340
Five drivers ignored the config handed to them on reinitialize, serving the
options their constructor captured — and the deployment sealed green anyway.
Fixed on both halves, as chosen.
Seam (load-bearing): DriverSpawnPlanner routes a changed DriverConfig to
ToStop + ToSpawn instead of an in-place delta, making the factory the single
parse authority. This REVERSES the deliberate decision documented at
DriverSpawnPlan.cs:49-50 ("a pure DriverConfig change stays an in-place delta
— no reconnect"). That reasoning was correct about the resilience pipeline and
wrong about the driver. The accepted price is a reconnect per config edit.
Per-driver, and this split was not anticipated:
- Re-parse in place — Modbus, AbLegacy, OpcUaClient. ParseOptions extracted
from each factory, called from InitializeAsync behind a HasConfigBody guard
so "{}" still keeps the constructor options.
- Respawn-only, deliberately NOT re-parsed — Sql and FOCAS. Each builds more
than options from config (Sql's dialect + resolved connection string,
FOCAS's client-factory backend, both injected at construction), so adopting
new options alone would run a NEW tag set against an OLD connection. Half a
re-parse is worse than none. SqlDriver's doc-comment asserted the opposite
premise — "config parsing belongs to the factory, which builds a fresh
instance" — which was false when written and is true only now.
Two green seals removed from ApplyChildDelta: it overwrote the cached Spec
synchronously BEFORE the child dequeued the message, so the host believed the
new config was live and the next reconcile computed no delta, sealing the
drift permanently; and it Tell'd with no Receive<ApplyResult> registered, so a
failed reinit — including Galaxy's deliberate NotSupportedException —
dead-lettered.
Exposed (not created) by the change: a factory throw is a CONFIG error, and
SpawnChild catches it and silently substitutes a stub. Only a brand-new driver
could reach that before; an ordinary config edit can now. Raised Warning ->
Error with an actionable message. It still does not fail the deployment —
doing so would let one malformed driver block a fleet deploy, so that is a
follow-up rather than a drive-by.
Tests: every pre-existing reinit test in these suites passes "{}", the exact
input a guarded re-parser treats as "keep constructor options" — blind to this
defect by construction. New tests pass CHANGED json; the Modbus one was
verified falsifiable by deleting the re-parse (goes red). Two tests asserted
the old behaviour and were rewritten, including the positive control in
DriverHostActorUnreadableArtifactTests, whose observable moved from
UnsubscribeAsync to ShutdownAsync now that teardown happens by stopping the
child rather than emptying its desired set.
Remaining full-suite failures are pre-existing and environmental, verified
identical on the pre-change tree: Host.IntegrationTests (3) and
Driver.AbLegacy.IntegrationTests (4, docker fixture-gated).
94 lines
5.4 KiB
C#
94 lines
5.4 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);
|
|
|
|
return JsonSerializer.Deserialize<OpcUaClientDriverOptions>(driverConfigJson, JsonOptions)
|
|
?? throw new InvalidOperationException(
|
|
$"OpcUaClient driver config for '{driverInstanceId}' deserialised to null");
|
|
}
|
|
}
|