using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Fragt konfigurierte Sonic-Verbindungen nach Containern ab /// und merged Remote-Treffer mit KnownContainers / Sample-Zielen. /// public sealed class SonicContainerDiscovery { private readonly IReadOnlyList _connections; public SonicContainerDiscovery(IReadOnlyList connections) { _connections = connections; } public bool HasConnections => _connections.Count > 0; public IReadOnlyList Connections => _connections; public async Task DiscoverAsync( string connectionName, IReadOnlyList knownTargets, CancellationToken cancellationToken = default) { SonicConnection? connection = _connections .FirstOrDefault(c => string.Equals(c.Name, connectionName, StringComparison.OrdinalIgnoreCase)); if (connection is null) { return SonicDiscoveryResult.Failed(connectionName, $"Sonic-Verbindung '{connectionName}' ist nicht in AppSettings konfiguriert."); } using SonicManagementClient client = new(connection); (bool reachable, string? pingError, string? resolvedPath) = await client.CheckConnectionAsync(cancellationToken); List diagnostics = []; List remoteContainers = []; bool listOk = false; string? listError = null; if (!reachable) { // Trotzdem KnownContainers nutzen – Neustart kann über SonicBin (stop/startcontainer) gehen. diagnostics.Add($"Management-Ping: {pingError}"); if (connection.KnownContainers.Count == 0 && !knownTargets.Any(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(t.ContainerName))) { return SonicDiscoveryResult.Failed(connectionName, $"Management-Konsole nicht erreichbar: {pingError}"); } } else { (listOk, IReadOnlyList remoteNames, listError) = await client.GetContainersAsync(cancellationToken); foreach (string line in remoteNames) { if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)) { diagnostics.Add(line); continue; } remoteContainers.Add(line); } if (!listOk) { diagnostics.Add($"Listen-Fehler: {listError}"); remoteContainers = []; } } // Fallback: explizit in appsettings hinterlegte Container List mergedNames = MergeContainerNames( remoteContainers, connection.KnownContainers, knownTargets .Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)) .Select(t => t.ContainerName) .Where(n => !string.IsNullOrWhiteSpace(n))); List discovered = BuildTargets(connection, mergedNames, knownTargets); string? hint = BuildHint(connection, remoteContainers.Count, connection.KnownContainers.Count, diagnostics, listOk, listError); return new SonicDiscoveryResult { ConnectionName = connectionName, DomainName = connection.DomainName, Success = true, ErrorMessage = hint, DiscoveredTargets = discovered, RawContainerNames = remoteContainers, ResolvedPath = resolvedPath }; } private static List MergeContainerNames( IEnumerable remote, IEnumerable knownConfigured, IEnumerable knownFromTargets) { List result = []; foreach (string name in remote.Concat(knownConfigured).Concat(knownFromTargets)) { if (string.IsNullOrWhiteSpace(name)) { continue; } if (!result.Any(existing => ContainerNamesMatch(existing, name, domain: null))) { result.Add(name.Trim()); } } return result; } private static string? BuildHint( SonicConnection connection, int remoteCount, int knownConfigCount, List diagnostics, bool listOk, string? listError) { List parts = []; if (!listOk && !string.IsNullOrWhiteSpace(listError)) { parts.Add(listError!); } if (remoteCount == 0) { parts.Add( $"Remote hat 0 Container geliefert (Domain '{connection.DomainName}', SonicHome='{connection.SonicHome}')."); if (knownConfigCount > 0) { parts.Add($"Fallback: {knownConfigCount} KnownContainers aus appsettings."); } else { parts.Add("Tipp: KnownContainers in appsettings setzen oder SonicHome korrigieren."); } } foreach (string d in diagnostics.Take(3)) { parts.Add(d); } return parts.Count == 0 ? null : string.Join(" ", parts); } private static List BuildTargets( SonicConnection connection, IReadOnlyList containerNames, IReadOnlyList knownTargets) { List result = []; int syntheticId = -1; foreach (string containerName in containerNames) { DeploymentTarget? existing = knownTargets.FirstOrDefault(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase) && ContainerNamesMatch(t.ContainerName, containerName, connection.DomainName)); if (existing is not null) { if (!result.Any(r => r.Id == existing.Id)) { result.Add(existing); } } else { result.Add(new DeploymentTarget { Id = syntheticId--, Name = $"{connection.Name} / {containerName}", Environment = connection.DomainName, IsActive = true, TargetDirectory = string.Empty, CertificateFileName = string.Empty, ContainerName = containerName, RestartType = RestartType.SonicContainer, SonicConnectionName = connection.Name, SortOrder = result.Count * 10 }); } } foreach (DeploymentTarget known in knownTargets) { if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)) { continue; } bool alreadyAdded = result.Any(r => ContainerNamesMatch(r.ContainerName, known.ContainerName, connection.DomainName) || r.Id == known.Id); if (!alreadyAdded) { result.Add(known); } } return result; } internal static bool ContainerNamesMatch(string? a, string? b, string? domain) { if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) { return false; } if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { return true; } string aShort = StripDomainPrefix(a, domain); string bShort = StripDomainPrefix(b, domain); return string.Equals(aShort, bShort, StringComparison.OrdinalIgnoreCase); } private static string StripDomainPrefix(string name, string? domain) { if (!string.IsNullOrWhiteSpace(domain) && name.StartsWith(domain + ".", StringComparison.OrdinalIgnoreCase)) { return name[(domain.Length + 1)..]; } int dot = name.IndexOf('.'); return dot > 0 ? name[(dot + 1)..] : name; } } public sealed class SonicDiscoveryResult { public required string ConnectionName { get; init; } public string DomainName { get; init; } = string.Empty; public bool Success { get; init; } public string? ErrorMessage { get; init; } public string? ResolvedPath { get; init; } public IReadOnlyList DiscoveredTargets { get; init; } = []; public IReadOnlyList RawContainerNames { get; init; } = []; public static SonicDiscoveryResult Failed(string connectionName, string error) => new() { ConnectionName = connectionName, Success = false, ErrorMessage = error }; }