product advisories, stella router improval, tests streghthening

This commit is contained in:
StellaOps Bot
2025-12-24 14:20:26 +02:00
parent 5540ce9430
commit 2c2bbf1005
171 changed files with 58943 additions and 135 deletions

View File

@@ -35,6 +35,61 @@ public sealed class GatewayTransportOptions
public GatewayTcpTransportOptions Tcp { get; set; } = new();
public GatewayTlsTransportOptions Tls { get; set; } = new();
public GatewayMessagingTransportOptions Messaging { get; set; } = new();
}
public sealed class GatewayMessagingTransportOptions
{
/// <summary>
/// Whether messaging (Valkey) transport is enabled.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Valkey connection string (e.g., "localhost:6379" or "valkey:6379,password=secret").
/// </summary>
public string ConnectionString { get; set; } = "localhost:6379";
/// <summary>
/// Valkey database number.
/// </summary>
public int? Database { get; set; }
/// <summary>
/// Queue name template for incoming requests. Use {service} placeholder.
/// </summary>
public string RequestQueueTemplate { get; set; } = "router:requests:{service}";
/// <summary>
/// Queue name for gateway responses.
/// </summary>
public string ResponseQueueName { get; set; } = "router:responses";
/// <summary>
/// Consumer group name for request processing.
/// </summary>
public string ConsumerGroup { get; set; } = "router-gateway";
/// <summary>
/// Timeout for RPC requests.
/// </summary>
public string RequestTimeout { get; set; } = "30s";
/// <summary>
/// Lease duration for message processing.
/// </summary>
public string LeaseDuration { get; set; } = "5m";
/// <summary>
/// Batch size for leasing messages.
/// </summary>
public int BatchSize { get; set; } = 10;
/// <summary>
/// Heartbeat interval.
/// </summary>
public string HeartbeatInterval { get; set; } = "10s";
}
public sealed class GatewayTcpTransportOptions

View File

@@ -21,6 +21,10 @@ using StellaOps.Router.Gateway.RateLimit;
using StellaOps.Router.Gateway.Routing;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
using StellaOps.Router.Transport.Messaging;
using StellaOps.Router.Transport.Messaging.Options;
using StellaOps.Messaging.DependencyInjection;
using StellaOps.Messaging.Transport.Valkey;
using StellaOps.Router.AspNet;
var builder = WebApplication.CreateBuilder(args);
@@ -53,6 +57,13 @@ builder.Services.AddSingleton<GatewayMetrics>();
builder.Services.AddTcpTransportServer();
builder.Services.AddTlsTransportServer();
// Messaging transport (Valkey)
if (bootstrapOptions.Transports.Messaging.Enabled)
{
builder.Services.AddMessagingTransport<ValkeyTransportPlugin>(builder.Configuration, "Gateway:Transports:Messaging");
builder.Services.AddMessagingTransportServer();
}
builder.Services.AddSingleton<GatewayTransportClient>();
builder.Services.AddSingleton<ITransportClient>(sp => sp.GetRequiredService<GatewayTransportClient>());
@@ -246,4 +257,25 @@ static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, Gatewa
options.RequireClientCertificate = tls.RequireClientCertificate;
options.AllowSelfSigned = tls.AllowSelfSigned;
});
builder.Services.AddOptions<MessagingTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.RequestQueueTemplate = messaging.RequestQueueTemplate;
options.ResponseQueueName = messaging.ResponseQueueName;
options.ConsumerGroup = messaging.ConsumerGroup;
options.RequestTimeout = GatewayValueParser.ParseDuration(messaging.RequestTimeout, TimeSpan.FromSeconds(30));
options.LeaseDuration = GatewayValueParser.ParseDuration(messaging.LeaseDuration, TimeSpan.FromMinutes(5));
options.BatchSize = messaging.BatchSize;
options.HeartbeatInterval = GatewayValueParser.ParseDuration(messaging.HeartbeatInterval, TimeSpan.FromSeconds(10));
});
builder.Services.AddOptions<ValkeyTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.ConnectionString = messaging.ConnectionString;
options.Database = messaging.Database;
});
}

View File

