fix(deployment): instance delete fully removes the record
Deleting an instance only undeployed it from the site and set the state to NotDeployed, leaving an orphan record that could never be removed — the state-transition matrix rejected delete from NotDeployed. Delete now removes the instance record entirely (deployment history, snapshot, attribute/alarm overrides, and connection bindings go with it), and is permitted from any state.
This commit is contained in:
@@ -34,5 +34,12 @@ public interface IDeploymentManagerRepository
|
||||
Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
|
||||
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an instance and everything that depends on it: deployment
|
||||
/// records, deployed config snapshot, attribute/alarm overrides, and
|
||||
/// connection bindings.
|
||||
/// </summary>
|
||||
Task DeleteInstanceAsync(int instanceId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -189,6 +189,27 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DeleteInstanceAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// DeploymentRecords have a Restrict FK to Instance — remove them
|
||||
// explicitly first. The snapshot, overrides, and connection bindings
|
||||
// are configured with cascade delete and go with the instance.
|
||||
var records = await _dbContext.DeploymentRecords
|
||||
.Where(d => d.InstanceId == instanceId)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (records.Count > 0)
|
||||
{
|
||||
_dbContext.DeploymentRecords.RemoveRange(records);
|
||||
}
|
||||
|
||||
var instance = await _dbContext.Set<Instance>()
|
||||
.FirstOrDefaultAsync(i => i.Id == instanceId, cancellationToken);
|
||||
if (instance != null)
|
||||
{
|
||||
_dbContext.Set<Instance>().Remove(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -282,7 +282,9 @@ public class DeploymentService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-6: Delete an instance. Stops actor, removes config. S&F NOT cleared.
|
||||
/// WP-6: Delete an instance. Stops the site actor, removes site config, and
|
||||
/// removes the central instance record (deployment history, snapshot,
|
||||
/// overrides, and connection bindings go with it). S&F NOT cleared.
|
||||
/// Delete fails if site unreachable (30s timeout via CommunicationOptions).
|
||||
/// </summary>
|
||||
public async Task<Result<InstanceLifecycleResponse>> DeleteInstanceAsync(
|
||||
@@ -309,12 +311,10 @@ public class DeploymentService
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
// Remove deployed snapshot
|
||||
await _repository.DeleteDeployedSnapshotAsync(instanceId, cancellationToken);
|
||||
|
||||
// Set state to NotDeployed (or the instance record could be deleted entirely by higher layers)
|
||||
instance.State = InstanceState.NotDeployed;
|
||||
await _repository.UpdateInstanceAsync(instance, cancellationToken);
|
||||
// Delete means delete: remove the instance record entirely.
|
||||
// Deployment records, snapshot, overrides, and connection bindings
|
||||
// are removed with it (see repository implementation).
|
||||
await _repository.DeleteInstanceAsync(instanceId, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ namespace ScadaLink.DeploymentManager;
|
||||
///
|
||||
/// State | Deploy | Disable | Enable | Delete
|
||||
/// ----------|--------|---------|--------|-------
|
||||
/// NotDeploy | OK | NO | NO | NO
|
||||
/// NotDeploy | OK | NO | NO | OK
|
||||
/// Enabled | OK | OK | NO | OK
|
||||
/// Disabled | OK* | NO | OK | OK
|
||||
///
|
||||
/// * Deploy on a Disabled instance also enables it.
|
||||
/// Delete removes the instance record entirely; it is valid from any state.
|
||||
/// </summary>
|
||||
public static class StateTransitionValidator
|
||||
{
|
||||
@@ -25,7 +26,7 @@ public static class StateTransitionValidator
|
||||
currentState == InstanceState.Disabled;
|
||||
|
||||
public static bool CanDelete(InstanceState currentState) =>
|
||||
currentState is InstanceState.Enabled or InstanceState.Disabled;
|
||||
currentState is InstanceState.NotDeployed or InstanceState.Enabled or InstanceState.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a human-readable error message if the transition is invalid, or null if valid.
|
||||
|
||||
@@ -188,15 +188,14 @@ public class DeploymentServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteInstanceAsync_WhenNotDeployed_ReturnsTransitionError()
|
||||
public async Task DeleteInstanceAsync_InstanceNotFound_ReturnsFailure()
|
||||
{
|
||||
var instance = new Instance("TestInst") { Id = 1, SiteId = 1, State = InstanceState.NotDeployed };
|
||||
_repo.GetInstanceByIdAsync(1).Returns(instance);
|
||||
_repo.GetInstanceByIdAsync(1).Returns((Instance?)null);
|
||||
|
||||
var result = await _service.DeleteInstanceAsync(1, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("not allowed", result.Error);
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
// ── WP-8: Deployment comparison ──
|
||||
|
||||
@@ -73,9 +73,9 @@ public class StateTransitionValidatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDelete_WhenNotDeployed_ReturnsFalse()
|
||||
public void CanDelete_WhenNotDeployed_ReturnsTrue()
|
||||
{
|
||||
Assert.False(StateTransitionValidator.CanDelete(InstanceState.NotDeployed));
|
||||
Assert.True(StateTransitionValidator.CanDelete(InstanceState.NotDeployed));
|
||||
}
|
||||
|
||||
// ── ValidateTransition ──
|
||||
@@ -103,10 +103,10 @@ public class StateTransitionValidatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTransition_InvalidDeleteOnNotDeployed_ReturnsError()
|
||||
public void ValidateTransition_ValidDeleteOnNotDeployed_ReturnsNull()
|
||||
{
|
||||
var error = StateTransitionValidator.ValidateTransition(InstanceState.NotDeployed, "delete");
|
||||
Assert.NotNull(error);
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user