using RackPeek.Domain.Resources.Services; namespace RackPeek.Domain.Discovery; /// Maps containers onto the Service resources RackPeek stores. Pure. public static class DockerServiceMapper { /// /// Containers nothing outside the host can reach are skipped — no published port, /// or every binding on a loopback address. They are not services anyone would put /// on an inventory, and a Service is required to carry an address. /// /// /// Identity of the machine the containers run on — the host's machine-id where /// there is one. Part of the container's own id, so the same container name on /// two different hosts stays two different resources. /// public static List ToResources( IReadOnlyList containers, string hostSeed, string hostName, string? hostIp) { var taken = new HashSet(StringComparer.OrdinalIgnoreCase); return containers .Where(c => c.PublishedPorts.Any(p => !p.IsLoopback)) .Select(c => ToResource(c, hostSeed, hostName, hostIp, taken)) .ToList(); } private static Service ToResource( DockerContainer container, string hostSeed, string hostName, string? hostIp, ISet taken) { var discoveryId = DiscoveryId.Create( DiscoveryId.DockerScheme, $"{hostSeed}/{container.Name}"); // A wildcard binding is reachable on the host's own address; a binding pinned to // one interface is only reachable there, so that address wins over the host's. DockerPortBinding port = container.PublishedPorts .Where(p => !p.IsLoopback) .OrderBy(p => p.IsWildcard ? 0 : 1) .ThenBy(p => p.HostPort) .First(); return new Service { Kind = Service.KindLabel, Name = DiscoveryNaming.Unique( DiscoveryNaming.Suggest(container.Name, "service", discoveryId), discoveryId, taken), DiscoveryId = discoveryId, Network = new Network { Ip = ServiceIp(port, hostIp), Port = port.HostPort, Protocol = port.Protocol }, Notes = container.Image, Tags = string.IsNullOrWhiteSpace(container.ComposeProject) ? [] : [DiscoveryNaming.Slug(container.ComposeProject)], RunsOn = [hostName] }; } /// /// The inventory schema holds IPv4 only, so a binding pinned to an IPv6 address /// falls back to the host's address — the right machine, if not the exact socket. /// private static string? ServiceIp(DockerPortBinding port, string? hostIp) => !port.IsWildcard && System.Net.IPAddress.TryParse(port.HostIp, out System.Net.IPAddress? ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? port.HostIp : hostIp; }