@@ -9,6 +9,7 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.OpenApi;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
using StellaOps.Router.Transport.Messaging;
namespace StellaOps.Gateway.WebService.Services;
@@ -16,6 +17,7 @@ public sealed class GatewayHostedService : IHostedService
{
private readonly TcpTransportServer _tcpServer;
private readonly TlsTransportServer _tlsServer;
private readonly MessagingTransportServer? _messagingServer;
private readonly IGlobalRoutingState _routingState;
private readonly GatewayTransportClient _transportClient;
private readonly IEffectiveClaimsStore _claimsStore;
@@ -26,6 +28,7 @@ public sealed class GatewayHostedService : IHostedService
private readonly JsonSerializerOptions _jsonOptions;
private bool _tcpEnabled;
private bool _tlsEnabled;
private bool _messagingEnabled;
public GatewayHostedService(
TcpTransportServer tcpServer,
@@ -36,10 +39,12 @@ public sealed class GatewayHostedService : IHostedService
IOptions<GatewayOptions> options,
GatewayServiceStatus status,
ILogger<GatewayHostedService> logger,
IRouterOpenApiDocumentCache? openApiCache = null)
IRouterOpenApiDocumentCache? openApiCache = null,
MessagingTransportServer? messagingServer = null)
{
_tcpServer = tcpServer;
_tlsServer = tlsServer;
_messagingServer = messagingServer;
_routingState = routingState;
_transportClient = transportClient;
_claimsStore = claimsStore;
@@ -59,8 +64,9 @@ public sealed class GatewayHostedService : IHostedService
var options = _options.Value;
_tcpEnabled = options.Transports.Tcp.Enabled;
_tlsEnabled = options.Transports.Tls.Enabled;
_messagingEnabled = options.Transports.Messaging.Enabled && _messagingServer is not null;
if (!_tcpEnabled && !_tlsEnabled)
if (!_tcpEnabled && !_tlsEnabled && !_messagingEnabled)
{
_logger.LogWarning("No transports enabled; gateway will not accept microservice connections.");
_status.MarkStarted();
@@ -84,6 +90,17 @@ public sealed class GatewayHostedService : IHostedService
_logger.LogInformation("TLS transport started on port {Port}", options.Transports.Tls.Port);
}
if (_messagingEnabled && _messagingServer is not null)
{
_messagingServer.OnHelloReceived += HandleMessagingHello;
_messagingServer.OnHeartbeatReceived += HandleMessagingHeartbeat;
_messagingServer.OnResponseReceived += HandleMessagingResponse;
_messagingServer.OnConnectionClosed += HandleMessagingDisconnection;
await _messagingServer.StartAsync(cancellationToken);
_logger.LogInformation("Messaging transport started (Valkey connection: {Connection})",
options.Transports.Messaging.ConnectionString);
}
_status.MarkStarted();
_status.MarkReady();
}
@@ -110,6 +127,15 @@ public sealed class GatewayHostedService : IHostedService
_tlsServer.OnFrame -= HandleTlsFrame;
_tlsServer.OnDisconnection -= HandleTlsDisconnection;
}
if (_messagingEnabled && _messagingServer is not null)
{
await _messagingServer.StopAsync(cancellationToken);
_messagingServer.OnHelloReceived -= HandleMessagingHello;
_messagingServer.OnHeartbeatReceived -= HandleMessagingHeartbeat;
_messagingServer.OnResponseReceived -= HandleMessagingResponse;
_messagingServer.OnConnectionClosed -= HandleMessagingDisconnection;
}
}
private void HandleTcpFrame(string connectionId, Frame frame)
@@ -438,8 +464,55 @@ public sealed class GatewayHostedService : IHostedService
{
_tlsServer.GetConnection(connectionId)?.Close();
}
// Messaging transport connections are managed by the queue system
// and do not support explicit close operations
}
#region Messaging Transport Event Handlers
private Task HandleMessagingHello(ConnectionState state, HelloPayload payload)
{
// The MessagingTransportServer already built the ConnectionState with TransportType.Messaging
// We need to add it to the routing state and update the claims store
_routingState.AddConnection(state);
_claimsStore.UpdateFromMicroservice(payload.Instance.ServiceName, payload.Endpoints);
_openApiCache?.Invalidate();
_logger.LogInformation(
"Messaging connection registered: {ConnectionId} service={ServiceName} version={Version}",
state.ConnectionId,
state.Instance.ServiceName,
state.Instance.Version);
return Task.CompletedTask;
}
private Task HandleMessagingHeartbeat(ConnectionState state, HeartbeatPayload payload)
{
_routingState.UpdateConnection(state.ConnectionId, conn =>
{
conn.LastHeartbeatUtc = DateTime.UtcNow;
conn.Status = payload.Status;
});
return Task.CompletedTask;
}
private Task HandleMessagingResponse(ConnectionState state, Frame frame)
{
_transportClient.HandleResponseFrame(frame);
return Task.CompletedTask;
}
private Task HandleMessagingDisconnection(string connectionId)
{
HandleDisconnect(connectionId);
return Task.CompletedTask;
}
#endregion
private sealed class EndpointKeyComparer : IEqualityComparer<(string Method, string Path)>
{
public bool Equals((string Method, string Path) x, (string Method, string Path) y)

View File

@@ -6,6 +6,7 @@ using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
using StellaOps.Router.Transport.Messaging;
namespace StellaOps.Gateway.WebService.Services;
@@ -13,6 +14,7 @@ public sealed class GatewayTransportClient : ITransportClient
{
private readonly TcpTransportServer _tcpServer;
private readonly TlsTransportServer _tlsServer;
private readonly MessagingTransportServer? _messagingServer;
private readonly ILogger<GatewayTransportClient> _logger;
private readonly ConcurrentDictionary<string, TaskCompletionSource<Frame>> _pendingRequests = new();
private readonly ConcurrentDictionary<string, Channel<Frame>> _streamingResponses = new();
@@ -20,10 +22,12 @@ public sealed class GatewayTransportClient : ITransportClient
public GatewayTransportClient(
TcpTransportServer tcpServer,
TlsTransportServer tlsServer,
ILogger<GatewayTransportClient> logger)
ILogger<GatewayTransportClient> logger,
MessagingTransportServer? messagingServer = null)
{
_tcpServer = tcpServer;
_tlsServer = tlsServer;
_messagingServer = messagingServer;
_logger = logger;
}
@@ -147,6 +151,13 @@ public sealed class GatewayTransportClient : ITransportClient
case TransportType.Certificate:
await _tlsServer.SendFrameAsync(connection.ConnectionId, frame, cancellationToken);
break;
case TransportType.Messaging:
if (_messagingServer is null)
{
throw new InvalidOperationException("Messaging transport is not enabled");
}
await _messagingServer.SendToMicroserviceAsync(connection.ConnectionId, frame, cancellationToken);
break;
default:
throw new NotSupportedException($"Transport type {connection.TransportType} is not supported by the gateway.");
}

