DockerContainer.cs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. using System.Text.Json;
  2. namespace RackPeek.Domain.Discovery;
  3. /// <summary>A published port binding on the host side.</summary>
  4. public sealed record DockerPortBinding(int HostPort, string Protocol, string HostIp = "") {
  5. /// <summary>
  6. /// Bound to a loopback address, so only the host itself can reach it. Docker
  7. /// writes the address as given (<c>127.0.0.1</c> mostly, but any 127.x works).
  8. /// </summary>
  9. public bool IsLoopback =>
  10. HostIp.StartsWith("127.", StringComparison.Ordinal) || HostIp == "::1";
  11. /// <summary>Bound to every interface rather than one address.</summary>
  12. public bool IsWildcard => HostIp is "" or "0.0.0.0" or "::";
  13. }
  14. /// <summary>
  15. /// A container, reduced to the parts RackPeek models. Everything here comes from a
  16. /// single <c>GET /containers/json</c> call — the per-container inspect adds nothing
  17. /// a Service resource can hold.
  18. /// </summary>
  19. public sealed record DockerContainer {
  20. public required string Name { get; init; }
  21. public required string Image { get; init; }
  22. public string? ComposeProject { get; init; }
  23. public IReadOnlyList<DockerPortBinding> PublishedPorts { get; init; } = [];
  24. }
  25. /// <summary>Parses the Docker Engine list response. Pure, so it tests from a fixture.</summary>
  26. public static class DockerContainerParser {
  27. private const string _composeProjectLabel = "com.docker.compose.project";
  28. public static List<DockerContainer> Parse(string json) {
  29. using var document = JsonDocument.Parse(json);
  30. if (document.RootElement.ValueKind != JsonValueKind.Array)
  31. return [];
  32. return document.RootElement.EnumerateArray()
  33. .Select(ParseContainer)
  34. .OfType<DockerContainer>()
  35. .OrderBy(c => c.Name, StringComparer.Ordinal)
  36. .ToList();
  37. }
  38. private static DockerContainer? ParseContainer(JsonElement element) {
  39. var name = ParseName(element);
  40. if (string.IsNullOrWhiteSpace(name))
  41. return null;
  42. return new DockerContainer {
  43. Name = name,
  44. Image = GetString(element, "Image") ?? "unknown",
  45. ComposeProject = GetLabel(element, _composeProjectLabel),
  46. PublishedPorts = ParsePorts(element)
  47. };
  48. }
  49. private static string? ParseName(JsonElement element) {
  50. if (!element.TryGetProperty("Names", out JsonElement names) || names.ValueKind != JsonValueKind.Array)
  51. return GetString(element, "Name")?.TrimStart('/');
  52. return names.EnumerateArray()
  53. .Select(n => n.GetString()?.TrimStart('/'))
  54. .FirstOrDefault(n => !string.IsNullOrWhiteSpace(n));
  55. }
  56. private static List<DockerPortBinding> ParsePorts(JsonElement element) {
  57. if (!element.TryGetProperty("Ports", out JsonElement ports) || ports.ValueKind != JsonValueKind.Array)
  58. return [];
  59. // Dual-stack publishes show up twice, once per family ("0.0.0.0" and "::");
  60. // folding wildcards to one spelling keeps that one binding, not two.
  61. return ports.EnumerateArray()
  62. .Where(p => p.TryGetProperty("PublicPort", out JsonElement port) && port.ValueKind == JsonValueKind.Number)
  63. .Select(p => new DockerPortBinding(
  64. p.GetProperty("PublicPort").GetInt32(),
  65. (GetString(p, "Type") ?? "tcp").ToUpperInvariant(),
  66. GetString(p, "IP") ?? ""))
  67. .Select(p => p.IsWildcard ? p with { HostIp = "" } : p)
  68. .DistinctBy(p => (p.HostPort, p.Protocol, p.HostIp))
  69. .OrderBy(p => p.HostPort)
  70. .ToList();
  71. }
  72. private static string? GetLabel(JsonElement element, string label) {
  73. if (!element.TryGetProperty("Labels", out JsonElement labels) || labels.ValueKind != JsonValueKind.Object)
  74. return null;
  75. return labels.TryGetProperty(label, out JsonElement value) ? value.GetString() : null;
  76. }
  77. private static string? GetString(JsonElement element, string property) =>
  78. element.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.String
  79. ? value.GetString()
  80. : null;
  81. }