381 lines
17 KiB
C#
381 lines
17 KiB
C#
// Port of Go server/reload.go — config diffing, validation, and CLI override merging
|
|
// for hot reload support. Reference: golang/nats-server/server/reload.go.
|
|
|
|
namespace NATS.Server.Configuration;
|
|
|
|
/// <summary>
|
|
/// Provides static methods for comparing two <see cref="NatsOptions"/> instances,
|
|
/// validating that detected changes are reloadable, and merging CLI overrides
|
|
/// so that command-line flags always take precedence over config file values.
|
|
/// </summary>
|
|
public static class ConfigReloader
|
|
{
|
|
// Non-reloadable options (match Go server — Host, Port, ServerName require restart)
|
|
private static readonly HashSet<string> NonReloadable =
|
|
["Host", "Port", "ServerName", "Cluster", "JetStream.StoreDir"];
|
|
|
|
// Logging-related options
|
|
private static readonly HashSet<string> LoggingOptions =
|
|
["Debug", "Trace", "TraceVerbose", "Logtime", "LogtimeUTC", "LogFile",
|
|
"LogSizeLimit", "LogMaxFiles", "Syslog", "RemoteSyslog"];
|
|
|
|
// Auth-related options
|
|
private static readonly HashSet<string> AuthOptions =
|
|
["Username", "Password", "Authorization", "Users", "NKeys",
|
|
"NoAuthUser", "AuthTimeout"];
|
|
|
|
// TLS-related options
|
|
private static readonly HashSet<string> TlsOptions =
|
|
["TlsCert", "TlsKey", "TlsCaCert", "TlsVerify", "TlsMap",
|
|
"TlsTimeout", "TlsHandshakeFirst", "TlsHandshakeFirstFallback",
|
|
"AllowNonTls", "TlsRateLimit", "TlsPinnedCerts"];
|
|
|
|
/// <summary>
|
|
/// Compares two <see cref="NatsOptions"/> instances property by property and returns
|
|
/// a list of <see cref="IConfigChange"/> for every property that differs. Each change
|
|
/// is tagged with the appropriate category flags.
|
|
/// </summary>
|
|
public static List<IConfigChange> Diff(NatsOptions oldOpts, NatsOptions newOpts)
|
|
{
|
|
var changes = new List<IConfigChange>();
|
|
|
|
// Non-reloadable
|
|
CompareAndAdd(changes, "Host", oldOpts.Host, newOpts.Host);
|
|
CompareAndAdd(changes, "Port", oldOpts.Port, newOpts.Port);
|
|
CompareAndAdd(changes, "ServerName", oldOpts.ServerName, newOpts.ServerName);
|
|
|
|
// Logging
|
|
CompareAndAdd(changes, "Debug", oldOpts.Debug, newOpts.Debug);
|
|
CompareAndAdd(changes, "Trace", oldOpts.Trace, newOpts.Trace);
|
|
CompareAndAdd(changes, "TraceVerbose", oldOpts.TraceVerbose, newOpts.TraceVerbose);
|
|
CompareAndAdd(changes, "Logtime", oldOpts.Logtime, newOpts.Logtime);
|
|
CompareAndAdd(changes, "LogtimeUTC", oldOpts.LogtimeUTC, newOpts.LogtimeUTC);
|
|
CompareAndAdd(changes, "LogFile", oldOpts.LogFile, newOpts.LogFile);
|
|
CompareAndAdd(changes, "LogSizeLimit", oldOpts.LogSizeLimit, newOpts.LogSizeLimit);
|
|
CompareAndAdd(changes, "LogMaxFiles", oldOpts.LogMaxFiles, newOpts.LogMaxFiles);
|
|
CompareAndAdd(changes, "Syslog", oldOpts.Syslog, newOpts.Syslog);
|
|
CompareAndAdd(changes, "RemoteSyslog", oldOpts.RemoteSyslog, newOpts.RemoteSyslog);
|
|
|
|
// Auth
|
|
CompareAndAdd(changes, "Username", oldOpts.Username, newOpts.Username);
|
|
CompareAndAdd(changes, "Password", oldOpts.Password, newOpts.Password);
|
|
CompareAndAdd(changes, "Authorization", oldOpts.Authorization, newOpts.Authorization);
|
|
CompareCollectionAndAdd(changes, "Users", oldOpts.Users, newOpts.Users);
|
|
CompareCollectionAndAdd(changes, "NKeys", oldOpts.NKeys, newOpts.NKeys);
|
|
CompareAndAdd(changes, "NoAuthUser", oldOpts.NoAuthUser, newOpts.NoAuthUser);
|
|
CompareAndAdd(changes, "AuthTimeout", oldOpts.AuthTimeout, newOpts.AuthTimeout);
|
|
|
|
// TLS
|
|
CompareAndAdd(changes, "TlsCert", oldOpts.TlsCert, newOpts.TlsCert);
|
|
CompareAndAdd(changes, "TlsKey", oldOpts.TlsKey, newOpts.TlsKey);
|
|
CompareAndAdd(changes, "TlsCaCert", oldOpts.TlsCaCert, newOpts.TlsCaCert);
|
|
CompareAndAdd(changes, "TlsVerify", oldOpts.TlsVerify, newOpts.TlsVerify);
|
|
CompareAndAdd(changes, "TlsMap", oldOpts.TlsMap, newOpts.TlsMap);
|
|
CompareAndAdd(changes, "TlsTimeout", oldOpts.TlsTimeout, newOpts.TlsTimeout);
|
|
CompareAndAdd(changes, "TlsHandshakeFirst", oldOpts.TlsHandshakeFirst, newOpts.TlsHandshakeFirst);
|
|
CompareAndAdd(changes, "TlsHandshakeFirstFallback", oldOpts.TlsHandshakeFirstFallback, newOpts.TlsHandshakeFirstFallback);
|
|
CompareAndAdd(changes, "AllowNonTls", oldOpts.AllowNonTls, newOpts.AllowNonTls);
|
|
CompareAndAdd(changes, "TlsRateLimit", oldOpts.TlsRateLimit, newOpts.TlsRateLimit);
|
|
CompareCollectionAndAdd(changes, "TlsPinnedCerts", oldOpts.TlsPinnedCerts, newOpts.TlsPinnedCerts);
|
|
|
|
// Limits
|
|
CompareAndAdd(changes, "MaxConnections", oldOpts.MaxConnections, newOpts.MaxConnections);
|
|
CompareAndAdd(changes, "MaxPayload", oldOpts.MaxPayload, newOpts.MaxPayload);
|
|
CompareAndAdd(changes, "MaxPending", oldOpts.MaxPending, newOpts.MaxPending);
|
|
CompareAndAdd(changes, "WriteDeadline", oldOpts.WriteDeadline, newOpts.WriteDeadline);
|
|
CompareAndAdd(changes, "PingInterval", oldOpts.PingInterval, newOpts.PingInterval);
|
|
CompareAndAdd(changes, "MaxPingsOut", oldOpts.MaxPingsOut, newOpts.MaxPingsOut);
|
|
CompareAndAdd(changes, "MaxControlLine", oldOpts.MaxControlLine, newOpts.MaxControlLine);
|
|
CompareAndAdd(changes, "MaxSubs", oldOpts.MaxSubs, newOpts.MaxSubs);
|
|
CompareAndAdd(changes, "MaxSubTokens", oldOpts.MaxSubTokens, newOpts.MaxSubTokens);
|
|
CompareAndAdd(changes, "MaxTracedMsgLen", oldOpts.MaxTracedMsgLen, newOpts.MaxTracedMsgLen);
|
|
CompareAndAdd(changes, "MaxClosedClients", oldOpts.MaxClosedClients, newOpts.MaxClosedClients);
|
|
|
|
// Misc
|
|
CompareCollectionAndAdd(changes, "Tags", oldOpts.Tags, newOpts.Tags);
|
|
CompareAndAdd(changes, "LameDuckDuration", oldOpts.LameDuckDuration, newOpts.LameDuckDuration);
|
|
CompareAndAdd(changes, "LameDuckGracePeriod", oldOpts.LameDuckGracePeriod, newOpts.LameDuckGracePeriod);
|
|
CompareAndAdd(changes, "ClientAdvertise", oldOpts.ClientAdvertise, newOpts.ClientAdvertise);
|
|
CompareAndAdd(changes, "DisableSublistCache", oldOpts.DisableSublistCache, newOpts.DisableSublistCache);
|
|
CompareAndAdd(changes, "ConnectErrorReports", oldOpts.ConnectErrorReports, newOpts.ConnectErrorReports);
|
|
CompareAndAdd(changes, "ReconnectErrorReports", oldOpts.ReconnectErrorReports, newOpts.ReconnectErrorReports);
|
|
CompareAndAdd(changes, "NoHeaderSupport", oldOpts.NoHeaderSupport, newOpts.NoHeaderSupport);
|
|
CompareAndAdd(changes, "NoSystemAccount", oldOpts.NoSystemAccount, newOpts.NoSystemAccount);
|
|
CompareAndAdd(changes, "SystemAccount", oldOpts.SystemAccount, newOpts.SystemAccount);
|
|
|
|
// Cluster and JetStream (restart-required boundaries)
|
|
if (!ClusterEquivalent(oldOpts.Cluster, newOpts.Cluster))
|
|
changes.Add(new ConfigChange("Cluster", isNonReloadable: true));
|
|
|
|
if (JetStreamStoreDirChanged(oldOpts.JetStream, newOpts.JetStream))
|
|
changes.Add(new ConfigChange("JetStream.StoreDir", isNonReloadable: true));
|
|
|
|
return changes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a list of config changes and returns error messages for any
|
|
/// non-reloadable changes (properties that require a server restart).
|
|
/// </summary>
|
|
public static List<string> Validate(List<IConfigChange> changes)
|
|
{
|
|
var errors = new List<string>();
|
|
foreach (var change in changes)
|
|
{
|
|
if (change.IsNonReloadable)
|
|
{
|
|
errors.Add($"Config reload: '{change.Name}' cannot be changed at runtime (requires restart)");
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges CLI overrides into a freshly-parsed config so that command-line flags
|
|
/// always take precedence. Only properties whose names appear in <paramref name="cliFlags"/>
|
|
/// are copied from <paramref name="cliValues"/> to <paramref name="fromConfig"/>.
|
|
/// </summary>
|
|
public static void MergeCliOverrides(NatsOptions fromConfig, NatsOptions cliValues, HashSet<string> cliFlags)
|
|
{
|
|
foreach (var flag in cliFlags)
|
|
{
|
|
switch (flag)
|
|
{
|
|
// Non-reloadable
|
|
case "Host":
|
|
fromConfig.Host = cliValues.Host;
|
|
break;
|
|
case "Port":
|
|
fromConfig.Port = cliValues.Port;
|
|
break;
|
|
case "ServerName":
|
|
fromConfig.ServerName = cliValues.ServerName;
|
|
break;
|
|
|
|
// Logging
|
|
case "Debug":
|
|
fromConfig.Debug = cliValues.Debug;
|
|
break;
|
|
case "Trace":
|
|
fromConfig.Trace = cliValues.Trace;
|
|
break;
|
|
case "TraceVerbose":
|
|
fromConfig.TraceVerbose = cliValues.TraceVerbose;
|
|
break;
|
|
case "Logtime":
|
|
fromConfig.Logtime = cliValues.Logtime;
|
|
break;
|
|
case "LogtimeUTC":
|
|
fromConfig.LogtimeUTC = cliValues.LogtimeUTC;
|
|
break;
|
|
case "LogFile":
|
|
fromConfig.LogFile = cliValues.LogFile;
|
|
break;
|
|
case "LogSizeLimit":
|
|
fromConfig.LogSizeLimit = cliValues.LogSizeLimit;
|
|
break;
|
|
case "LogMaxFiles":
|
|
fromConfig.LogMaxFiles = cliValues.LogMaxFiles;
|
|
break;
|
|
case "Syslog":
|
|
fromConfig.Syslog = cliValues.Syslog;
|
|
break;
|
|
case "RemoteSyslog":
|
|
fromConfig.RemoteSyslog = cliValues.RemoteSyslog;
|
|
break;
|
|
|
|
// Auth
|
|
case "Username":
|
|
fromConfig.Username = cliValues.Username;
|
|
break;
|
|
case "Password":
|
|
fromConfig.Password = cliValues.Password;
|
|
break;
|
|
case "Authorization":
|
|
fromConfig.Authorization = cliValues.Authorization;
|
|
break;
|
|
case "Users":
|
|
fromConfig.Users = cliValues.Users;
|
|
break;
|
|
case "NKeys":
|
|
fromConfig.NKeys = cliValues.NKeys;
|
|
break;
|
|
case "NoAuthUser":
|
|
fromConfig.NoAuthUser = cliValues.NoAuthUser;
|
|
break;
|
|
case "AuthTimeout":
|
|
fromConfig.AuthTimeout = cliValues.AuthTimeout;
|
|
break;
|
|
|
|
// TLS
|
|
case "TlsCert":
|
|
fromConfig.TlsCert = cliValues.TlsCert;
|
|
break;
|
|
case "TlsKey":
|
|
fromConfig.TlsKey = cliValues.TlsKey;
|
|
break;
|
|
case "TlsCaCert":
|
|
fromConfig.TlsCaCert = cliValues.TlsCaCert;
|
|
break;
|
|
case "TlsVerify":
|
|
fromConfig.TlsVerify = cliValues.TlsVerify;
|
|
break;
|
|
case "TlsMap":
|
|
fromConfig.TlsMap = cliValues.TlsMap;
|
|
break;
|
|
case "TlsTimeout":
|
|
fromConfig.TlsTimeout = cliValues.TlsTimeout;
|
|
break;
|
|
case "TlsHandshakeFirst":
|
|
fromConfig.TlsHandshakeFirst = cliValues.TlsHandshakeFirst;
|
|
break;
|
|
case "TlsHandshakeFirstFallback":
|
|
fromConfig.TlsHandshakeFirstFallback = cliValues.TlsHandshakeFirstFallback;
|
|
break;
|
|
case "AllowNonTls":
|
|
fromConfig.AllowNonTls = cliValues.AllowNonTls;
|
|
break;
|
|
case "TlsRateLimit":
|
|
fromConfig.TlsRateLimit = cliValues.TlsRateLimit;
|
|
break;
|
|
case "TlsPinnedCerts":
|
|
fromConfig.TlsPinnedCerts = cliValues.TlsPinnedCerts;
|
|
break;
|
|
|
|
// Limits
|
|
case "MaxConnections":
|
|
fromConfig.MaxConnections = cliValues.MaxConnections;
|
|
break;
|
|
case "MaxPayload":
|
|
fromConfig.MaxPayload = cliValues.MaxPayload;
|
|
break;
|
|
case "MaxPending":
|
|
fromConfig.MaxPending = cliValues.MaxPending;
|
|
break;
|
|
case "WriteDeadline":
|
|
fromConfig.WriteDeadline = cliValues.WriteDeadline;
|
|
break;
|
|
case "PingInterval":
|
|
fromConfig.PingInterval = cliValues.PingInterval;
|
|
break;
|
|
case "MaxPingsOut":
|
|
fromConfig.MaxPingsOut = cliValues.MaxPingsOut;
|
|
break;
|
|
case "MaxControlLine":
|
|
fromConfig.MaxControlLine = cliValues.MaxControlLine;
|
|
break;
|
|
case "MaxSubs":
|
|
fromConfig.MaxSubs = cliValues.MaxSubs;
|
|
break;
|
|
case "MaxSubTokens":
|
|
fromConfig.MaxSubTokens = cliValues.MaxSubTokens;
|
|
break;
|
|
case "MaxTracedMsgLen":
|
|
fromConfig.MaxTracedMsgLen = cliValues.MaxTracedMsgLen;
|
|
break;
|
|
case "MaxClosedClients":
|
|
fromConfig.MaxClosedClients = cliValues.MaxClosedClients;
|
|
break;
|
|
|
|
// Misc
|
|
case "Tags":
|
|
fromConfig.Tags = cliValues.Tags;
|
|
break;
|
|
case "LameDuckDuration":
|
|
fromConfig.LameDuckDuration = cliValues.LameDuckDuration;
|
|
break;
|
|
case "LameDuckGracePeriod":
|
|
fromConfig.LameDuckGracePeriod = cliValues.LameDuckGracePeriod;
|
|
break;
|
|
case "ClientAdvertise":
|
|
fromConfig.ClientAdvertise = cliValues.ClientAdvertise;
|
|
break;
|
|
case "DisableSublistCache":
|
|
fromConfig.DisableSublistCache = cliValues.DisableSublistCache;
|
|
break;
|
|
case "ConnectErrorReports":
|
|
fromConfig.ConnectErrorReports = cliValues.ConnectErrorReports;
|
|
break;
|
|
case "ReconnectErrorReports":
|
|
fromConfig.ReconnectErrorReports = cliValues.ReconnectErrorReports;
|
|
break;
|
|
case "NoHeaderSupport":
|
|
fromConfig.NoHeaderSupport = cliValues.NoHeaderSupport;
|
|
break;
|
|
case "NoSystemAccount":
|
|
fromConfig.NoSystemAccount = cliValues.NoSystemAccount;
|
|
break;
|
|
case "SystemAccount":
|
|
fromConfig.SystemAccount = cliValues.SystemAccount;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Comparison helpers ─────────────────────────────────────────
|
|
|
|
private static void CompareAndAdd<T>(List<IConfigChange> changes, string name, T oldVal, T newVal)
|
|
{
|
|
if (!Equals(oldVal, newVal))
|
|
{
|
|
changes.Add(new ConfigChange(
|
|
name,
|
|
isLoggingChange: LoggingOptions.Contains(name),
|
|
isAuthChange: AuthOptions.Contains(name),
|
|
isTlsChange: TlsOptions.Contains(name),
|
|
isNonReloadable: NonReloadable.Contains(name)));
|
|
}
|
|
}
|
|
|
|
private static void CompareCollectionAndAdd<T>(List<IConfigChange> changes, string name, T? oldVal, T? newVal)
|
|
where T : class
|
|
{
|
|
// For collections we compare by reference and null state.
|
|
// A change from null to non-null (or vice versa), or a different reference, counts as changed.
|
|
if (ReferenceEquals(oldVal, newVal))
|
|
return;
|
|
|
|
if (oldVal is null || newVal is null || !ReferenceEquals(oldVal, newVal))
|
|
{
|
|
changes.Add(new ConfigChange(
|
|
name,
|
|
isLoggingChange: LoggingOptions.Contains(name),
|
|
isAuthChange: AuthOptions.Contains(name),
|
|
isTlsChange: TlsOptions.Contains(name),
|
|
isNonReloadable: NonReloadable.Contains(name)));
|
|
}
|
|
}
|
|
|
|
private static bool ClusterEquivalent(ClusterOptions? oldCluster, ClusterOptions? newCluster)
|
|
{
|
|
if (oldCluster is null && newCluster is null)
|
|
return true;
|
|
|
|
if (oldCluster is null || newCluster is null)
|
|
return false;
|
|
|
|
if (!string.Equals(oldCluster.Name, newCluster.Name, StringComparison.Ordinal))
|
|
return false;
|
|
|
|
if (!string.Equals(oldCluster.Host, newCluster.Host, StringComparison.Ordinal))
|
|
return false;
|
|
|
|
if (oldCluster.Port != newCluster.Port)
|
|
return false;
|
|
|
|
return oldCluster.Routes.SequenceEqual(newCluster.Routes, StringComparer.Ordinal);
|
|
}
|
|
|
|
private static bool JetStreamStoreDirChanged(JetStreamOptions? oldJetStream, JetStreamOptions? newJetStream)
|
|
{
|
|
if (oldJetStream is null && newJetStream is null)
|
|
return false;
|
|
|
|
if (oldJetStream is null || newJetStream is null)
|
|
return true;
|
|
|
|
return !string.Equals(oldJetStream.StoreDir, newJetStream.StoreDir, StringComparison.Ordinal);
|
|
}
|
|
}
|