Phase 8: Production readiness — failover tests, security hardening, sandboxing, deployment docs
- WP-1-3: Central/site failover + dual-node recovery tests (17 tests) - WP-4: Performance testing framework for target scale (7 tests) - WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests) - WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs) - WP-7: Recovery drill test scaffolds (5 tests) - WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests) - WP-9: Message contract compatibility (forward/backward compat) (18 tests) - WP-10: Deployment packaging (installation guide, production checklist, topology) - WP-11: Operational runbooks (failover, troubleshooting, maintenance) 92 new tests, all passing. Zero warnings.
This commit is contained in:
162
src/ScadaLink.InboundAPI/RouteHelper.cs
Normal file
162
src/ScadaLink.InboundAPI/RouteHelper.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Messages.InboundApi;
|
||||
using ScadaLink.Communication;
|
||||
|
||||
namespace ScadaLink.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Route.To() helper for cross-site calls from inbound API scripts.
|
||||
/// Resolves instance to site, routes via CommunicationService, blocks until response or timeout.
|
||||
/// Site unreachable returns error (no store-and-forward).
|
||||
/// </summary>
|
||||
public class RouteHelper
|
||||
{
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly CommunicationService _communicationService;
|
||||
|
||||
public RouteHelper(
|
||||
IInstanceLocator instanceLocator,
|
||||
CommunicationService communicationService)
|
||||
{
|
||||
_instanceLocator = instanceLocator;
|
||||
_communicationService = communicationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a route target for the specified instance.
|
||||
/// </summary>
|
||||
public RouteTarget To(string instanceCode)
|
||||
{
|
||||
return new RouteTarget(instanceCode, _instanceLocator, _communicationService);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-4: Represents a route target (an instance) for cross-site calls.
|
||||
/// </summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
private readonly string _instanceCode;
|
||||
private readonly IInstanceLocator _instanceLocator;
|
||||
private readonly CommunicationService _communicationService;
|
||||
|
||||
internal RouteTarget(
|
||||
string instanceCode,
|
||||
IInstanceLocator instanceLocator,
|
||||
CommunicationService communicationService)
|
||||
{
|
||||
_instanceCode = instanceCode;
|
||||
_instanceLocator = instanceLocator;
|
||||
_communicationService = communicationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a script on the remote instance. Synchronous from API caller's perspective.
|
||||
/// </summary>
|
||||
public async Task<object?> Call(
|
||||
string scriptName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToCallRequest(
|
||||
correlationId, _instanceCode, scriptName, parameters, DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToCallAsync(
|
||||
siteId, request, cancellationToken);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote script call failed");
|
||||
}
|
||||
|
||||
return response.ReturnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single attribute value from the remote instance.
|
||||
/// </summary>
|
||||
public async Task<object?> GetAttribute(
|
||||
string attributeName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await GetAttributes(new[] { attributeName }, cancellationToken);
|
||||
return result.TryGetValue(attributeName, out var value) ? value : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple attribute values from the remote instance (batch read).
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
||||
IEnumerable<string> attributeNames,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToGetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeNames.ToList(), DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToGetAttributesAsync(
|
||||
siteId, request, cancellationToken);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote attribute read failed");
|
||||
}
|
||||
|
||||
return response.Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single attribute value on the remote instance.
|
||||
/// </summary>
|
||||
public async Task SetAttribute(
|
||||
string attributeName,
|
||||
string value,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SetAttributes(
|
||||
new Dictionary<string, string> { { attributeName, value } },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets multiple attribute values on the remote instance (batch write).
|
||||
/// </summary>
|
||||
public async Task SetAttributes(
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var siteId = await ResolveSiteAsync(cancellationToken);
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var request = new RouteToSetAttributesRequest(
|
||||
correlationId, _instanceCode, attributeValues, DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _communicationService.RouteToSetAttributesAsync(
|
||||
siteId, request, cancellationToken);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
response.ErrorMessage ?? "Remote attribute write failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveSiteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var siteId = await _instanceLocator.GetSiteIdForInstanceAsync(_instanceCode, cancellationToken);
|
||||
if (siteId == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Instance '{_instanceCode}' not found or has no assigned site");
|
||||
}
|
||||
|
||||
return siteId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user