1424a21419
Add a secret:NAME arm to GalaxySecretRef.ResolveApiKey that resolves the Galaxy gateway API key through the shared ISecretResolver — fail-closed if the secret is absent (never falls through to the cleartext literal arm), retiring the dev:/literal in-DB path for production. Because GetAsync is async the method becomes ResolveApiKeyAsync; the await cascade threads ISecretResolver by ctor injection into GalaxyDriver + GalaxyDriverBrowser and (since GalaxyDriver is built by a static factory closure, not DI) through GalaxyDriverFactoryExtensions + DriverFactoryBootstrap (which pulls the real resolver from the service provider — registered unconditionally in Slice 1). A NullSecretResolver null-object backs the parse-only/test paths only; the runtime path always gets the real resolver (verified end-to-end). TDD: 3 new secret:-arm tests (resolve / fail-closed-on-absent / no-literal-warning) RED without the arm, GREEN with it; 338 Galaxy tests pass; no sync-over-async.
188 lines
10 KiB
C#
188 lines
10 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
|
|
|
/// <summary>
|
|
/// Static factory registration helper for <see cref="GalaxyDriver"/>. Mirrors
|
|
/// <c>GalaxyProxyDriverFactoryExtensions</c> / <c>ModbusDriverFactoryExtensions</c>.
|
|
/// Server's <c>Program.cs</c> calls <see cref="Register"/> once at startup; the driver
|
|
/// bootstrap pipeline materialises DriverInstance rows whose <c>DriverType</c> matches
|
|
/// <see cref="DriverTypeName"/> into live <see cref="GalaxyDriver"/> instances.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <c>"GalaxyMxGateway"</c> is the sole Galaxy driver-type name. The legacy
|
|
/// <c>"Galaxy"</c> proxy type (retired in PR 7.2) no longer exists; there is no
|
|
/// server-side backend switch — every Galaxy DriverInstance row uses this type name.
|
|
/// </remarks>
|
|
public static class GalaxyDriverFactoryExtensions
|
|
{
|
|
public const string DriverTypeName = "GalaxyMxGateway";
|
|
|
|
/// <summary>Registers the Galaxy driver factory with the given registry, secret resolver, and optional logger factory.</summary>
|
|
/// <param name="registry">The driver factory registry to register with.</param>
|
|
/// <param name="secretResolver">
|
|
/// The shared secret resolver (from DI) used to resolve the <c>secret:</c> arm of
|
|
/// each instance's <c>Gateway.ApiKeySecretRef</c>.
|
|
/// </param>
|
|
/// <param name="loggerFactory">The optional logger factory for creating drivers.</param>
|
|
public static void Register(
|
|
DriverFactoryRegistry registry,
|
|
ISecretResolver secretResolver,
|
|
ILoggerFactory? loggerFactory = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
ArgumentNullException.ThrowIfNull(secretResolver);
|
|
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, secretResolver));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience for tests + standalone callers. Uses the <see cref="NullSecretResolver"/>
|
|
/// null-object, so the <c>secret:</c> API-key arm resolves fail-closed (absent) — callers
|
|
/// that need a working <c>secret:</c> ref must use the <see cref="Register"/> path that
|
|
/// threads the DI resolver.
|
|
/// </summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The driver configuration in JSON format.</param>
|
|
/// <returns>The constructed Galaxy driver instance.</returns>
|
|
public static GalaxyDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
|
=> CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null, NullSecretResolver.Instance);
|
|
|
|
/// <summary>Creates a Galaxy driver instance from configuration JSON with optional logger factory and a secret resolver.</summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The driver configuration in JSON format.</param>
|
|
/// <param name="loggerFactory">The optional logger factory for creating drivers.</param>
|
|
/// <param name="secretResolver">The secret resolver for the driver's <c>secret:</c> API-key arm.</param>
|
|
/// <returns>The constructed Galaxy driver instance.</returns>
|
|
public static GalaxyDriver CreateInstance(
|
|
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory, ISecretResolver secretResolver)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(secretResolver);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
|
|
|
var dto = JsonSerializer.Deserialize<GalaxyDriverConfigDto>(driverConfigJson, JsonOptions)
|
|
?? throw new InvalidOperationException(
|
|
$"Galaxy driver config for '{driverInstanceId}' deserialised to null");
|
|
|
|
var options = new GalaxyDriverOptions(
|
|
Gateway: new GalaxyGatewayOptions(
|
|
Endpoint: dto.Gateway?.Endpoint
|
|
?? throw new InvalidOperationException(
|
|
$"Galaxy driver '{driverInstanceId}' missing required Gateway.Endpoint"),
|
|
ApiKeySecretRef: dto.Gateway.ApiKeySecretRef
|
|
?? throw new InvalidOperationException(
|
|
$"Galaxy driver '{driverInstanceId}' missing required Gateway.ApiKeySecretRef"),
|
|
UseTls: dto.Gateway.UseTls ?? true,
|
|
CaCertificatePath: dto.Gateway.CaCertificatePath,
|
|
ConnectTimeoutSeconds: dto.Gateway.ConnectTimeoutSeconds ?? 10,
|
|
DefaultCallTimeoutSeconds: dto.Gateway.DefaultCallTimeoutSeconds ?? 30,
|
|
StreamTimeoutSeconds: dto.Gateway.StreamTimeoutSeconds ?? 0),
|
|
MxAccess: new GalaxyMxAccessOptions(
|
|
ClientName: dto.MxAccess?.ClientName
|
|
?? throw new InvalidOperationException(
|
|
$"Galaxy driver '{driverInstanceId}' missing required MxAccess.ClientName"),
|
|
PublishingIntervalMs: dto.MxAccess.PublishingIntervalMs ?? 1000,
|
|
WriteUserId: dto.MxAccess.WriteUserId ?? 0,
|
|
EventPumpChannelCapacity: dto.MxAccess.EventPumpChannelCapacity ?? 50_000),
|
|
Repository: new GalaxyRepositoryOptions(
|
|
DiscoverPageSize: dto.Repository?.DiscoverPageSize ?? 5000,
|
|
WatchDeployEvents: dto.Repository?.WatchDeployEvents ?? true),
|
|
Reconnect: new GalaxyReconnectOptions(
|
|
InitialBackoffMs: dto.Reconnect?.InitialBackoffMs ?? 500,
|
|
MaxBackoffMs: dto.Reconnect?.MaxBackoffMs ?? 30_000,
|
|
ReplayOnSessionLost: dto.Reconnect?.ReplayOnSessionLost ?? true))
|
|
{
|
|
// v3: the deploy artifact delivers the authored raw tags (RawPath identity + TagConfig blob
|
|
// carrying the camelCase "attributeRef" + WriteIdempotent flag). The driver builds its
|
|
// RawPath → attributeRef table from these. Empty when the config omits them.
|
|
RawTags = dto.RawTags ?? [],
|
|
};
|
|
|
|
return new GalaxyDriver(driverInstanceId, options, secretResolver, loggerFactory?.CreateLogger<GalaxyDriver>());
|
|
}
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
AllowTrailingCommas = true,
|
|
};
|
|
|
|
/// <summary>Data transfer object for Galaxy driver configuration JSON.</summary>
|
|
internal sealed class GalaxyDriverConfigDto
|
|
{
|
|
/// <summary>Gets or sets the gateway configuration.</summary>
|
|
public GatewayDto? Gateway { get; init; }
|
|
/// <summary>Gets or sets the MX Access configuration.</summary>
|
|
public MxAccessDto? MxAccess { get; init; }
|
|
/// <summary>Gets or sets the repository configuration.</summary>
|
|
public RepositoryDto? Repository { get; init; }
|
|
/// <summary>Gets or sets the reconnect configuration.</summary>
|
|
public ReconnectDto? Reconnect { get; init; }
|
|
/// <summary>
|
|
/// Gets or sets the v3 authored raw tags. Each <see cref="RawTagEntry"/> carries the RawPath
|
|
/// identity, the driver <c>TagConfig</c> blob (with the Galaxy <c>attributeRef</c>), and the
|
|
/// WriteIdempotent flag. Bound straight through to <see cref="GalaxyDriverOptions.RawTags"/>.
|
|
/// </summary>
|
|
public List<RawTagEntry>? RawTags { get; init; }
|
|
}
|
|
|
|
/// <summary>Gateway configuration section.</summary>
|
|
internal sealed class GatewayDto
|
|
{
|
|
/// <summary>Gets or sets the gateway endpoint address.</summary>
|
|
public string? Endpoint { get; init; }
|
|
/// <summary>Gets or sets the API key secret reference.</summary>
|
|
public string? ApiKeySecretRef { get; init; }
|
|
/// <summary>Gets or sets whether to use TLS.</summary>
|
|
public bool? UseTls { get; init; }
|
|
/// <summary>Gets or sets the CA certificate path.</summary>
|
|
public string? CaCertificatePath { get; init; }
|
|
/// <summary>Gets or sets the connection timeout in seconds.</summary>
|
|
public int? ConnectTimeoutSeconds { get; init; }
|
|
/// <summary>Gets or sets the default call timeout in seconds.</summary>
|
|
public int? DefaultCallTimeoutSeconds { get; init; }
|
|
/// <summary>Gets or sets the stream timeout in seconds.</summary>
|
|
public int? StreamTimeoutSeconds { get; init; }
|
|
}
|
|
|
|
/// <summary>MX Access configuration section.</summary>
|
|
internal sealed class MxAccessDto
|
|
{
|
|
/// <summary>Gets or sets the client name.</summary>
|
|
public string? ClientName { get; init; }
|
|
/// <summary>Gets or sets the publishing interval in milliseconds.</summary>
|
|
public int? PublishingIntervalMs { get; init; }
|
|
/// <summary>Gets or sets the write user ID.</summary>
|
|
public int? WriteUserId { get; init; }
|
|
/// <summary>Gets or sets the event pump channel capacity.</summary>
|
|
public int? EventPumpChannelCapacity { get; init; }
|
|
}
|
|
|
|
/// <summary>Repository configuration section.</summary>
|
|
internal sealed class RepositoryDto
|
|
{
|
|
/// <summary>Gets or sets the discover page size.</summary>
|
|
public int? DiscoverPageSize { get; init; }
|
|
/// <summary>Gets or sets whether to watch deploy events.</summary>
|
|
public bool? WatchDeployEvents { get; init; }
|
|
}
|
|
|
|
/// <summary>Reconnect configuration section.</summary>
|
|
internal sealed class ReconnectDto
|
|
{
|
|
/// <summary>Gets or sets the initial backoff in milliseconds.</summary>
|
|
public int? InitialBackoffMs { get; init; }
|
|
/// <summary>Gets or sets the maximum backoff in milliseconds.</summary>
|
|
public int? MaxBackoffMs { get; init; }
|
|
/// <summary>Gets or sets whether to replay on session lost.</summary>
|
|
public bool? ReplayOnSessionLost { get; init; }
|
|
}
|
|
}
|