refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Validates required configuration before Akka.NET actor system creation.
|
||||
/// Runs early in startup to fail fast with clear error messages.
|
||||
/// </summary>
|
||||
public static class StartupValidator
|
||||
{
|
||||
/// <summary>Validates required configuration values and throws <see cref="InvalidOperationException"/> listing all errors if any are found.</summary>
|
||||
/// <param name="configuration">The application configuration to validate.</param>
|
||||
public static void Validate(IConfiguration configuration)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
var nodeSection = configuration.GetSection("ScadaBridge:Node");
|
||||
var role = nodeSection["Role"];
|
||||
if (string.IsNullOrEmpty(role) || (role != "Central" && role != "Site"))
|
||||
errors.Add("ScadaBridge:Node:Role must be 'Central' or 'Site'");
|
||||
|
||||
if (string.IsNullOrEmpty(nodeSection["NodeHostname"]))
|
||||
errors.Add("ScadaBridge:Node:NodeHostname is required");
|
||||
|
||||
var portStr = nodeSection["RemotingPort"];
|
||||
if (!int.TryParse(portStr, out var port) || port < 1 || port > 65535)
|
||||
errors.Add("ScadaBridge:Node:RemotingPort must be 1-65535");
|
||||
|
||||
if (role == "Site" && string.IsNullOrEmpty(nodeSection["SiteId"]))
|
||||
errors.Add("ScadaBridge:Node:SiteId is required for Site nodes");
|
||||
|
||||
if (role == "Central")
|
||||
{
|
||||
var dbSection = configuration.GetSection("ScadaBridge:Database");
|
||||
if (string.IsNullOrEmpty(dbSection["ConfigurationDb"]))
|
||||
errors.Add("ScadaBridge:Database:ConfigurationDb connection string required for Central");
|
||||
|
||||
var secSection = configuration.GetSection("ScadaBridge:Security");
|
||||
if (string.IsNullOrEmpty(secSection["LdapServer"]))
|
||||
errors.Add("ScadaBridge:Security:LdapServer required for Central");
|
||||
if (string.IsNullOrEmpty(secSection["JwtSigningKey"]))
|
||||
errors.Add("ScadaBridge:Security:JwtSigningKey required for Central");
|
||||
}
|
||||
|
||||
var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();
|
||||
if (seedNodes == null || seedNodes.Count < 2)
|
||||
errors.Add("ScadaBridge:Cluster:SeedNodes must have at least 2 entries");
|
||||
|
||||
if (role == "Site")
|
||||
{
|
||||
var grpcPortStr = nodeSection["GrpcPort"];
|
||||
int grpcPort = 8083; // NodeOptions default when the key is absent
|
||||
if (grpcPortStr != null && (!int.TryParse(grpcPortStr, out grpcPort) || grpcPort < 1 || grpcPort > 65535))
|
||||
errors.Add("ScadaBridge:Node:GrpcPort must be 1-65535");
|
||||
|
||||
// Host-007 / REQ-HOST-4: the gRPC (Kestrel HTTP/2) port and the Akka
|
||||
// remoting port must differ. Identical values make Kestrel and
|
||||
// Akka.Remote contend for the same TCP port and fail opaquely at
|
||||
// runtime. Uses the resolved GrpcPort, including the 8083 default.
|
||||
if (port == grpcPort)
|
||||
errors.Add("ScadaBridge:Node:GrpcPort must differ from RemotingPort");
|
||||
|
||||
var dbSection = configuration.GetSection("ScadaBridge:Database");
|
||||
if (string.IsNullOrEmpty(dbSection["SiteDbPath"]))
|
||||
errors.Add("ScadaBridge:Database:SiteDbPath required for Site nodes");
|
||||
|
||||
// Host-004: a seed node must reference an Akka.Remote endpoint, never the
|
||||
// Kestrel HTTP/2 gRPC port. A seed entry whose port equals this node's
|
||||
// GrpcPort would make a joining node attempt an Akka.Remote TCP
|
||||
// association against the gRPC listener and fail.
|
||||
if (seedNodes != null)
|
||||
{
|
||||
foreach (var seed in seedNodes)
|
||||
{
|
||||
if (SeedNodePort(seed) == grpcPort)
|
||||
errors.Add(
|
||||
$"ScadaBridge:Cluster:SeedNodes entry '{seed}' must not target the gRPC port " +
|
||||
$"({grpcPort}); seed nodes must reference Akka remoting ports");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Configuration validation failed:\n{string.Join("\n", errors.Select(e => $" - {e}"))}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the TCP port from an Akka seed-node address of the form
|
||||
/// <c>akka.tcp://system@host:port</c>. Returns <c>-1</c> when no port can be parsed.
|
||||
/// </summary>
|
||||
private static int SeedNodePort(string seedNode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(seedNode))
|
||||
return -1;
|
||||
|
||||
var lastColon = seedNode.LastIndexOf(':');
|
||||
if (lastColon < 0 || lastColon == seedNode.Length - 1)
|
||||
return -1;
|
||||
|
||||
return int.TryParse(seedNode[(lastColon + 1)..], out var port) ? port : -1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user