fix(cluster): all four role-scoped validators read the authority — 'Driver' no longer skips every driver rule (archreview 01/C-9)
This commit is contained in:
@@ -15,18 +15,17 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
/// </remarks>
|
||||
public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSourceOptions>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly EffectiveRoles _effectiveRoles;
|
||||
|
||||
/// <summary>
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
|
||||
/// constructor dependency is safe here. <see cref="IConfiguration"/> — not
|
||||
/// <c>IClusterRoleInfo</c> — is the source of this node's roles: <c>IClusterRoleInfo</c>'s
|
||||
/// implementation needs the <c>ActorSystem</c>, which does not exist yet at
|
||||
/// <c>ValidateOnStart</c> time.
|
||||
/// constructor dependency is safe here. This validator's only use of configuration is this
|
||||
/// node's roles, so it takes <see cref="EffectiveRoles"/> — the single role authority
|
||||
/// (archreview R3-04) — directly rather than <see cref="IConfiguration"/>.
|
||||
/// </summary>
|
||||
public ConfigSourceOptionsValidator(IConfiguration configuration)
|
||||
public ConfigSourceOptionsValidator(EffectiveRoles effectiveRoles)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_effectiveRoles = effectiveRoles;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -93,15 +92,12 @@ public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSource
|
||||
|
||||
// Per-cluster mesh Phase 4: a driver-only node has no central ConfigDb connection to fall back
|
||||
// on, so Direct is only ever valid on a fused admin+driver node (admin holds the SQL
|
||||
// connection). Roles come from configuration, not IClusterRoleInfo — see the ctor remark — and
|
||||
// through RoleParser.ResolveEffectiveRoles so the OTOPCUA_ROLES fallback applies (Gitea #532).
|
||||
// Reading Cluster:Roles alone exempted the exact node this rule exists for: an env-only
|
||||
// driver-only node has no ConfigDb (Program.cs gates it on the same env var) yet saw an empty
|
||||
// role list here and sailed through on Direct, to read its configuration from nothing.
|
||||
var roles = RoleParser.ResolveEffectiveRoles(
|
||||
_configuration.GetSection("Cluster:Roles").Get<string[]>());
|
||||
var isDriver = Array.IndexOf(roles, RoleParser.Driver) >= 0;
|
||||
var isAdmin = Array.IndexOf(roles, RoleParser.Admin) >= 0;
|
||||
// connection). EffectiveRoles is the single role authority (archreview R3-04): its precedence
|
||||
// already applies the OTOPCUA_ROLES fallback (Gitea #532) and its normalization already folds
|
||||
// case, so neither an env-only driver-only node nor one spelled 'Driver' can sail through here
|
||||
// on Direct to read its configuration from nothing.
|
||||
var isDriver = _effectiveRoles.HasDriver;
|
||||
var isAdmin = _effectiveRoles.HasAdmin;
|
||||
|
||||
if (isDriver && !isAdmin && isDirect)
|
||||
{
|
||||
|
||||
@@ -17,19 +17,21 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
public sealed class MeshTransportOptionsValidator : IValidateOptions<MeshTransportOptions>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly EffectiveRoles _effectiveRoles;
|
||||
|
||||
/// <summary>
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
|
||||
/// constructor dependency is safe here. <see cref="IConfiguration"/> — not
|
||||
/// <c>IClusterRoleInfo</c> — is the source of this node's roles: <c>IClusterRoleInfo</c>'s
|
||||
/// implementation needs the <c>ActorSystem</c>, which does not exist yet at
|
||||
/// <c>ValidateOnStart</c> time. Same shape as <see cref="ConfigSourceOptionsValidator"/> and
|
||||
/// <see cref="TelemetryOptionsValidator"/>.
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so constructor
|
||||
/// dependencies are safe here. <see cref="IConfiguration"/> is still needed for the
|
||||
/// <c>ConfigServe:GrpcListenPort</c> cross-read (the admin-side obligation below) —
|
||||
/// <see cref="EffectiveRoles"/> is the single role authority (archreview R3-04) this rule reads
|
||||
/// roles through instead of re-deriving them.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The application configuration, read for <c>Cluster:Roles</c>.</param>
|
||||
public MeshTransportOptionsValidator(IConfiguration configuration)
|
||||
/// <param name="configuration">The application configuration, read for <c>ConfigServe:GrpcListenPort</c>.</param>
|
||||
/// <param name="effectiveRoles">This node's resolved effective roles.</param>
|
||||
public MeshTransportOptionsValidator(IConfiguration configuration, EffectiveRoles effectiveRoles)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_effectiveRoles = effectiveRoles;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -68,15 +70,12 @@ public sealed class MeshTransportOptionsValidator : IValidateOptions<MeshTranspo
|
||||
// follows it is a separate scope). Scoped to the driver role because an admin-only central node
|
||||
// dials nodes rather than being dialled, and resolves per-cluster keys at runtime through
|
||||
// MeshTransportOptions.ClusterPsks / the Secrets store, so requiring a single GrpcPsk and an
|
||||
// endpoint list of it would demand configuration it never reads. Roles come from
|
||||
// configuration, not IClusterRoleInfo (see the ctor remark), and through
|
||||
// RoleParser.ResolveEffectiveRoles so the OTOPCUA_ROLES fallback applies (Gitea #532) — a node
|
||||
// configured only through the env var is still the driver that has to dial central, and
|
||||
// exempting it here would leave its acks rejected unauthenticated at runtime.
|
||||
var roles = RoleParser.ResolveEffectiveRoles(
|
||||
_configuration.GetSection("Cluster:Roles").Get<string[]>());
|
||||
var isDriver = Array.IndexOf(roles, RoleParser.Driver) >= 0;
|
||||
var isAdmin = Array.IndexOf(roles, RoleParser.Admin) >= 0;
|
||||
// endpoint list of it would demand configuration it never reads. EffectiveRoles is the single
|
||||
// role authority (archreview R3-04) — its precedence already applies the OTOPCUA_ROLES
|
||||
// fallback and its normalization already folds case, so a node configured only through the env
|
||||
// var, or spelled 'Driver', is still the driver that has to dial central.
|
||||
var isDriver = _effectiveRoles.HasDriver;
|
||||
var isAdmin = _effectiveRoles.HasAdmin;
|
||||
|
||||
if (isGrpc && isDriver)
|
||||
{
|
||||
|
||||
@@ -36,58 +36,40 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
||||
public sealed class SplitTopologyTransportValidator : IValidateOptions<MeshTransportOptions>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly EffectiveRoles _effectiveRoles;
|
||||
|
||||
/// <summary>
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
|
||||
/// constructor dependency is safe here. <see cref="IConfiguration"/> — not
|
||||
/// <c>IClusterRoleInfo</c> — is the source of this node's roles and the three transport modes:
|
||||
/// <c>IClusterRoleInfo</c>'s implementation needs the <c>ActorSystem</c>, which does not exist
|
||||
/// yet at <c>ValidateOnStart</c> time. Reading the modes from configuration too (rather than
|
||||
/// from three injected <c>IOptions</c>) mirrors how the sibling validators cross-read
|
||||
/// <c>Cluster:Roles</c>, and avoids a service-resolution ordering dependency at validation
|
||||
/// time.
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so constructor
|
||||
/// dependencies are safe here. <see cref="IConfiguration"/> is still needed for the three
|
||||
/// transport-mode cross-reads (<c>MeshTransport:Mode</c>, <c>Telemetry:Mode</c>,
|
||||
/// <c>TelemetryDial:Mode</c>) — not <c>IClusterRoleInfo</c>, whose implementation needs the
|
||||
/// <c>ActorSystem</c>, which does not exist yet at <c>ValidateOnStart</c> time.
|
||||
/// <see cref="EffectiveRoles"/> is the single role authority (archreview R3-04) — this rule no
|
||||
/// longer re-derives the effective, normalized role list itself.
|
||||
/// </summary>
|
||||
public SplitTopologyTransportValidator(IConfiguration configuration)
|
||||
public SplitTopologyTransportValidator(IConfiguration configuration, EffectiveRoles effectiveRoles)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_effectiveRoles = effectiveRoles;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValidateOptionsResult Validate(string? name, MeshTransportOptions options)
|
||||
{
|
||||
// The options instance is intentionally unused: every value this rule depends on is cross-read
|
||||
// from IConfiguration, the same way ConfigSourceOptionsValidator reads Cluster:Roles.
|
||||
//
|
||||
// Resolve the EFFECTIVE Akka roles exactly as the node does, or this "fail loud" rule fires on
|
||||
// the wrong role view and passes silently on the two shapes it exists to catch:
|
||||
// (a) Roles-source divergence. AkkaClusterOptions.Roles binds Cluster:Roles but falls back to
|
||||
// OTOPCUA_ROLES when that is empty. A node configured with only
|
||||
// OTOPCUA_ROLES=driver,cluster-SITE-A genuinely carries the cluster role in Akka, so it
|
||||
// must be validated too. Until Gitea #532 this validator was the ONLY code that applied
|
||||
// that fallback — it is now RoleParser.ResolveEffectiveRoles, shared with the
|
||||
// AkkaClusterOptions PostConfigure that finally makes the option itself agree.
|
||||
// (b) Case. The Cluster:Roles bind path is verbatim (no lowercasing), whereas RoleParser.Parse
|
||||
// lowercases the OTOPCUA_ROLES path. Normalize BOTH here (trim + ToLowerInvariant) so
|
||||
// 'Driver' / 'Admin' cannot slip past the ordinal driver / admin checks. (IsClusterRole
|
||||
// itself is case-insensitive as of #532; the trim is still this rule's own business.)
|
||||
var configured = _configuration.GetSection("Cluster:Roles").Get<string[]>() ?? Array.Empty<string>();
|
||||
var effective = RoleParser.ResolveEffectiveRoles(configured);
|
||||
|
||||
var normalized = Array.ConvertAll(effective, r => r.Trim().ToLowerInvariant());
|
||||
|
||||
var clusterRoleIndex = Array.FindIndex(normalized, RoleParser.IsClusterRole);
|
||||
if (clusterRoleIndex < 0)
|
||||
// from IConfiguration (the three transport modes) or EffectiveRoles (this node's roles).
|
||||
if (_effectiveRoles.ClusterRole is null)
|
||||
{
|
||||
// No cluster-{ClusterId} role → not the split topology → DPS is legitimate. This is the
|
||||
// exemption that keeps the validator a no-op for every legacy / single-mesh / test node.
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
|
||||
// Classification is normalized (above); the ClusterId in the message is taken from the
|
||||
// operator's original spelling so it names what they actually wrote.
|
||||
var clusterId = RoleParser.ClusterIdFromRole(effective[clusterRoleIndex].Trim());
|
||||
var isDriver = Array.IndexOf(normalized, RoleParser.Driver) >= 0;
|
||||
var isAdmin = Array.IndexOf(normalized, RoleParser.Admin) >= 0;
|
||||
// ClusterId keeps the operator's original spelling (EffectiveRoles preserves it verbatim by
|
||||
// design — see EffectiveRoles' remarks) so the message names what they actually wrote.
|
||||
var clusterId = _effectiveRoles.ClusterId;
|
||||
var isDriver = _effectiveRoles.HasDriver;
|
||||
var isAdmin = _effectiveRoles.HasAdmin;
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
|
||||
@@ -87,18 +87,17 @@ public sealed class TelemetryDialOptions
|
||||
/// </remarks>
|
||||
public sealed class TelemetryOptionsValidator : IValidateOptions<TelemetryOptions>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly EffectiveRoles _effectiveRoles;
|
||||
|
||||
/// <summary>
|
||||
/// DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
|
||||
/// constructor dependency is safe here. <see cref="IConfiguration"/> — not
|
||||
/// <c>IClusterRoleInfo</c> — is the source of this node's roles: <c>IClusterRoleInfo</c>'s
|
||||
/// implementation needs the <c>ActorSystem</c>, which does not exist yet at
|
||||
/// <c>ValidateOnStart</c> time.
|
||||
/// constructor dependency is safe here. This validator's only use of configuration is this
|
||||
/// node's roles, so it takes <see cref="EffectiveRoles"/> — the single role authority
|
||||
/// (archreview R3-04) — directly rather than <see cref="IConfiguration"/>.
|
||||
/// </summary>
|
||||
public TelemetryOptionsValidator(IConfiguration configuration)
|
||||
public TelemetryOptionsValidator(EffectiveRoles effectiveRoles)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_effectiveRoles = effectiveRoles;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -118,12 +117,11 @@ public sealed class TelemetryOptionsValidator : IValidateOptions<TelemetryOption
|
||||
+ $"Expected '{TelemetryOptions.ModeDps}' or '{TelemetryOptions.ModeGrpc}'.");
|
||||
}
|
||||
|
||||
// Through ResolveEffectiveRoles so the OTOPCUA_ROLES fallback applies (Gitea #532) — a node
|
||||
// configured only through the env var is still a driver node and must satisfy the Grpc-mode
|
||||
// requirements below.
|
||||
var roles = RoleParser.ResolveEffectiveRoles(
|
||||
_configuration.GetSection("Cluster:Roles").Get<string[]>());
|
||||
var isDriver = Array.IndexOf(roles, RoleParser.Driver) >= 0;
|
||||
// EffectiveRoles is the single role authority (archreview R3-04) — its precedence already
|
||||
// applies the OTOPCUA_ROLES fallback (Gitea #532) and its normalization already folds case, so
|
||||
// a node configured only through the env var, or spelled 'Driver', still satisfies the
|
||||
// Grpc-mode requirements below.
|
||||
var isDriver = _effectiveRoles.HasDriver;
|
||||
|
||||
if (isGrpc)
|
||||
{
|
||||
@@ -137,7 +135,7 @@ public sealed class TelemetryOptionsValidator : IValidateOptions<TelemetryOption
|
||||
+ $"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.GrpcListenPort)}.");
|
||||
}
|
||||
|
||||
if (roles.Length > 0 && string.IsNullOrEmpty(options.ApiKey))
|
||||
if (_effectiveRoles.Gossiped.Count > 0 && string.IsNullOrEmpty(options.ApiKey))
|
||||
{
|
||||
errors.Add(
|
||||
$"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.ApiKey)} is empty. Under "
|
||||
|
||||
Reference in New Issue
Block a user