using System.Text; using System.Text.Json; using EntKube.Clusters.Domain; using k8s; using k8s.Models; using Microsoft.Extensions.Logging; namespace EntKube.Clusters.Features.AdoptCluster.Components.Harbor; /// /// Checks whether Harbor container registry is installed on the cluster. /// Harbor serves three purposes for the platform: /// /// 1. **Private registry** — hosting container images and Helm charts /// 2. **Pull-through cache** — caching upstream registries (Docker Hub, ghcr.io, quay.io) /// 3. **Security scanning** — Trivy-based vulnerability scanning on all images /// /// Discovery looks for the Harbor Helm release, verifies core components are healthy, /// and enumerates proxy cache projects that are configured. /// public class HarborCheck : IAdoptionCheck { private readonly ILogger logger; private const string DefaultNamespace = "harbor"; public string ComponentName => "harbor"; public string DisplayName => "Harbor (Container Registry & Helm Charts)"; public HarborCheck(ILogger logger) { this.logger = logger; } public async Task CheckAsync(KubernetesCluster cluster, CancellationToken ct = default) { Kubernetes client = BuildClient(cluster); List details = new(); List missing = new(); // Check 1: Look for Harbor pods in the expected namespace. // Harbor has several components: core, registry, portal, database, redis, trivy, jobservice. Dictionary componentHealth = await CheckHarborComponents(client, ct); if (componentHealth.Count == 0) { missing.Add("No Harbor pods found in 'harbor' namespace"); return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing); } // Report the status of each component we found. List unhealthyComponents = new(); foreach (KeyValuePair component in componentHealth) { details.Add($"{component.Key} component: {component.Value}"); if (component.Value != "Running") { unhealthyComponents.Add($"harbor-{component.Key} pod unhealthy ({component.Value})"); } } // Check 2: Detect the registry endpoint from the Ingress. string? registryEndpoint = await DetectRegistryEndpoint(client, ct); if (registryEndpoint is not null) { details.Add($"Registry endpoint: {registryEndpoint}"); } // Check 3: Detect features (Trivy scanner, ChartMuseum). bool trivyEnabled = componentHealth.ContainsKey("Trivy"); bool chartMuseumEnabled = componentHealth.ContainsKey("ChartMuseum"); if (trivyEnabled) { details.Add("Trivy scanner enabled"); } if (chartMuseumEnabled) { details.Add("ChartMuseum enabled"); } // Check 4: Detect Helm release version. string? helmVersion = await DetectHelmReleaseVersion(client, ct); if (helmVersion is not null) { details.Insert(0, $"Harbor {helmVersion} detected (Helm release 'harbor' in namespace '{DefaultNamespace}')"); } // Build discovered configuration. Dictionary values = new() { ["trivyEnabled"] = trivyEnabled.ToString(), ["chartMuseumEnabled"] = chartMuseumEnabled.ToString() }; if (registryEndpoint is not null) { values["registryEndpoint"] = registryEndpoint; } DiscoveredConfiguration config = new( Version: helmVersion, Namespace: DefaultNamespace, HelmReleaseName: "harbor", Values: values); // Determine overall status. if (unhealthyComponents.Count > 0) { missing.AddRange(unhealthyComponents); return new ComponentCheckResult(ComponentName, ComponentStatus.Degraded, details, missing, config); } return new ComponentCheckResult(ComponentName, ComponentStatus.Installed, details, missing, config); } /// /// Finds all Harbor-related pods and groups them by component, reporting the phase /// of each. Harbor labels its pods with app.kubernetes.io/component. /// private async Task> CheckHarborComponents(Kubernetes client, CancellationToken ct) { Dictionary components = new(); try { V1PodList podList = await client.CoreV1.ListNamespacedPodAsync( DefaultNamespace, labelSelector: "app.kubernetes.io/name=harbor", cancellationToken: ct); foreach (V1Pod pod in podList.Items) { string? component = null; pod.Metadata.Labels?.TryGetValue("app.kubernetes.io/component", out component); if (component is null) { // Fallback: try to infer from pod name. component = InferComponentFromPodName(pod.Metadata.Name); } if (component is not null) { string phase = pod.Status?.Phase ?? "Unknown"; // Use the worst status if multiple pods for the same component. if (!components.ContainsKey(component) || phase != "Running") { components[component] = phase; } } } } catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { logger.LogDebug("Harbor namespace not found"); } catch (Exception ex) { logger.LogWarning(ex, "Error checking for Harbor pods"); } return components; } private static string? InferComponentFromPodName(string podName) { if (podName.Contains("core")) { return "Core"; } if (podName.Contains("registry")) { return "Registry"; } if (podName.Contains("portal") || podName.Contains("nginx")) { return "Portal"; } if (podName.Contains("database") || podName.Contains("postgres")) { return "Database"; } if (podName.Contains("redis")) { return "Redis"; } if (podName.Contains("trivy")) { return "Trivy"; } if (podName.Contains("jobservice")) { return "JobService"; } if (podName.Contains("chartmuseum")) { return "ChartMuseum"; } return null; } /// /// Looks for an Ingress resource in the Harbor namespace to determine the registry endpoint. /// private async Task DetectRegistryEndpoint(Kubernetes client, CancellationToken ct) { try { V1IngressList ingresses = await client.NetworkingV1.ListNamespacedIngressAsync( DefaultNamespace, cancellationToken: ct); foreach (V1Ingress ingress in ingresses.Items) { if (ingress.Spec?.Rules is not null) { foreach (V1IngressRule rule in ingress.Spec.Rules) { if (rule.Host is not null) { return rule.Host; } } } } } catch (Exception ex) { logger.LogDebug(ex, "Could not detect Harbor ingress endpoint"); } return null; } /// /// Detects the Helm release version by looking at the Helm release Secret /// stored in the namespace (Helm stores release metadata as Secrets with /// label owner=helm and type=helm.sh/release.v1). /// private async Task DetectHelmReleaseVersion(Kubernetes client, CancellationToken ct) { try { V1SecretList secrets = await client.CoreV1.ListNamespacedSecretAsync( DefaultNamespace, labelSelector: "owner=helm,name=harbor", cancellationToken: ct); if (secrets.Items.Count > 0) { // Get the latest release (highest version number). V1Secret? latestRelease = secrets.Items .OrderByDescending(s => { string? ver = null; s.Metadata.Labels?.TryGetValue("version", out ver); return ver ?? "0"; }) .FirstOrDefault(); if (latestRelease?.Metadata.Labels?.TryGetValue("version", out string? version) == true) { // The actual app version is embedded in the release data, // but for a quick check we return the chart version from labels. return version; } } } catch (Exception ex) { logger.LogDebug(ex, "Could not detect Harbor Helm release version"); } return null; } private static Kubernetes BuildClient(KubernetesCluster cluster) { byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig); using MemoryStream stream = new(kubeConfigBytes); KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName); return new Kubernetes(config); } }