feat(controlplane): deploy-gate TagConfig strictness — Warn default, Error opt-in at the draft gate (R2-11, 05/CONV-2)

This commit is contained in:
Joseph Doherty
2026-07-13 11:15:34 -04:00
parent 8401055c9d
commit 4260b6ecaa
5 changed files with 199 additions and 6 deletions
@@ -25,31 +25,37 @@ public sealed class AdminOperationsActor : ReceiveActor
private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
private readonly IActorRef _coordinator;
private readonly IReadOnlyDictionary<string, IDriverProbe> _probesByType;
private readonly TagConfigValidationMode _tagConfigValidationMode;
private readonly ILoggingAdapter _log = Context.GetLogger();
/// <summary>Creates actor props for the admin operations actor.</summary>
/// <param name="dbFactory">Factory for creating config database contexts.</param>
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
/// <param name="probes">Driver probes registered in DI; keyed by DriverType (case-insensitive).</param>
/// <param name="tagConfigValidationMode">Deploy-gate TagConfig strictness mode (Warn default | Error opt-in).</param>
/// <returns>Props configured to create an AdminOperationsActor.</returns>
public static Props Props(
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
IActorRef coordinator,
IEnumerable<IDriverProbe> probes) =>
Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator, probes));
IEnumerable<IDriverProbe> probes,
TagConfigValidationMode tagConfigValidationMode = TagConfigValidationMode.Warn) =>
Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator, probes, tagConfigValidationMode));
/// <summary>Initializes a new instance of the AdminOperationsActor.</summary>
/// <param name="dbFactory">Factory for creating config database contexts.</param>
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
/// <param name="probes">Driver probes registered in DI; keyed by DriverType (case-insensitive).</param>
/// <param name="tagConfigValidationMode">Deploy-gate TagConfig strictness mode (Warn default | Error opt-in).</param>
public AdminOperationsActor(
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
IActorRef coordinator,
IEnumerable<IDriverProbe> probes)
IEnumerable<IDriverProbe> probes,
TagConfigValidationMode tagConfigValidationMode = TagConfigValidationMode.Warn)
{
_dbFactory = dbFactory;
_coordinator = coordinator;
_probesByType = probes.ToDictionary(p => p.DriverType, StringComparer.OrdinalIgnoreCase);
_tagConfigValidationMode = tagConfigValidationMode;
ReceiveAsync<StartDeployment>(HandleStartDeploymentAsync);
ReceiveAsync<TestDriverConnect>(HandleTestDriverConnectAsync);
@@ -202,6 +208,31 @@ public sealed class AdminOperationsActor : ReceiveActor
errors.AddRange(DraftValidator.ValidateClusterTopology(cluster, nodes));
}
// R2-11 (05/CONV-2): deploy-time TagConfig strictness inspection over equipment tags. The
// driver-typed EquipmentTagConfigInspector surfaces present-but-invalid enum values (which the
// lenient runtime silently defaults) + structurally-unparseable TagConfig. In Warn mode
// (default) these are non-blocking (logged + appended to the accepted result Message); in Error
// mode they fold into the reject list so a typo'd config cannot be re-deployed until fixed.
// Running servers are untouched either way — the gate only sees re-deploys.
var driverTypeById = draft.DriverInstances
.GroupBy(d => d.DriverInstanceId, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().DriverType, StringComparer.Ordinal);
var tagConfigWarnings = new List<(string TagId, string Text)>();
foreach (var t in draft.Tags.Where(t => t.EquipmentId is not null))
{
if (!driverTypeById.TryGetValue(t.DriverInstanceId, out var driverType)) continue;
foreach (var w in EquipmentTagConfigInspector.Inspect(driverType, t.TagConfig))
tagConfigWarnings.Add((t.TagId, $"tag '{t.TagId}' (equipment '{t.EquipmentId}'): {w}"));
}
if (tagConfigWarnings.Count > 0)
{
if (_tagConfigValidationMode == TagConfigValidationMode.Error)
errors.AddRange(tagConfigWarnings.Select(x => new ValidationError("TagConfigInvalid", x.Text, x.TagId)));
else
foreach (var x in tagConfigWarnings)
_log.Warning("StartDeployment: TagConfig warning (Warn mode, non-blocking): {Warning}", x.Text);
}
if (errors.Count > 0)
{
var summary = string.Join("; ", errors.Select(e => $"[{e.Code}] {e.Message}"));
@@ -277,11 +308,18 @@ public sealed class AdminOperationsActor : ReceiveActor
_coordinator.Tell(new DispatchDeployment(deploymentId, revHash, msg.CorrelationId));
// Accepted message = the compile-cost advisory (if any) + any Warn-mode TagConfig warnings, so
// the operator sees the non-blocking strictness findings on a successful deploy.
var messageParts = new List<string>();
if (advisory is not null) messageParts.Add(advisory);
messageParts.AddRange(tagConfigWarnings.Select(x => x.Text));
var acceptedMessage = messageParts.Count > 0 ? string.Join("; ", messageParts) : null;
replyTo.Tell(new StartDeploymentResult(
StartDeploymentOutcome.Accepted,
deploymentId,
revHash,
Message: advisory,
Message: acceptedMessage,
msg.CorrelationId));
}
catch (Exception ex)
@@ -0,0 +1,18 @@
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
/// <summary>
/// How the deploy gate treats TagConfig strictness inspection results (R2-11, 05/CONV-2). Sourced from
/// <c>Deployment:TagConfigValidationMode</c> in appsettings; defaults to <see cref="Warn"/> so existing
/// deployments are never blocked by a previously-tolerated typo.
/// </summary>
public enum TagConfigValidationMode
{
/// <summary>Non-blocking: inspection warnings are logged and appended to the accepted
/// <c>StartDeploymentResult.Message</c>, but the deploy still succeeds. The default.</summary>
Warn,
/// <summary>Blocking: inspection warnings fold into the draft-gate reject list, so a config with a
/// typo'd enum (or unparseable TagConfig) cannot be re-deployed until fixed. Opt-in per environment
/// once the fleet deploys warning-clean.</summary>
Error,
}
@@ -2,6 +2,7 @@ using Akka.Actor;
using Akka.Cluster.Hosting;
using Akka.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
@@ -61,7 +62,12 @@ public static class ServiceCollectionExtensions
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
var coordinator = registry.Get<ConfigPublishCoordinatorKey>();
var probes = resolver.GetService<IEnumerable<IDriverProbe>>() ?? Enumerable.Empty<IDriverProbe>();
return AdminOperationsActor.Props(dbFactory, coordinator, probes);
// R2-11 (05/CONV-2): deploy-gate TagConfig strictness mode from appsettings
// (Deployment:TagConfigValidationMode = Warn default | Error opt-in).
var mode = Enum.TryParse<TagConfigValidationMode>(
resolver.GetService<IConfiguration>()?["Deployment:TagConfigValidationMode"],
ignoreCase: true, out var m) ? m : TagConfigValidationMode.Warn;
return AdminOperationsActor.Props(dbFactory, coordinator, probes, mode);
},
singletonOptions);