// 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; /// /// Provides static methods for comparing two instances, /// validating that detected changes are reloadable, and merging CLI overrides /// so that command-line flags always take precedence over config file values. /// public static class ConfigReloader { // Non-reloadable options (match Go server — Host, Port, ServerName require restart) private static readonly HashSet NonReloadable = ["Host", "Port", "ServerName"]; // Logging-related options private static readonly HashSet LoggingOptions = ["Debug", "Trace", "TraceVerbose", "Logtime", "LogtimeUTC", "LogFile", "LogSizeLimit", "LogMaxFiles", "Syslog", "RemoteSyslog"]; // Auth-related options private static readonly HashSet AuthOptions = ["Username", "Password", "Authorization", "Users", "NKeys", "NoAuthUser", "AuthTimeout"]; // TLS-related options private static readonly HashSet TlsOptions = ["TlsCert", "TlsKey", "TlsCaCert", "TlsVerify", "TlsMap", "TlsTimeout", "TlsHandshakeFirst", "TlsHandshakeFirstFallback", "AllowNonTls", "TlsRateLimit", "TlsPinnedCerts"]; /// /// Compares two instances property by property and returns /// a list of for every property that differs. Each change /// is tagged with the appropriate category flags. /// public static List Diff(NatsOptions oldOpts, NatsOptions newOpts) { var changes = new List(); // 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); return changes; } /// /// Validates a list of config changes and returns error messages for any /// non-reloadable changes (properties that require a server restart). /// public static List Validate(List changes) { var errors = new List(); foreach (var change in changes) { if (change.IsNonReloadable) { errors.Add($"Config reload: '{change.Name}' cannot be changed at runtime (requires restart)"); } } return errors; } /// /// Merges CLI overrides into a freshly-parsed config so that command-line flags /// always take precedence. Only properties whose names appear in /// are copied from to . /// public static void MergeCliOverrides(NatsOptions fromConfig, NatsOptions cliValues, HashSet 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(List 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(List 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))); } } }