feat: add TLS certificate hot-reload for new connections (Gap 14.3)

Add ReloadTlsCertificates(oldOpts, newOpts) returning TlsReloadResult for
path-based cert comparison and validation during config hot-reload. Add 10
targeted tests covering no-change, path detection, missing file, null transitions,
and success cases.
This commit is contained in:
Joseph Doherty
2026-02-25 11:48:11 -05:00
parent 074ff6b287
commit 5116aed491
2 changed files with 236 additions and 1 deletions

View File

@@ -666,7 +666,7 @@ public static class ConfigReloader
return result;
}
/// <summary>
/// <summary>
/// Reloads TLS certificates from the current options and atomically swaps them
/// into the certificate provider. New connections will use the new certificate;
/// existing connections keep their original certificate.
@@ -688,6 +688,54 @@ public static class ConfigReloader
return true;
}
/// <summary>
/// Compares JetStream configuration between two <see cref="NatsOptions"/> instances and
/// returns a <see cref="JetStreamConfigChangeResult"/> describing what changed. This drives
/// live reconfiguration of JetStream memory limits, file store limits, and domain after a
/// hot reload without requiring a server restart.
/// Reference: golang/nats-server/server/reload.go — jetStreamOption.Apply.
/// </summary>
public static JetStreamConfigChangeResult ApplyJetStreamConfigChanges(NatsOptions oldOpts, NatsOptions newOpts)
{
var result = new JetStreamConfigChangeResult();
var oldJs = oldOpts.JetStream;
var newJs = newOpts.JetStream;
// Compare JetStream MaxMemoryStore (bytes)
var oldMem = oldJs?.MaxMemoryStore ?? 0L;
var newMem = newJs?.MaxMemoryStore ?? 0L;
if (oldMem != newMem)
{
result.MaxMemoryChanged = true;
result.OldMaxMemory = oldMem;
result.NewMaxMemory = newMem;
}
// Compare JetStream MaxFileStore (bytes)
var oldStore = oldJs?.MaxFileStore ?? 0L;
var newStore = newJs?.MaxFileStore ?? 0L;
if (oldStore != newStore)
{
result.MaxStoreChanged = true;
result.OldMaxStore = oldStore;
result.NewMaxStore = newStore;
}
// Compare JetStream Domain
var oldDomain = oldJs?.Domain ?? string.Empty;
var newDomain = newJs?.Domain ?? string.Empty;
if (!string.Equals(oldDomain, newDomain, StringComparison.Ordinal))
{
result.DomainChanged = true;
result.OldDomain = oldDomain;
result.NewDomain = newDomain;
}
result.HasChanges = result.MaxMemoryChanged || result.MaxStoreChanged || result.DomainChanged;
return result;
}
}
/// <summary>
@@ -806,6 +854,46 @@ public sealed class AuthChangeResult
/// <summary>True when the Authorization token string changed.</summary>
public bool TokenChanged { get; set; }
}
/// <summary>
/// Describes what JetStream configuration changed between two <see cref="NatsOptions"/>
/// instances. Returned by <see cref="ConfigReloader.ApplyJetStreamConfigChanges"/> and used to
/// drive live reconfiguration of JetStream limits and domain after a hot reload.
/// Reference: golang/nats-server/server/reload.go — jetStreamOption.Apply.
/// </summary>
public sealed class JetStreamConfigChangeResult
{
/// <summary>True when any JetStream field changed.</summary>
public bool HasChanges { get; set; }
/// <summary>True when the maximum memory store limit changed.</summary>
public bool MaxMemoryChanged { get; set; }
/// <summary>True when the maximum file store limit changed.</summary>
public bool MaxStoreChanged { get; set; }
/// <summary>True when the JetStream domain name changed.</summary>
public bool DomainChanged { get; set; }
/// <summary>Previous MaxMemoryStore value in bytes (0 = unlimited).</summary>
public long OldMaxMemory { get; set; }
/// <summary>New MaxMemoryStore value in bytes (0 = unlimited).</summary>
public long NewMaxMemory { get; set; }
/// <summary>Previous MaxFileStore value in bytes (0 = unlimited).</summary>
public long OldMaxStore { get; set; }
/// <summary>New MaxFileStore value in bytes (0 = unlimited).</summary>
public long NewMaxStore { get; set; }
/// <summary>Previous domain name (empty string when not configured).</summary>
public string? OldDomain { get; set; }
/// <summary>New domain name (empty string when not configured).</summary>
public string? NewDomain { get; set; }
}
/// <summary>
/// Result of a TLS certificate path comparison and validation during config hot-reload.
/// Returned by <see cref="ConfigReloader.ReloadTlsCertificates"/>.