DockerServiceMapper.cs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. using RackPeek.Domain.Resources.Services;
  2. namespace RackPeek.Domain.Discovery;
  3. /// <summary>Maps containers onto the Service resources RackPeek stores. Pure.</summary>
  4. public static class DockerServiceMapper {
  5. /// <summary>
  6. /// Containers nothing outside the host can reach are skipped — no published port,
  7. /// or every binding on a loopback address. They are not services anyone would put
  8. /// on an inventory, and a Service is required to carry an address.
  9. /// </summary>
  10. /// <param name="hostSeed">
  11. /// Identity of the machine the containers run on — the host's machine-id where
  12. /// there is one. Part of the container's own id, so the same container name on
  13. /// two different hosts stays two different resources.
  14. /// </param>
  15. public static List<Service> ToResources(
  16. IReadOnlyList<DockerContainer> containers,
  17. string hostSeed,
  18. string hostName,
  19. string? hostIp) {
  20. var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  21. return containers
  22. .Where(c => c.PublishedPorts.Any(p => !p.IsLoopback))
  23. .Select(c => ToResource(c, hostSeed, hostName, hostIp, taken))
  24. .ToList();
  25. }
  26. private static Service ToResource(
  27. DockerContainer container,
  28. string hostSeed,
  29. string hostName,
  30. string? hostIp,
  31. ISet<string> taken) {
  32. var discoveryId = DiscoveryId.Create(
  33. DiscoveryId.DockerScheme,
  34. $"{hostSeed}/{container.Name}");
  35. // A wildcard binding is reachable on the host's own address; a binding pinned to
  36. // one interface is only reachable there, so that address wins over the host's.
  37. DockerPortBinding port = container.PublishedPorts
  38. .Where(p => !p.IsLoopback)
  39. .OrderBy(p => p.IsWildcard ? 0 : 1)
  40. .ThenBy(p => p.HostPort)
  41. .First();
  42. return new Service {
  43. Kind = Service.KindLabel,
  44. Name = DiscoveryNaming.Unique(
  45. DiscoveryNaming.Suggest(container.Name, "service", discoveryId),
  46. discoveryId,
  47. taken),
  48. DiscoveryId = discoveryId,
  49. Network = new Network {
  50. Ip = ServiceIp(port, hostIp),
  51. Port = port.HostPort,
  52. Protocol = port.Protocol
  53. },
  54. Notes = container.Image,
  55. Tags = string.IsNullOrWhiteSpace(container.ComposeProject)
  56. ? []
  57. : [DiscoveryNaming.Slug(container.ComposeProject)],
  58. RunsOn = [hostName]
  59. };
  60. }
  61. /// <summary>
  62. /// The inventory schema holds IPv4 only, so a binding pinned to an IPv6 address
  63. /// falls back to the host's address — the right machine, if not the exact socket.
  64. /// </summary>
  65. private static string? ServiceIp(DockerPortBinding port, string? hostIp) =>
  66. !port.IsWildcard && System.Net.IPAddress.TryParse(port.HostIp, out System.Net.IPAddress? ip)
  67. && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork
  68. ? port.HostIp
  69. : hostIp;
  70. }