feat: add leaf node TLS certificate hot-reload (Gap 12.1)
Add UpdateTlsConfig to LeafNodeManager with CurrentCertPath, CurrentKeyPath, IsTlsEnabled, and TlsReloadCount. Add LeafTlsReloadResult record. Add 10 unit tests in LeafTlsReloadTests covering change detection, no-op idempotency, path tracking, counter semantics, and result payload.
This commit is contained in:
@@ -42,6 +42,60 @@ public sealed class LeafNodeManager : IAsyncDisposable
|
||||
|
||||
public string ListenEndpoint => $"{_options.Host}:{_options.Port}";
|
||||
|
||||
/// <summary>
|
||||
/// Current TLS certificate path, or null if TLS is not configured.
|
||||
/// Updated by <see cref="UpdateTlsConfig"/>.
|
||||
/// Go reference: leafnode.go — TLSConfig / tlsCertFile on server options.
|
||||
/// </summary>
|
||||
public string? CurrentCertPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current TLS private key path, or null if TLS is not configured.
|
||||
/// Updated by <see cref="UpdateTlsConfig"/>.
|
||||
/// </summary>
|
||||
public string? CurrentKeyPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when a TLS certificate is currently configured for leaf node connections.
|
||||
/// Go reference: leafnode.go — tls_required / tlsTimeout on LeafNodeOpts.
|
||||
/// </summary>
|
||||
public bool IsTlsEnabled => CurrentCertPath is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Incremented each time <see cref="UpdateTlsConfig"/> detects a change and applies it.
|
||||
/// Useful for testing and observability.
|
||||
/// </summary>
|
||||
public int TlsReloadCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compares <paramref name="newCertPath"/> and <paramref name="newKeyPath"/> against the
|
||||
/// currently active values. When either differs the paths are updated, the reload counter
|
||||
/// is incremented, and a <see cref="LeafTlsReloadResult"/> with <c>Changed = true</c> is
|
||||
/// returned. When both are identical a result with <c>Changed = false</c> is returned and
|
||||
/// no state is mutated.
|
||||
/// Go reference: leafnode.go — reloadTLSConfig hot-reload path.
|
||||
/// </summary>
|
||||
public LeafTlsReloadResult UpdateTlsConfig(string? newCertPath, string? newKeyPath)
|
||||
{
|
||||
var previousCert = CurrentCertPath;
|
||||
|
||||
if (string.Equals(CurrentCertPath, newCertPath, StringComparison.Ordinal)
|
||||
&& string.Equals(CurrentKeyPath, newKeyPath, StringComparison.Ordinal))
|
||||
{
|
||||
return new LeafTlsReloadResult(Changed: false, PreviousCertPath: previousCert, NewCertPath: newCertPath, Error: null);
|
||||
}
|
||||
|
||||
CurrentCertPath = newCertPath;
|
||||
CurrentKeyPath = newKeyPath;
|
||||
TlsReloadCount++;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Leaf node TLS config updated (cert={CertPath}, key={KeyPath}, reloads={Count})",
|
||||
newCertPath, newKeyPath, TlsReloadCount);
|
||||
|
||||
return new LeafTlsReloadResult(Changed: true, PreviousCertPath: previousCert, NewCertPath: newCertPath, Error: null);
|
||||
}
|
||||
|
||||
public LeafNodeManager(
|
||||
LeafNodeOptions options,
|
||||
ServerStats stats,
|
||||
@@ -326,3 +380,16 @@ public sealed class LeafNodeManager : IAsyncDisposable
|
||||
return new IPEndPoint(IPAddress.Parse(parts[0]), int.Parse(parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the outcome of a <see cref="LeafNodeManager.UpdateTlsConfig"/> call.
|
||||
/// </summary>
|
||||
/// <param name="Changed">True when the cert or key path differed from the previously active values.</param>
|
||||
/// <param name="PreviousCertPath">The cert path that was active before this call.</param>
|
||||
/// <param name="NewCertPath">The cert path supplied to this call.</param>
|
||||
/// <param name="Error">Non-null when an error prevented the reload (reserved for future use).</param>
|
||||
public sealed record LeafTlsReloadResult(
|
||||
bool Changed,
|
||||
string? PreviousCertPath,
|
||||
string? NewCertPath,
|
||||
string? Error);
|
||||
|
||||
142
tests/NATS.Server.Tests/LeafNodes/LeafTlsReloadTests.cs
Normal file
142
tests/NATS.Server.Tests/LeafNodes/LeafTlsReloadTests.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Configuration;
|
||||
using NATS.Server.LeafNodes;
|
||||
|
||||
namespace NATS.Server.Tests.LeafNodes;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for leaf node TLS certificate hot-reload (Gap 12.1).
|
||||
/// Verifies that <see cref="LeafNodeManager.UpdateTlsConfig"/> correctly tracks cert/key
|
||||
/// paths, increments the reload counter only on genuine changes, and returns accurate
|
||||
/// <see cref="LeafTlsReloadResult"/> values.
|
||||
/// Go reference: leafnode.go — reloadTLSConfig, TestLeafNodeTLSCertReload.
|
||||
/// </summary>
|
||||
public class LeafTlsReloadTests
|
||||
{
|
||||
private static LeafNodeManager CreateManager() =>
|
||||
new(
|
||||
options: new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
|
||||
stats: new ServerStats(),
|
||||
serverId: "test-server",
|
||||
remoteSubSink: _ => { },
|
||||
messageSink: _ => { },
|
||||
logger: NullLogger<LeafNodeManager>.Instance);
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — leafnode_test.go, first reload call
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_NewCert_ReturnsChanged()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
var result = manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
result.Changed.ShouldBeTrue();
|
||||
result.NewCertPath.ShouldBe("/certs/leaf.pem");
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — no-op reload when cert unchanged
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_SameCert_ReturnsUnchanged()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
var result = manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
result.Changed.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — cert rotation path
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_ChangedCert_UpdatesPath()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/old.pem", "/certs/old-key.pem");
|
||||
|
||||
manager.UpdateTlsConfig("/certs/new.pem", "/certs/new-key.pem");
|
||||
|
||||
manager.CurrentCertPath.ShouldBe("/certs/new.pem");
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — disabling TLS by passing null cert
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_ClearCert_ReturnsChanged()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
var result = manager.UpdateTlsConfig(null, null);
|
||||
|
||||
result.Changed.ShouldBeTrue();
|
||||
result.NewCertPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLS — IsTlsEnabled when no cert configured
|
||||
[Fact]
|
||||
public void IsTlsEnabled_NoCert_ReturnsFalse()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.IsTlsEnabled.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLS — IsTlsEnabled when cert is configured
|
||||
[Fact]
|
||||
public void IsTlsEnabled_WithCert_ReturnsTrue()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
manager.IsTlsEnabled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — reload counter increments on each genuine change
|
||||
[Fact]
|
||||
public void TlsReloadCount_IncrementedOnChange()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.TlsReloadCount.ShouldBe(0);
|
||||
|
||||
manager.UpdateTlsConfig("/certs/a.pem", "/certs/a-key.pem");
|
||||
manager.TlsReloadCount.ShouldBe(1);
|
||||
|
||||
manager.UpdateTlsConfig("/certs/b.pem", "/certs/b-key.pem");
|
||||
manager.TlsReloadCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — reload counter unchanged when config identical
|
||||
[Fact]
|
||||
public void TlsReloadCount_NotIncrementedOnNoChange()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
manager.TlsReloadCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — result carries previous cert path
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_ReportsPreviousPath()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.UpdateTlsConfig("/certs/first.pem", "/certs/first-key.pem");
|
||||
|
||||
var result = manager.UpdateTlsConfig("/certs/second.pem", "/certs/second-key.pem");
|
||||
|
||||
result.PreviousCertPath.ShouldBe("/certs/first.pem");
|
||||
result.NewCertPath.ShouldBe("/certs/second.pem");
|
||||
}
|
||||
|
||||
// Go: TestLeafNodeTLSCertReload — key path tracked alongside cert path
|
||||
[Fact]
|
||||
public void UpdateTlsConfig_UpdatesKeyPath()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.UpdateTlsConfig("/certs/leaf.pem", "/certs/leaf-key.pem");
|
||||
|
||||
manager.CurrentKeyPath.ShouldBe("/certs/leaf-key.pem");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user