refactor(mesh): step 6 — delete the ClusterClient mode value; split topology now requires Grpc
MeshTransportOptions.ModeClusterClient is gone. MeshTransportOptionsValidator accepts Dps or Grpc and nothing else, so a node left carrying the string "ClusterClient" is now REFUSED at boot naming MeshTransport:Mode and both remaining values — deliberately fail-closed rather than falling back to Dps, which on a split-topology node would deliver nothing at all. A new fact pins exactly that. SplitTopologyTransportValidator tightens from an allow-list of two to Grpc only, as its own comment said it would once ClusterClient was deleted. Dps on a cluster-role node stays refused; the rule is still an explicit allow-list, so a typo'd mode fails too. Tests: the two ClusterClient-mode facts in MeshTransportOptionsValidatorTests are deleted (their Dps siblings already assert the same rule for the one remaining non-Grpc mode), the SplitTopology fixtures move to Grpc, and the three-mode Theories in ControlPlaneStarterOrderTests / CentralCommunicationActorTests become two-mode.
This commit is contained in:
@@ -71,15 +71,26 @@ public class MeshTransportOptionsValidatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_mode_message_names_all_three_accepted_values()
|
||||
public void Unknown_mode_message_names_both_accepted_values()
|
||||
{
|
||||
// An operator who mistypes the mode gets the full menu, not a partial one — Grpc became a
|
||||
// legal value in Phase 0 and an error that omitted it would send them back to ClusterClient.
|
||||
var result = Validate(new MeshTransportOptions { Mode = "receptionist" });
|
||||
// An operator who mistypes the mode gets the full menu, not a partial one.
|
||||
var result = Validate(new MeshTransportOptions { Mode = "not-a-mode" });
|
||||
|
||||
result.Failed.ShouldBeTrue();
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeDps);
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_retired_ClusterClient_mode_fails_rather_than_falling_back_to_Dps()
|
||||
{
|
||||
// A node left carrying the deleted transport's mode value must be refused at boot. Silently
|
||||
// treating it as Dps would leave a split-topology node running, listening, and delivering
|
||||
// nothing across its mesh boundary — the exact absence this validator exists to prevent.
|
||||
var result = Validate(new MeshTransportOptions { Mode = "ClusterClient" });
|
||||
|
||||
result.Failed.ShouldBeTrue();
|
||||
result.FailureMessage.ShouldContain("MeshTransport:Mode");
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
|
||||
}
|
||||
|
||||
@@ -293,15 +304,6 @@ public class MeshTransportOptionsValidatorTests
|
||||
"admin", "cluster-MAIN").Succeeded.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClusterClient_admin_node_needs_no_config_serve_listener()
|
||||
{
|
||||
// Fails only if the ConfigServe rule wrongly fires outside Grpc mode.
|
||||
Validate(
|
||||
new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient },
|
||||
"admin", "cluster-MAIN").Succeeded.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_admin_role_is_also_read_from_OTOPCUA_ROLES()
|
||||
{
|
||||
@@ -348,14 +350,6 @@ public class MeshTransportOptionsValidatorTests
|
||||
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClusterClient_mode_requires_none_of_the_new_Grpc_keys()
|
||||
{
|
||||
Validate(
|
||||
new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient },
|
||||
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Grpc_fused_admin_and_driver_node_must_satisfy_BOTH_surfaces()
|
||||
{
|
||||
|
||||
+16
-17
@@ -51,7 +51,6 @@ public class SplitTopologyTransportValidatorTests
|
||||
|
||||
result.Failed.ShouldBeTrue();
|
||||
result.FailureMessage.ShouldContain("MeshTransport:Mode");
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
|
||||
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
|
||||
result.FailureMessage.ShouldContain("SITE-A");
|
||||
}
|
||||
@@ -59,9 +58,8 @@ public class SplitTopologyTransportValidatorTests
|
||||
[Fact]
|
||||
public void Cluster_role_driver_node_on_Grpc_mesh_transport_passes()
|
||||
{
|
||||
// The migration window: Grpc is the transport this rule exists to arrive at, so a rig node
|
||||
// flipped to it must boot. Without this relaxation the very first Grpc rollout step — setting
|
||||
// the mode on a split-topology node — is a host-start failure.
|
||||
// Grpc is now the ONLY mode that satisfies this rule: it is the only transport that crosses
|
||||
// a mesh boundary.
|
||||
Validate(
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeGrpc,
|
||||
@@ -92,8 +90,9 @@ public class SplitTopologyTransportValidatorTests
|
||||
[Fact]
|
||||
public void Cluster_role_node_on_an_unrecognised_mesh_transport_still_fails()
|
||||
{
|
||||
// Widening the rule from one accepted value to two must not widen it to "anything that is not
|
||||
// Dps" — a typo'd mode is still a mode that delivers nothing across the boundary.
|
||||
// The rule is an explicit allow-list, not "anything that is not Dps" — a typo'd mode is
|
||||
// still a mode that delivers nothing across the boundary. The retired 'ClusterClient' value
|
||||
// lands here too.
|
||||
var result = Validate(
|
||||
meshMode: "receptionist",
|
||||
telemetryMode: TelemetryOptions.ModeGrpc,
|
||||
@@ -108,7 +107,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Cluster_role_driver_node_fully_configured_passes()
|
||||
{
|
||||
Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeGrpc,
|
||||
telemetryDialMode: null,
|
||||
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
|
||||
@@ -118,7 +117,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Cluster_role_admin_node_fully_configured_passes()
|
||||
{
|
||||
Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: null,
|
||||
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
|
||||
"admin", "cluster-SITE-A").Succeeded.ShouldBeTrue();
|
||||
@@ -128,7 +127,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Cluster_role_driver_node_on_Dps_telemetry_fails()
|
||||
{
|
||||
var result = Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeDps,
|
||||
telemetryDialMode: null,
|
||||
"driver", "cluster-SITE-A");
|
||||
@@ -142,7 +141,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Cluster_role_admin_node_on_Dps_telemetry_dial_fails()
|
||||
{
|
||||
var result = Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: null,
|
||||
telemetryDialMode: TelemetryDialOptions.ModeDps,
|
||||
"admin", "cluster-SITE-A");
|
||||
@@ -156,7 +155,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Fused_cluster_role_node_needs_all_three_to_pass()
|
||||
{
|
||||
Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeGrpc,
|
||||
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
|
||||
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
|
||||
@@ -166,7 +165,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Fused_cluster_role_node_missing_telemetry_grpc_fails()
|
||||
{
|
||||
var result = Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeDps,
|
||||
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
|
||||
"admin", "driver", "cluster-MAIN");
|
||||
@@ -179,7 +178,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Fused_cluster_role_node_missing_telemetry_dial_grpc_fails()
|
||||
{
|
||||
var result = Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: TelemetryOptions.ModeGrpc,
|
||||
telemetryDialMode: TelemetryDialOptions.ModeDps,
|
||||
"admin", "driver", "cluster-MAIN");
|
||||
@@ -229,7 +228,7 @@ public class SplitTopologyTransportValidatorTests
|
||||
public void Mode_matching_is_case_insensitive()
|
||||
{
|
||||
Validate(
|
||||
meshMode: "clusterclient",
|
||||
meshMode: "grpc",
|
||||
telemetryMode: "grpc",
|
||||
telemetryDialMode: "grpc",
|
||||
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
|
||||
@@ -287,13 +286,13 @@ public class SplitTopologyTransportValidatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cluster_role_node_with_neither_admin_nor_driver_only_needs_ClusterClient()
|
||||
public void Cluster_role_node_with_neither_admin_nor_driver_only_needs_the_mesh_mode()
|
||||
{
|
||||
// A cluster-role node that is neither admin nor driver has no telemetry surface to constrain —
|
||||
// only the always-on MeshTransport=ClusterClient rule applies. Telemetry/TelemetryDial left on
|
||||
// only the always-on MeshTransport=Grpc rule applies. Telemetry/TelemetryDial left on
|
||||
// their Dps defaults must not fail it.
|
||||
Validate(
|
||||
meshMode: MeshTransportOptions.ModeClusterClient,
|
||||
meshMode: MeshTransportOptions.ModeGrpc,
|
||||
telemetryMode: null,
|
||||
telemetryDialMode: null,
|
||||
"cluster-SITE-A").Succeeded.ShouldBeTrue();
|
||||
|
||||
+4
-5
@@ -515,17 +515,16 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
|
||||
|
||||
[Theory]
|
||||
[InlineData(MeshTransportOptions.ModeDps)]
|
||||
[InlineData(MeshTransportOptions.ModeClusterClient)]
|
||||
[InlineData(MeshTransportOptions.ModeGrpc)]
|
||||
public void The_known_cluster_gate_is_published_in_every_transport_mode(string mode)
|
||||
{
|
||||
// Replaces an earlier fact asserting the OPPOSITE, whose premise did not survive Phase 1B: the
|
||||
// gate feeds CentralCommandAuthInterceptor, which is registered on every admin node, and the
|
||||
// ack service it protects is mapped by LISTENER PORT — neither consults MeshTransport:Mode.
|
||||
// Gating the publication on Grpc meant a dark-switch rollback to Dps left that endpoint
|
||||
// serving with a permanently inactive gate, silently restoring both the timing
|
||||
// oracle and the pre-authentication store/audit flood. The set is central's cluster
|
||||
// inventory, not transport state.
|
||||
// Gating the publication on Grpc meant a rollback to Dps left that endpoint serving with a
|
||||
// permanently inactive gate, silently restoring both the timing oracle and the
|
||||
// pre-authentication store/audit flood. The set is central's cluster inventory, not
|
||||
// transport state.
|
||||
var factory = NewInMemoryDbFactory();
|
||||
SeedGrpcCluster(factory, "C1", ("host-a:4053", "host-a", 4056, true, false));
|
||||
var psk = new RecordingPskProvider();
|
||||
|
||||
+2
-4
@@ -55,11 +55,10 @@ public sealed class ControlPlaneStarterOrderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(MeshTransportOptions.ModeDps)]
|
||||
[InlineData(MeshTransportOptions.ModeClusterClient)]
|
||||
[InlineData(MeshTransportOptions.ModeGrpc)]
|
||||
public async Task An_admin_node_boots_with_the_real_mesh_dependency_graph(string mode)
|
||||
{
|
||||
// Every mode, because the eager resolve was mode-independent: the starter resolved both
|
||||
// Both modes, because the eager resolve was mode-independent: the starter resolved both
|
||||
// dependencies before anything looked at MeshTransport:Mode, so Dps — the DEFAULT, and the
|
||||
// rollback target — died exactly like Grpc did.
|
||||
using var host = BuildAdminHost(mode);
|
||||
@@ -82,7 +81,6 @@ public sealed class ControlPlaneStarterOrderTests
|
||||
|
||||
[Theory]
|
||||
[InlineData(MeshTransportOptions.ModeDps)]
|
||||
[InlineData(MeshTransportOptions.ModeClusterClient)]
|
||||
[InlineData(MeshTransportOptions.ModeGrpc)]
|
||||
public async Task The_deferred_dependencies_are_resolved_and_used_once_the_host_is_up(string mode)
|
||||
{
|
||||
@@ -90,7 +88,7 @@ public sealed class ControlPlaneStarterOrderTests
|
||||
// silent one, with the known-cluster gate permanently unpublished and the dialer permanently
|
||||
// absent. So this asserts the far end of the deferral: the actor resolved the PSK provider
|
||||
// FROM THE CONTAINER and drove it, through the ordinary contact-refresh path. A Theory over
|
||||
// all three modes because publication is mode-independent (the gate follows the endpoint).
|
||||
// both modes because publication is mode-independent (the gate follows the endpoint).
|
||||
//
|
||||
// The provider is registered as a TYPE with an ISecretResolver constructor, so the resolve
|
||||
// walks the same registry-bound chain the production MeshPskProvider does — which makes
|
||||
|
||||
Reference in New Issue
Block a user