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;
///
/// Ziele nur aus appsettings KnownContainers (kein Offline-JSON).
///
public IReadOnlyList BuildTargetsFromConfig()
{
List result = [];
int id = 1;
foreach (SonicConnection connection in _connections)
{
foreach (string containerName in connection.KnownContainers
.Where(n => !string.IsNullOrWhiteSpace(n)))
{
result.Add(new DeploymentTarget
{
Id = id++,
Name = $"{connection.Name} / {containerName}",
Environment = connection.DomainName,
IsActive = true,
TargetDirectory = string.Empty,
CertificateFileName = string.Empty,
ContainerName = containerName.Trim(),
RestartType = RestartType.SonicContainer,
SonicConnectionName = connection.Name,
SortOrder = id * 10
});
}
}
return result;
}
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 };
}