View File

@@ -10,6 +10,9 @@
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Transport.Tcp\StellaOps.Router.Transport.Tcp.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Transport.Tls\StellaOps.Router.Transport.Tls.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Router.Transport.Messaging\StellaOps.Router.Transport.Messaging.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Messaging.Transport.Valkey\StellaOps.Messaging.Transport.Valkey.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Auth.Security\StellaOps.Auth.Security.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />

View File

@@ -0,0 +1,215 @@
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Gateway.WebService.Configuration;
using GatewayClaimsStore = StellaOps.Gateway.WebService.Authorization.IEffectiveClaimsStore;
using StellaOps.Gateway.WebService.Services;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.Messaging;
using StellaOps.Router.Transport.Messaging.Options;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
namespace StellaOps.Gateway.WebService.Tests.Integration;
/// <summary>
/// Unit tests for the messaging transport integration in GatewayHostedService and GatewayTransportClient.
/// These tests verify the wiring and event handling without requiring a real Valkey instance.
/// </summary>
public sealed class MessagingTransportIntegrationTests
{
private readonly JsonSerializerOptions _jsonOptions;
public MessagingTransportIntegrationTests()
{
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
}
[Fact]
public void GatewayHostedService_CanAcceptMessagingServer()
{
// Arrange
var mockQueueFactory = new Mock<IMessageQueueFactory>();
var messagingOptions = Options.Create(new MessagingTransportOptions());
var messagingServer = new MessagingTransportServer(
mockQueueFactory.Object,
messagingOptions,
NullLogger<MessagingTransportServer>.Instance);
var gatewayOptions = Options.Create(new GatewayOptions());
var routingState = new Mock<IGlobalRoutingState>();
var claimsStore = new Mock<GatewayClaimsStore>();
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29100 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29443 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer);
// Act & Assert - construction should succeed with messaging server
var hostedService = new GatewayHostedService(
tcpServer,
tlsServer,
routingState.Object,
transportClient,
claimsStore.Object,
gatewayOptions,
new GatewayServiceStatus(),
NullLogger<GatewayHostedService>.Instance,
openApiCache: null,
messagingServer: messagingServer);
Assert.NotNull(hostedService);
}
[Fact]
public void GatewayHostedService_CanAcceptNullMessagingServer()
{
// Arrange
var gatewayOptions = Options.Create(new GatewayOptions());
var routingState = new Mock<IGlobalRoutingState>();
var claimsStore = new Mock<GatewayClaimsStore>();
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29101 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29444 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer: null);
// Act & Assert - construction should succeed without messaging server
var hostedService = new GatewayHostedService(
tcpServer,
tlsServer,
routingState.Object,
transportClient,
claimsStore.Object,
gatewayOptions,
new GatewayServiceStatus(),
NullLogger<GatewayHostedService>.Instance,
openApiCache: null,
messagingServer: null);
Assert.NotNull(hostedService);
}
[Fact]
public void GatewayTransportClient_WithMessagingServer_CanBeConstructed()
{
// Arrange
var mockQueueFactory = new Mock<IMessageQueueFactory>();
var messagingOptions = Options.Create(new MessagingTransportOptions());
var messagingServer = new MessagingTransportServer(
mockQueueFactory.Object,
messagingOptions,
NullLogger<MessagingTransportServer>.Instance);
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29102 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29445 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
// Act
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer);
// Assert
Assert.NotNull(transportClient);
}
[Fact]
public async Task GatewayTransportClient_SendToMessagingConnection_ThrowsWhenServerNull()
{
// Arrange
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29103 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29446 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
// No messaging server provided
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer: null);
var connection = new ConnectionState
{
ConnectionId = "msg-conn-001",
Instance = new InstanceDescriptor
{
InstanceId = "test-001",
ServiceName = "test-service",
Version = "1.0.0",
Region = "test"
},
TransportType = TransportType.Messaging
};
var frame = new Frame
{
Type = FrameType.Request,
CorrelationId = Guid.NewGuid().ToString("N"),
Payload = new byte[] { 1, 2, 3 }
};
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await transportClient.SendRequestAsync(connection, frame, TimeSpan.FromSeconds(5), CancellationToken.None));
}
[Fact]
public void GatewayOptions_MessagingTransport_HasCorrectDefaults()
{
// Arrange & Act
var options = new GatewayMessagingTransportOptions();
// Assert
Assert.False(options.Enabled);
Assert.Equal("localhost:6379", options.ConnectionString);
Assert.Null(options.Database);
Assert.Equal("router:requests:{service}", options.RequestQueueTemplate);
Assert.Equal("router:responses", options.ResponseQueueName);
Assert.Equal("router-gateway", options.ConsumerGroup);
Assert.Equal("30s", options.RequestTimeout);
Assert.Equal("5m", options.LeaseDuration);
Assert.Equal(10, options.BatchSize);
Assert.Equal("10s", options.HeartbeatInterval);
}
[Fact]
public void GatewayTransportOptions_IncludesMessaging()
{
// Arrange & Act
var options = new GatewayTransportOptions();
// Assert
Assert.NotNull(options.Tcp);
Assert.NotNull(options.Tls);
Assert.NotNull(options.Messaging);
}
}

View File

@@ -29,6 +29,8 @@
<ProjectReference Include="..\..\StellaOps.Gateway.WebService\StellaOps.Gateway.WebService.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Router.Transport.Messaging\StellaOps.Router.Transport.Messaging.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
</ItemGroup>
</Project>