Fix empty Sonic discovery: KnownContainers fallback and clearer domain status.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 08:19:51 +02:00
co-authored by Cursor
parent c2268f1908
commit 0e9eb27119
5 changed files with 235 additions and 47 deletions
@@ -3,8 +3,8 @@ using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Fragt alle konfigurierten Sonic-Management-Verbindungen nach ihren Containern
/// und gleicht die gefundenen Container mit den bereits konfigurierten Deployment-Zielen ab.
/// Fragt konfigurierte Sonic-Verbindungen nach Containern ab
/// und merged Remote-Treffer mit KnownContainers / Sample-Zielen.
/// </summary>
public sealed class SonicContainerDiscovery
{
@@ -18,10 +18,6 @@ public sealed class SonicContainerDiscovery
public bool HasConnections => _connections.Count > 0;
public IReadOnlyList<SonicConnection> Connections => _connections;
/// <summary>
/// Verbindet sich mit der angegebenen Sonic-Instanz, liest die Container-Liste
/// und reichert sie mit konfigurierten Ziel-Metadaten an.
/// </summary>
public async Task<SonicDiscoveryResult> DiscoverAsync(
string connectionName,
IReadOnlyList<DeploymentTarget> knownTargets,
@@ -45,28 +41,116 @@ public sealed class SonicContainerDiscovery
$"Management-Konsole nicht erreichbar: {pingError}");
}
(bool listOk, IReadOnlyList<string> containerNames, string? listError) =
(bool listOk, IReadOnlyList<string> remoteNames, string? listError) =
await client.GetContainersAsync(cancellationToken);
List<string> diagnostics = [];
List<string> remoteContainers = [];
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)
{
// Fallback: CheckConnection erfolgreich, aber /containers nicht gefunden
// dies kann passieren wenn die API-Pfade abweichen. Container-Namen sind dann leer.
containerNames = [];
diagnostics.Add($"Listen-Fehler: {listError}");
remoteContainers = [];
}
List<DeploymentTarget> discovered = BuildTargets(connection, containerNames, knownTargets);
// Fallback: explizit in appsettings hinterlegte Container
List<string> 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<DeploymentTarget> 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 = listOk ? null : $"Container-Liste konnte nicht geladen werden: {listError}",
ErrorMessage = hint,
DiscoveredTargets = discovered,
RawContainerNames = containerNames
RawContainerNames = remoteContainers,
ResolvedPath = resolvedPath
};
}
private static List<string> MergeContainerNames(
IEnumerable<string> remote,
IEnumerable<string> knownConfigured,
IEnumerable<string> knownFromTargets)
{
List<string> 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<string> diagnostics,
bool listOk,
string? listError)
{
List<string> 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<DeploymentTarget> BuildTargets(
SonicConnection connection,
IReadOnlyList<string> containerNames,
@@ -77,19 +161,19 @@ public sealed class SonicContainerDiscovery
foreach (string containerName in containerNames)
{
// Bekanntes, voll-konfiguriertes Ziel suchen (nach ContainerName + SonicConnectionName)
DeploymentTarget? existing = knownTargets.FirstOrDefault(t =>
string.Equals(t.ContainerName, containerName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase));
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)
&& ContainerNamesMatch(t.ContainerName, containerName, connection.DomainName));
if (existing is not null)
{
result.Add(existing);
if (!result.Any(r => r.Id == existing.Id))
{
result.Add(existing);
}
}
else
{
// Minimales Ziel aus der Discovery erzeugen.
// TargetDirectory/CertificateFileName sind leer → Preflight-Validator zeigt Warnung.
result.Add(new DeploymentTarget
{
Id = syntheticId--,
@@ -106,15 +190,16 @@ public sealed class SonicContainerDiscovery
}
}
// Konfigurierte Ziele dieser Verbindung, die NICHT in der Discovery aufgetaucht sind,
// trotzdem anzeigen (könnten offline / gestoppt sein).
foreach (DeploymentTarget known in knownTargets)
{
if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
{
continue;
}
bool alreadyAdded = result.Any(r =>
string.Equals(r.ContainerName, known.ContainerName, StringComparison.OrdinalIgnoreCase));
ContainerNamesMatch(r.ContainerName, known.ContainerName, connection.DomainName)
|| r.Id == known.Id);
if (!alreadyAdded)
{
@@ -124,13 +209,44 @@ public sealed class SonicContainerDiscovery
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<DeploymentTarget> DiscoveredTargets { get; init; } = [];
public IReadOnlyList<string> RawContainerNames { get; init; } = [];
@@ -185,9 +185,11 @@ public sealed class SonicManagementClient : IDisposable
List<string> names = (output ?? string.Empty)
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(l => l.Length > 0 && !l.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase))
.Where(l => l.Length > 0)
.Where(l => !l.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase))
.ToList();
// INFO:/WARN:-Zeilen aus dem Script bewusst durchreichen (Diagnose in Discovery)
return (true, names, null);
}