fix(site-secrets): mirror secrets before pushing artifact data connections to the DCL
Final review M2: HandleDeployArtifacts now sends ApplyArtifactDataConnectionsToDcl after the shipped secrets are written and mirrored (from a finally, so the push still goes out if the secrets step fails), so a connection created by the same deploy resolves a newly shipped secret on first connect. Definitions are still stored at the same point; response semantics unchanged.
This commit is contained in:
@@ -3231,6 +3231,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
// M2 (final review): the DCL push for this deploy's data connections is captured
|
||||||
|
// here and sent only AFTER the shipped secrets are written and mirrored (in the
|
||||||
|
// finally below), so a connection whose config references a secret this same deploy
|
||||||
|
// ships resolves it on its FIRST connect instead of failing "not found" and waiting
|
||||||
|
// out the reconnect interval. The definitions are still stored in SQLite at the same
|
||||||
|
// point as before, and the push still goes out if the secrets step throws.
|
||||||
|
ApplyArtifactDataConnectionsToDcl? pendingDclApply = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Each non-null artifact list is the COMPLETE system-wide set
|
// Each non-null artifact list is the COMPLETE system-wide set
|
||||||
@@ -3328,8 +3335,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
// artifact bundle. Reconciled removals ride the same
|
// artifact bundle. Reconciled removals ride the same
|
||||||
// message so the live DCL connection actor is stopped
|
// message so the live DCL connection actor is stopped
|
||||||
// on the actor thread alongside the hash-cache eviction.
|
// on the actor thread alongside the hash-cache eviction.
|
||||||
self.Tell(new ApplyArtifactDataConnectionsToDcl(
|
// Sent after the secrets step — see pendingDclApply above (M2).
|
||||||
command.DataConnections, removedConnections));
|
pendingDclApply = new ApplyArtifactDataConnectionsToDcl(
|
||||||
|
command.DataConnections, removedConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMTP configuration is
|
// SMTP configuration is
|
||||||
@@ -3417,6 +3425,15 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
return new ArtifactDeploymentResponse(
|
return new ArtifactDeploymentResponse(
|
||||||
command.DeploymentId, "", false, ex.Message, DateTimeOffset.UtcNow);
|
command.DeploymentId, "", false, ex.Message, DateTimeOffset.UtcNow);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Runs before the Task completes, so the DCL push is still enqueued ahead of the
|
||||||
|
// PipeTo reply, as it was before the reorder.
|
||||||
|
if (pendingDclApply != null)
|
||||||
|
{
|
||||||
|
self.Tell(pendingDclApply);
|
||||||
|
}
|
||||||
|
}
|
||||||
}).PipeTo(sender);
|
}).PipeTo(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+58
-2
@@ -1,4 +1,5 @@
|
|||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
|
using Akka.TestKit;
|
||||||
using Akka.TestKit.Xunit2;
|
using Akka.TestKit.Xunit2;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
@@ -47,7 +48,7 @@ public class DeploymentManagerActorArtifactSecretsTests : TestKit, IDisposable
|
|||||||
TestLocalDb.DeleteFiles(path);
|
TestLocalDb.DeleteFiles(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IActorRef CreateDeploymentManager(DeployedSecretsMirror? secretsMirror = null)
|
private IActorRef CreateDeploymentManager(DeployedSecretsMirror? secretsMirror = null, IActorRef? dclManager = null)
|
||||||
{
|
{
|
||||||
var options = new SiteRuntimeOptions();
|
var options = new SiteRuntimeOptions();
|
||||||
// Positional, not named, past the constructor's first six parameters: a named
|
// Positional, not named, past the constructor's first six parameters: a named
|
||||||
@@ -62,7 +63,7 @@ public class DeploymentManagerActorArtifactSecretsTests : TestKit, IDisposable
|
|||||||
null, // streamManager: no stream manager in tests
|
null, // streamManager: no stream manager in tests
|
||||||
options,
|
options,
|
||||||
NullLogger<DeploymentManagerActor>.Instance,
|
NullLogger<DeploymentManagerActor>.Instance,
|
||||||
null, // dclManager
|
dclManager,
|
||||||
null, // healthCollector
|
null, // healthCollector
|
||||||
null, // serviceProvider
|
null, // serviceProvider
|
||||||
null, // loggerFactory
|
null, // loggerFactory
|
||||||
@@ -245,6 +246,61 @@ public class DeploymentManagerActorArtifactSecretsTests : TestKit, IDisposable
|
|||||||
Assert.Null(r2.ErrorMessage);
|
Assert.Null(r2.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DataConnections_PushedToDcl_OnlyAfterShippedSecretsAreMirrored()
|
||||||
|
{
|
||||||
|
// M2 (final review): a connection whose config references a secret shipped by THIS
|
||||||
|
// deploy must find it on its first connect, so the DCL create must not reach the
|
||||||
|
// DCL manager before the secret is in this node's store.
|
||||||
|
var store = new FakeSecretStore();
|
||||||
|
var resolver = new FakeSecretResolver();
|
||||||
|
var mirror = CreateMirror(store, resolver);
|
||||||
|
var dcl = CreateTestProbe();
|
||||||
|
bool? secretPresentWhenDclSawCreate = null;
|
||||||
|
dcl.SetAutoPilot(new DelegateAutoPilot((_, msg) =>
|
||||||
|
{
|
||||||
|
if (msg is ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection.CreateConnectionCommand)
|
||||||
|
{
|
||||||
|
secretPresentWhenDclSawCreate ??= store.Contains("gw-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutoPilot.KeepRunning;
|
||||||
|
}));
|
||||||
|
var manager = CreateDeploymentManager(mirror, dcl.Ref);
|
||||||
|
|
||||||
|
var command = ArtifactsCommand("dep-order", secrets: [Art("gw-key", 1)]) with
|
||||||
|
{
|
||||||
|
DataConnections = [new DataConnectionArtifact("Gw", "OpcUa", "{}", null, 3)],
|
||||||
|
};
|
||||||
|
manager.Tell(command);
|
||||||
|
|
||||||
|
var response = ExpectMsg<ArtifactDeploymentResponse>(TimeSpan.FromSeconds(10));
|
||||||
|
Assert.True(response.Success);
|
||||||
|
Assert.Equal(1, response.SecretsApplied);
|
||||||
|
AwaitCondition(() => secretPresentWhenDclSawCreate.HasValue, TimeSpan.FromSeconds(10));
|
||||||
|
Assert.True(secretPresentWhenDclSawCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DataConnections_StillStoredAndPushed_WhenSecretsStepFails()
|
||||||
|
{
|
||||||
|
// The reorder must not lose the connection push when the secrets step fails.
|
||||||
|
var dcl = CreateTestProbe();
|
||||||
|
var manager = CreateDeploymentManager(secretsMirror: null, dclManager: dcl.Ref);
|
||||||
|
|
||||||
|
var command = ArtifactsCommand("dep-order-fail", secrets: [Art("gw-key", 1)]) with
|
||||||
|
{
|
||||||
|
DataConnections = [new DataConnectionArtifact("Gw", "OpcUa", "{}", null, 3)],
|
||||||
|
};
|
||||||
|
manager.Tell(command);
|
||||||
|
|
||||||
|
var response = ExpectMsg<ArtifactDeploymentResponse>(TimeSpan.FromSeconds(10));
|
||||||
|
Assert.False(response.Success);
|
||||||
|
dcl.ExpectMsg<ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection.CreateConnectionCommand>(
|
||||||
|
TimeSpan.FromSeconds(10));
|
||||||
|
Assert.Contains(await _storage.GetAllDataConnectionDefinitionsAsync(), d => d.Name == "Gw");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Fakes (mirrors DeployedSecretsMirrorTests' fakes; kept local/minimal per-task) ──
|
// ── Fakes (mirrors DeployedSecretsMirrorTests' fakes; kept local/minimal per-task) ──
|
||||||
|
|
||||||
private sealed class FakeSecretStore : ISecretStore
|
private sealed class FakeSecretStore : ISecretStore
|
||||||
|
|||||||
Reference in New Issue
Block a user