feat(configdb): add Deployment, NodeDeploymentState, ConfigEdit, DataProtectionKey entities
Phase 1 entities for the v2 live-edit + snapshot-deploy model:
Deployment — immutable artifact snapshot (replaces v1 ConfigGeneration row)
Status enum {Dispatching, AwaitingApplyAcks, Sealed,
PartiallyFailed, TimedOut}; carries the SHA256 RevisionHash and
the SnapshotAndFlatten() ArtifactBlob; RowVersion for optimistic
concurrency.
NodeDeploymentState — per-(node, deployment) apply progress row owned by
DriverHostActor (replaces single-row ClusterNodeGenerationState).
Composite key (NodeId, DeploymentId) gives the
ConfigPublishCoordinator the full history it needs to
reconstruct in-flight state after a failover.
ConfigEdit — append-only audit row written by AdminOperationsActor on every
mutating op; optional ExecutionId correlates edits inside one
admin transaction (e.g. an import batch).
DataProtectionKey — ASP.NET DataProtection key ring storage via
IDataProtectionKeyContext so every admin-role node decrypts
the same cookies without sharing a filesystem.
OtOpcUaConfigDbContext now implements IDataProtectionKeyContext and registers four new
DbSets + four new ConfigureXxx mappings.
Central package bumps (forced by Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
10.0.7's transitive dep):
Microsoft.EntityFrameworkCore.{,Design,InMemory,SqlServer} 10.0.0 -> 10.0.7
Microsoft.Extensions.{Configuration.Abstractions,Configuration.Json,Hosting,Hosting.WindowsServices,Http} 10.0.0 -> 10.0.7
EF migration generation + the ConfigGeneration drop + RedundancyRole column removal are
deferred to Task 14 (high-risk, non-parallelizable).
This commit is contained in:
@@ -54,18 +54,18 @@
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.1" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Append-only audit row written by AdminOperationsActor on every mutating live-edit
|
||||
/// operation. The ExecutionId optionally correlates a sequence of edits that ran inside one
|
||||
/// admin transaction (e.g. an import batch that updates many Equipment rows).
|
||||
/// </summary>
|
||||
public sealed class ConfigEdit
|
||||
{
|
||||
public Guid EditId { get; init; } = Guid.NewGuid();
|
||||
|
||||
public required string EntityType { get; init; }
|
||||
|
||||
public Guid EntityId { get; init; }
|
||||
|
||||
/// <summary>JSON payload of the column-name → new-value pairs touched by this edit.</summary>
|
||||
public required string FieldsJson { get; init; }
|
||||
|
||||
/// <summary>Optional correlation across edits inside a single admin operation.</summary>
|
||||
public Guid? ExecutionId { get; init; }
|
||||
|
||||
public required string EditedBy { get; init; }
|
||||
|
||||
public DateTime EditedAtUtc { get; init; } = DateTime.UtcNow;
|
||||
|
||||
public required string SourceNode { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable snapshot of a config artifact dispatched to every driver-role node by the
|
||||
/// ConfigPublishCoordinator. Replaces the v1 <see cref="ConfigGeneration"/> draft/publish
|
||||
/// row; the ArtifactBlob carries the SnapshotAndFlatten() output produced by
|
||||
/// AdminOperationsActor.
|
||||
/// </summary>
|
||||
public sealed class Deployment
|
||||
{
|
||||
public Guid DeploymentId { get; init; } = Guid.NewGuid();
|
||||
|
||||
public required string RevisionHash { get; init; }
|
||||
|
||||
public DeploymentStatus Status { get; set; } = DeploymentStatus.Dispatching;
|
||||
|
||||
public required string CreatedBy { get; init; }
|
||||
|
||||
public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow;
|
||||
|
||||
public byte[] ArtifactBlob { get; init; } = Array.Empty<byte>();
|
||||
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
public DateTime? SealedAtUtc { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per-(node, deployment) apply progress row owned by the DriverHostActor. Replaces the
|
||||
/// v1 <see cref="ClusterNodeGenerationState"/> single-row-per-node model with a history
|
||||
/// of every apply attempt so the ConfigPublishCoordinator can reconstruct in-flight state
|
||||
/// after a failover.
|
||||
/// </summary>
|
||||
public sealed class NodeDeploymentState
|
||||
{
|
||||
public required string NodeId { get; init; }
|
||||
|
||||
public Guid DeploymentId { get; init; }
|
||||
|
||||
public NodeDeploymentStatus Status { get; set; } = NodeDeploymentStatus.Applying;
|
||||
|
||||
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? AppliedAtUtc { get; set; }
|
||||
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
public ClusterNode? Node { get; set; }
|
||||
public Deployment? Deployment { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Lifecycle of a deployment artifact dispatched by the v2 ConfigPublishCoordinator.
|
||||
/// Replaces the v1 ConfigGeneration draft/publish lifecycle (decision tracked in the
|
||||
/// v2 hosting-alignment design doc).
|
||||
/// </summary>
|
||||
public enum DeploymentStatus
|
||||
{
|
||||
Dispatching = 0,
|
||||
AwaitingApplyAcks = 1,
|
||||
Sealed = 2,
|
||||
PartiallyFailed = 3,
|
||||
TimedOut = 4,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Per-node deployment apply state. Replaces the v1 NodeApplyStatus that was attached to
|
||||
/// ClusterNodeGenerationState.
|
||||
/// </summary>
|
||||
public enum NodeDeploymentStatus
|
||||
{
|
||||
Applying = 0,
|
||||
Applied = 1,
|
||||
Failed = 2,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
@@ -9,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
/// any divergence is a defect caught by the SchemaComplianceTests introspection check.
|
||||
/// </summary>
|
||||
public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbContext> options)
|
||||
: DbContext(options)
|
||||
: DbContext(options), IDataProtectionKeyContext
|
||||
{
|
||||
public DbSet<ServerCluster> ServerClusters => Set<ServerCluster>();
|
||||
public DbSet<ClusterNode> ClusterNodes => Set<ClusterNode>();
|
||||
@@ -37,6 +38,16 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
public DbSet<ScriptedAlarm> ScriptedAlarms => Set<ScriptedAlarm>();
|
||||
public DbSet<ScriptedAlarmState> ScriptedAlarmStates => Set<ScriptedAlarmState>();
|
||||
|
||||
// v2 deploy-model tables (Phase 1 of the Akka + fused-hosting alignment). Replace the
|
||||
// ConfigGeneration/ClusterNodeGenerationState pair when Task 14's migration runs.
|
||||
public DbSet<Deployment> Deployments => Set<Deployment>();
|
||||
public DbSet<NodeDeploymentState> NodeDeploymentStates => Set<NodeDeploymentState>();
|
||||
public DbSet<ConfigEdit> ConfigEdits => Set<ConfigEdit>();
|
||||
|
||||
// ASP.NET DataProtection key ring storage (decision: keys persisted in ConfigDb so every
|
||||
// admin-role node decrypts the same cookies without sharing a filesystem).
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@@ -64,6 +75,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
ConfigureVirtualTag(modelBuilder);
|
||||
ConfigureScriptedAlarm(modelBuilder);
|
||||
ConfigureScriptedAlarmState(modelBuilder);
|
||||
ConfigureDeployment(modelBuilder);
|
||||
ConfigureNodeDeploymentState(modelBuilder);
|
||||
ConfigureConfigEdit(modelBuilder);
|
||||
ConfigureDataProtectionKey(modelBuilder);
|
||||
}
|
||||
|
||||
private static void ConfigureServerCluster(ModelBuilder modelBuilder)
|
||||
@@ -729,4 +744,79 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.UpdatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDeployment(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Deployment>(e =>
|
||||
{
|
||||
e.ToTable("Deployment");
|
||||
e.HasKey(x => x.DeploymentId);
|
||||
e.Property(x => x.DeploymentId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.RevisionHash).HasMaxLength(64).IsRequired();
|
||||
e.Property(x => x.Status).HasConversion<int>();
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128).IsRequired();
|
||||
e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.ArtifactBlob).HasColumnType("varbinary(max)");
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
e.Property(x => x.FailureReason).HasMaxLength(2048);
|
||||
e.Property(x => x.SealedAtUtc).HasColumnType("datetime2(3)");
|
||||
|
||||
e.HasIndex(x => x.Status).HasDatabaseName("IX_Deployment_Status");
|
||||
e.HasIndex(x => x.CreatedAtUtc).HasDatabaseName("IX_Deployment_CreatedAt");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureNodeDeploymentState(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<NodeDeploymentState>(e =>
|
||||
{
|
||||
e.ToTable("NodeDeploymentState");
|
||||
e.HasKey(x => new { x.NodeId, x.DeploymentId });
|
||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||
e.Property(x => x.Status).HasConversion<int>();
|
||||
e.Property(x => x.StartedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.AppliedAtUtc).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.FailureReason).HasMaxLength(2048);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasOne(x => x.Node).WithMany().HasForeignKey(x => x.NodeId).OnDelete(DeleteBehavior.Restrict);
|
||||
e.HasOne(x => x.Deployment).WithMany().HasForeignKey(x => x.DeploymentId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasIndex(x => x.DeploymentId).HasDatabaseName("IX_NodeDeploymentState_Deployment");
|
||||
e.HasIndex(x => x.Status).HasDatabaseName("IX_NodeDeploymentState_Status");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureConfigEdit(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ConfigEdit>(e =>
|
||||
{
|
||||
e.ToTable("ConfigEdit", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_ConfigEdit_FieldsJson_IsJson", "ISJSON(FieldsJson) = 1");
|
||||
});
|
||||
e.HasKey(x => x.EditId);
|
||||
e.Property(x => x.EditId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.EntityType).HasMaxLength(64).IsRequired();
|
||||
e.Property(x => x.FieldsJson).HasColumnType("nvarchar(max)").IsRequired();
|
||||
e.Property(x => x.EditedBy).HasMaxLength(128).IsRequired();
|
||||
e.Property(x => x.EditedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.SourceNode).HasMaxLength(64).IsRequired();
|
||||
|
||||
// Replays of admin operations group rows by ExecutionId, then by time.
|
||||
e.HasIndex(x => new { x.EntityType, x.EntityId }).HasDatabaseName("IX_ConfigEdit_Entity");
|
||||
e.HasIndex(x => x.ExecutionId).HasFilter("[ExecutionId] IS NOT NULL").HasDatabaseName("IX_ConfigEdit_Execution");
|
||||
e.HasIndex(x => x.EditedAtUtc).HasDatabaseName("IX_ConfigEdit_EditedAt");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDataProtectionKey(ModelBuilder modelBuilder)
|
||||
{
|
||||
// ASP.NET DataProtection ships its own EF mapping; override only the table name so it
|
||||
// matches the rest of the schema's PascalCase convention.
|
||||
modelBuilder.Entity<DataProtectionKey>(e =>
|
||||
{
|
||||
e.ToTable("DataProtectionKeys");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
|
||||
<PackageReference Include="LiteDB"/>
|
||||
|
||||
Reference in New Issue
Block a user