9cad9ed0fc
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
135 lines
6.3 KiB
C#
135 lines
6.3 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.MxGateway.Client;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser;
|
|
|
|
/// <summary>
|
|
/// Opens transient gateway connections for the AdminUI address picker. Mirrors the
|
|
/// runtime <c>GalaxyDriver.BuildClientOptions</c> pattern so the gateway sees the
|
|
/// same option shape, but tweaks <see cref="MxGatewayClientOptions.ApiKey"/>
|
|
/// resolution + the gateway-side client identity so browse sessions are
|
|
/// distinguishable from the runtime driver's live MX session.
|
|
/// </summary>
|
|
public sealed class GalaxyDriverBrowser : IDriverBrowser
|
|
{
|
|
/// <summary>
|
|
/// Identifier used in gateway-side logs / metrics for AdminUI browse sessions.
|
|
/// Distinct from any runtime driver's <c>MxAccess.ClientName</c> so an operator
|
|
/// can tell the two apart when triaging.
|
|
/// </summary>
|
|
internal const string BrowseClientIdentity = "OtOpcUa-AdminUI-Browse";
|
|
|
|
/// <summary>Hard cap on the time we'll wait for the initial gateway handshake.</summary>
|
|
private static readonly TimeSpan ConnectBudget = TimeSpan.FromSeconds(30);
|
|
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
private readonly ILogger<GalaxyDriverBrowser> _logger;
|
|
|
|
/// <summary>Creates a new browser. Logger defaults to <see cref="NullLogger{T}"/>.</summary>
|
|
/// <param name="logger">Optional logger; null is allowed for unit-test construction.</param>
|
|
public GalaxyDriverBrowser(ILogger<GalaxyDriverBrowser>? logger = null)
|
|
{
|
|
_logger = logger ?? NullLogger<GalaxyDriverBrowser>.Instance;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
// Hardcoded literal: this project references Driver.Galaxy.Contracts, not Driver.Galaxy,
|
|
// so GalaxyDriverFactoryExtensions.DriverTypeName isn't available here.
|
|
public string DriverType => "GalaxyMxGateway";
|
|
|
|
// Deserializes a GalaxyDriverOptions blob, opens a transient GalaxyRepositoryClient
|
|
// against the configured gateway endpoint, and returns a browse session over it. The
|
|
// session owns the client and disposes it on IBrowseSession.DisposeAsync. Throws
|
|
// InvalidOperationException when the JSON deserialises to null, when Gateway.Endpoint
|
|
// is empty, or when MxAccess.ClientName is empty.
|
|
/// <inheritdoc />
|
|
public async Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken)
|
|
{
|
|
var opts = JsonSerializer.Deserialize<GalaxyDriverOptions>(configJson, JsonOpts)
|
|
?? throw new InvalidOperationException("Galaxy options deserialized to null.");
|
|
|
|
if (string.IsNullOrWhiteSpace(opts.Gateway.Endpoint))
|
|
throw new InvalidOperationException("Galaxy browser requires Gateway.Endpoint.");
|
|
|
|
// The form persists MXAccess identity as ClientName (there is no separate
|
|
// "galaxy name" knob on the driver — the gateway picks the galaxy via its
|
|
// own GalaxyRepository config). Refuse a blank ClientName so the gateway side
|
|
// doesn't see anonymous browse sessions during triage.
|
|
if (string.IsNullOrWhiteSpace(opts.MxAccess.ClientName))
|
|
throw new InvalidOperationException("Galaxy browser requires MxAccess.ClientName.");
|
|
|
|
var clientOpts = BuildClientOptions(opts.Gateway);
|
|
|
|
// 30s wall-clock budget for the connect phase, linked to the caller's token so
|
|
// an AdminUI cancel still wins early.
|
|
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
connectCts.CancelAfter(ConnectBudget);
|
|
|
|
GalaxyRepositoryClient? client = null;
|
|
try
|
|
{
|
|
client = GalaxyRepositoryClient.Create(clientOpts);
|
|
|
|
// TestConnectionAsync gives the gateway a chance to surface auth / TLS / DNS
|
|
// failures synchronously inside the connect budget rather than waiting for
|
|
// the first DiscoverHierarchyAsync call to fail. The client's own
|
|
// ConnectTimeout already bounds the underlying gRPC handshake; the linked
|
|
// CTS layered on top guarantees the AdminUI never blocks past 30s.
|
|
await client.TestConnectionAsync(connectCts.Token).ConfigureAwait(false);
|
|
|
|
_logger.LogInformation(
|
|
"AdminUI Galaxy browse session opened against {Endpoint} (admin-client={Identity}, runtime-client={RuntimeClient})",
|
|
opts.Gateway.Endpoint, BrowseClientIdentity, opts.MxAccess.ClientName);
|
|
|
|
var session = new GalaxyBrowseSession(client);
|
|
client = null; // Ownership transferred — keep finally from disposing.
|
|
return session;
|
|
}
|
|
catch
|
|
{
|
|
if (client is not null)
|
|
{
|
|
try
|
|
{
|
|
await client.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort cleanup; the original exception is more useful.
|
|
}
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build the gateway client options from the form's Gateway section. Mirrors the
|
|
/// runtime driver's <c>GalaxyDriver.BuildClientOptions</c> field-for-field so the
|
|
/// gateway sees an identical option shape. The API-key reference is resolved via
|
|
/// the shared <see cref="GalaxySecretRef.ResolveApiKey"/> in Driver.Galaxy.Contracts
|
|
/// (the same resolver the runtime driver uses), so browse and runtime stay in lock-step.
|
|
/// </summary>
|
|
private MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new()
|
|
{
|
|
Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
|
|
ApiKey = GalaxySecretRef.ResolveApiKey(gw.ApiKeySecretRef, _logger),
|
|
UseTls = gw.UseTls,
|
|
CaCertificatePath = gw.CaCertificatePath,
|
|
ConnectTimeout = TimeSpan.FromSeconds(gw.ConnectTimeoutSeconds),
|
|
DefaultCallTimeout = TimeSpan.FromSeconds(gw.DefaultCallTimeoutSeconds),
|
|
StreamTimeout = gw.StreamTimeoutSeconds > 0
|
|
? TimeSpan.FromSeconds(gw.StreamTimeoutSeconds)
|
|
: null,
|
|
};
|
|
}
|