fix(heidenhain): resolve the probe's secret resolver lazily — eager resolution crash-looped central nodes at boot (#803)
v2-ci / unit-tests (push) Successful in 1h9m7s

The Test-Connect probe set is resolved inside the AdminOperations Akka starter; resolving
ISecretResolver there walks AddZbSecrets -> IAuditWriter -> ActorAuditWriter, whose ctor needs
AuditWriterActorKey (registered later in the same builder). Found by the docker-dev live gate.
Regression test: resolving the probe set never resolves ISecretResolver (fails before the fix).
Also: TagRowProblem doc comment re-attached; Heidenhain credential help text names the
secret:heidenhain/ and env:OTOPCUA_HEIDENHAIN_ namespaces.
This commit is contained in:
Joseph Doherty
2026-11-11 00:27:37 -05:00
parent 7af9c40e41
commit 5bbe1e3c7a
8 changed files with 60 additions and 17 deletions
@@ -65,7 +65,7 @@ public sealed class HeidenhainDeviceOptions
/// <summary>
/// Access settings. Password fields are <b>references</b>, never literals: blank (no password),
/// <c>secret:&lt;name&gt;</c> (the node's encrypted secret store) or <c>env:&lt;VARIABLE&gt;</c>. A literal is
/// <c>secret:heidenhain/&lt;name&gt;</c> (the node's encrypted secret store) or <c>env:OTOPCUA_HEIDENHAIN_&lt;NAME&gt;</c>. A literal is
/// refused at parse and at the deploy gate, so a controller password never lands in the config database.
/// </summary>
public sealed class HeidenhainAccessOptions
@@ -13,9 +13,25 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Heidenhain;
/// <remarks>
/// Registered on admin nodes as well as driver nodes (the Test-Connect singleton is admin-pinned). The
/// resolver is optional: without one a <c>secret:</c> INSPECT password fails closed with a clear message.
/// <para>
/// The host registers the probe with a resolver <em>accessor</em>, never a resolved instance: the probe set is
/// resolved inside the AdminOperations Akka starter, and resolving <see cref="ISecretResolver" /> there walks
/// <c>AddZbSecrets</c> → <c>IAuditWriter</c> → <c>ActorAuditWriter</c>, whose constructor needs the
/// <c>AuditWriterActorKey</c> registered later in the same builder — every central node failed to boot (#803
/// live gate). The accessor runs per probe call, after the host is built.
/// </para>
/// </remarks>
public sealed class HeidenhainDriverProbe(ISecretResolver? secretResolver = null) : IDriverProbe
public sealed class HeidenhainDriverProbe : IDriverProbe
{
private readonly Func<ISecretResolver?> _secretResolver;
/// <summary>Creates a probe with a fixed (optional) resolver — for tests and graphs without the Akka audit writer.</summary>
public HeidenhainDriverProbe(ISecretResolver? secretResolver = null) => _secretResolver = () => secretResolver;
/// <summary>Creates a probe that resolves the secret resolver lazily, once per probe call.</summary>
public HeidenhainDriverProbe(Func<ISecretResolver?> secretResolverAccessor) =>
_secretResolver = secretResolverAccessor ?? throw new ArgumentNullException(nameof(secretResolverAccessor));
/// <inheritdoc />
public string DriverType => DriverTypeNames.Heidenhain;
@@ -42,7 +58,7 @@ public sealed class HeidenhainDriverProbe(ISecretResolver? secretResolver = null
string password;
// Resolved only through the namespaced resolver (fail-closed at resolve time): Test Connect can never be
// used to send another subsystem's secret or environment variable to an arbitrary Host.
try { password = await new HeidenhainCredentials(secretResolver).ResolveAsync(options, device, "INSPECT", cts.Token).ConfigureAwait(false); }
try { password = await new HeidenhainCredentials(_secretResolver()).ResolveAsync(options, device, "INSPECT", cts.Token).ConfigureAwait(false); }
catch (HeidenhainCredentialException ex) { return new(false, ex.Message, null); }
try { _ = Lsv2ReadOnlyPolicy.LoginPayload("INSPECT", password); }