too much for one commit
This commit is contained in:
728
tests/EntKube.Clusters.Tests/Features/HarborTests.cs
Normal file
728
tests/EntKube.Clusters.Tests/Features/HarborTests.cs
Normal file
@@ -0,0 +1,728 @@
|
||||
using EntKube.Clusters.Domain;
|
||||
using EntKube.Clusters.Features.AdoptCluster;
|
||||
using EntKube.Clusters.Features.AdoptCluster.Components;
|
||||
using EntKube.Clusters.Features.AdoptCluster.ConfigureComponent;
|
||||
using EntKube.Clusters.Features.AdoptCluster.DeployComponent;
|
||||
using EntKube.Clusters.Infrastructure;
|
||||
using EntKube.SharedKernel.Domain;
|
||||
using FluentAssertions;
|
||||
using k8s.Models;
|
||||
using Moq;
|
||||
|
||||
namespace EntKube.Clusters.Tests.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the Harbor container registry component. Harbor provides:
|
||||
///
|
||||
/// 1. **Container registry** — hosting private container images
|
||||
/// 2. **Helm chart repository** — hosting Helm charts via OCI
|
||||
/// 3. **Pull-through cache** — caching upstream registries (Docker Hub, ghcr.io, quay.io, etc.)
|
||||
///
|
||||
/// The pull-through cache is especially valuable: all services and tenant apps
|
||||
/// pull images through Harbor, gaining caching, vulnerability scanning, and
|
||||
/// network isolation from public registries.
|
||||
///
|
||||
/// Dependency chain: ingress (for Harbor UI/API) → cert-manager (TLS) → Harbor
|
||||
/// </summary>
|
||||
public class HarborTests
|
||||
{
|
||||
private readonly InMemoryClusterRepository repository;
|
||||
private readonly Mock<IComponentInstaller> harborInstaller;
|
||||
|
||||
public HarborTests()
|
||||
{
|
||||
repository = new InMemoryClusterRepository();
|
||||
|
||||
harborInstaller = new Mock<IComponentInstaller>();
|
||||
harborInstaller.Setup(i => i.ComponentName).Returns("harbor");
|
||||
}
|
||||
|
||||
// ─── Harbor Installation ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Install_DeploysRegistryWithPullThroughCache()
|
||||
{
|
||||
// Arrange — We want a full Harbor deployment with pull-through proxy cache
|
||||
// enabled for major upstream registries. This gives us a local mirror that
|
||||
// speeds up pulls and survives upstream outages.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.InstallAsync(cluster, It.IsAny<ComponentInstallOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Harbor 2.12.0 installed with pull-through cache for docker.io, ghcr.io, quay.io",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Ensured namespace 'harbor'",
|
||||
"Helm install harbor v2.12.0",
|
||||
"Created proxy cache project 'dockerhub-cache' → docker.io",
|
||||
"Created proxy cache project 'ghcr-cache' → ghcr.io",
|
||||
"Created proxy cache project 'quay-cache' → quay.io",
|
||||
"Harbor registry available at registry.internal.corp.com"
|
||||
}));
|
||||
|
||||
DeployComponentHandler handler = new(repository, new List<IComponentInstaller>
|
||||
{
|
||||
harborInstaller.Object
|
||||
}, Microsoft.Extensions.Logging.Abstractions.NullLogger<DeployComponentHandler>.Instance);
|
||||
|
||||
DeployComponentRequest request = new("harbor", Version: "2.12.0",
|
||||
Parameters: new Dictionary<string, string>
|
||||
{
|
||||
["harborDomain"] = "registry.internal.corp.com",
|
||||
["adminPassword"] = "Harbor12345",
|
||||
["storageClass"] = "longhorn",
|
||||
["storageSize"] = "100Gi",
|
||||
["cacheRegistries"] = "docker.io,ghcr.io,quay.io",
|
||||
["enableChartMuseum"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
|
||||
|
||||
// Assert — Harbor deployed with proxy cache projects for each registry.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("Helm install harbor"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("dockerhub-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("ghcr-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("quay-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("registry.internal.corp.com"));
|
||||
|
||||
harborInstaller.Verify(i => i.InstallAsync(
|
||||
cluster,
|
||||
It.Is<ComponentInstallOptions>(o =>
|
||||
o.Parameters!["harborDomain"] == "registry.internal.corp.com" &&
|
||||
o.Parameters["cacheRegistries"] == "docker.io,ghcr.io,quay.io"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Install_WithTLS_UsesClusterIssuerForCertificate()
|
||||
{
|
||||
// Arrange — Harbor should use a cert-manager ClusterIssuer to auto-provision
|
||||
// TLS certificates. This integrates with the internal CA or domain CA
|
||||
// we set up earlier.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.InstallAsync(cluster, It.IsAny<ComponentInstallOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Harbor 2.12.0 installed with TLS via ClusterIssuer 'platform-internal-ca'",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Ensured namespace 'harbor'",
|
||||
"Helm install harbor v2.12.0",
|
||||
"Configured TLS via ClusterIssuer 'platform-internal-ca'",
|
||||
"Created Ingress for registry.internal.corp.com with TLS",
|
||||
"Harbor registry available at registry.internal.corp.com"
|
||||
}));
|
||||
|
||||
DeployComponentHandler handler = new(repository, new List<IComponentInstaller>
|
||||
{
|
||||
harborInstaller.Object
|
||||
}, Microsoft.Extensions.Logging.Abstractions.NullLogger<DeployComponentHandler>.Instance);
|
||||
|
||||
DeployComponentRequest request = new("harbor", Version: "2.12.0",
|
||||
Parameters: new Dictionary<string, string>
|
||||
{
|
||||
["harborDomain"] = "registry.internal.corp.com",
|
||||
["adminPassword"] = "Harbor12345",
|
||||
["storageClass"] = "longhorn",
|
||||
["storageSize"] = "50Gi",
|
||||
["tlsIssuer"] = "platform-internal-ca",
|
||||
["enableTLS"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
|
||||
|
||||
// Assert — TLS configured via cert-manager integration.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("TLS via ClusterIssuer"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("Ingress"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Install_WithoutPrerequisites_ReportsFailure()
|
||||
{
|
||||
// Arrange — Harbor requires ingress to be accessible. If the cluster
|
||||
// has no ingress controller, the install should fail gracefully.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.InstallAsync(cluster, It.IsAny<ComponentInstallOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: false,
|
||||
ComponentName: "harbor",
|
||||
Message: "Harbor requires an ingress controller but none was detected",
|
||||
Actions: new List<string> { "Prerequisite check failed: no ingress controller found" }));
|
||||
|
||||
DeployComponentHandler handler = new(repository, new List<IComponentInstaller>
|
||||
{
|
||||
harborInstaller.Object
|
||||
}, Microsoft.Extensions.Logging.Abstractions.NullLogger<DeployComponentHandler>.Instance);
|
||||
|
||||
DeployComponentRequest request = new("harbor",
|
||||
Parameters: new Dictionary<string, string>
|
||||
{
|
||||
["harborDomain"] = "registry.internal.corp.com",
|
||||
["adminPassword"] = "Harbor12345"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
|
||||
|
||||
// Assert — Prerequisite failure reported.
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("ingress");
|
||||
}
|
||||
|
||||
// ─── Harbor Configuration ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Configure_AddProxyCacheRegistry()
|
||||
{
|
||||
// Arrange — Harbor is running. We want to add a new upstream registry
|
||||
// as a proxy cache (e.g., registry.k8s.io for Kubernetes images).
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.ConfigureAsync(cluster, It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Proxy cache project added for registry.k8s.io",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Created proxy cache project 'k8s-registry-cache' → registry.k8s.io",
|
||||
"Proxy cache now covers: docker.io, ghcr.io, quay.io, registry.k8s.io"
|
||||
}));
|
||||
|
||||
List<IComponentInstaller> installers = new() { harborInstaller.Object };
|
||||
ConfigureComponentHandler configHandler = new(repository, installers);
|
||||
|
||||
ConfigureComponentRequest configRequest = new(
|
||||
ComponentName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["addProxyCache"] = "registry.k8s.io",
|
||||
["proxyCacheProjectName"] = "k8s-registry-cache"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await configHandler.HandleAsync(cluster.Id, configRequest);
|
||||
|
||||
// Assert — New proxy cache project created.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("registry.k8s.io"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("k8s-registry-cache"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Configure_SetAsDefaultPullThroughCache()
|
||||
{
|
||||
// Arrange — We want to configure the cluster so all pod image pulls
|
||||
// go through Harbor as a mirror/cache. This typically involves configuring
|
||||
// containerd mirror settings or using Kyverno to mutate image references.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.ConfigureAsync(cluster, It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Cluster configured to use Harbor as default pull-through cache",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Created Kyverno ClusterPolicy 'harbor-image-mirror' to rewrite image references",
|
||||
"Policy rewrites docker.io/* → registry.internal.corp.com/dockerhub-cache/*",
|
||||
"Policy rewrites ghcr.io/* → registry.internal.corp.com/ghcr-cache/*",
|
||||
"Policy rewrites quay.io/* → registry.internal.corp.com/quay-cache/*"
|
||||
}));
|
||||
|
||||
List<IComponentInstaller> installers = new() { harborInstaller.Object };
|
||||
ConfigureComponentHandler configHandler = new(repository, installers);
|
||||
|
||||
ConfigureComponentRequest configRequest = new(
|
||||
ComponentName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["setDefaultCache"] = "true",
|
||||
["harborDomain"] = "registry.internal.corp.com",
|
||||
["cacheStrategy"] = "kyverno-rewrite"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await configHandler.HandleAsync(cluster.Id, configRequest);
|
||||
|
||||
// Assert — Kyverno policy created to rewrite image refs through Harbor.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("ClusterPolicy"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("docker.io"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("ghcr.io"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Configure_CreatePrivateProject()
|
||||
{
|
||||
// Arrange — Create a private project in Harbor for tenant images.
|
||||
// Each tenant gets their own project with isolation.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.ConfigureAsync(cluster, It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Private project 'tenant-alpha' created",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Created private project 'tenant-alpha'",
|
||||
"Created robot account 'robot$tenant-alpha-pull' with pull access",
|
||||
"Created image pull Secret 'harbor-pull-tenant-alpha' in namespace 'tenant-alpha'"
|
||||
}));
|
||||
|
||||
List<IComponentInstaller> installers = new() { harborInstaller.Object };
|
||||
ConfigureComponentHandler configHandler = new(repository, installers);
|
||||
|
||||
ConfigureComponentRequest configRequest = new(
|
||||
ComponentName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["createProject"] = "tenant-alpha",
|
||||
["projectVisibility"] = "private",
|
||||
["createPullSecret"] = "true",
|
||||
["targetNamespace"] = "tenant-alpha"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await configHandler.HandleAsync(cluster.Id, configRequest);
|
||||
|
||||
// Assert — Project, robot account, and pull secret created.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("private project 'tenant-alpha'"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("robot account"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("pull Secret"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Configure_EnableVulnerabilityScanning()
|
||||
{
|
||||
// Arrange — Enable automatic vulnerability scanning on all pushed images.
|
||||
// Harbor uses Trivy for scanning. We want to configure the scan policy.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.ConfigureAsync(cluster, It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Vulnerability scanning configured",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Enabled automatic scan on push for all projects",
|
||||
"Set vulnerability severity threshold to 'High'",
|
||||
"Configured scan schedule: daily at 02:00 UTC"
|
||||
}));
|
||||
|
||||
List<IComponentInstaller> installers = new() { harborInstaller.Object };
|
||||
ConfigureComponentHandler configHandler = new(repository, installers);
|
||||
|
||||
ConfigureComponentRequest configRequest = new(
|
||||
ComponentName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["enableScanOnPush"] = "true",
|
||||
["severityThreshold"] = "High",
|
||||
["scanSchedule"] = "0 2 * * *"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await configHandler.HandleAsync(cluster.Id, configRequest);
|
||||
|
||||
// Assert — Scanning policies configured.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("scan on push"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("severity threshold"));
|
||||
}
|
||||
|
||||
// ─── Wire All Platform Services ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Configure_WireAllServices_CreatesComprehensiveCacheAndPolicy()
|
||||
{
|
||||
// Arrange — Harbor is running. We want to wire ALL platform services
|
||||
// (MinIO, CloudNativePG, cert-manager, Kyverno, Istio, Grafana, Prometheus,
|
||||
// Redis, RabbitMQ, External Secrets) to pull images through Harbor.
|
||||
// This creates proxy cache projects for every upstream registry used by
|
||||
// our services and a Kyverno policy to rewrite image references.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.ConfigureAsync(cluster, It.IsAny<ComponentConfiguration>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "All platform services wired to pull through Harbor",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Ensured proxy cache project 'dockerhub-cache' → docker.io (MinIO, Istio, Grafana, Redis, RabbitMQ)",
|
||||
"Ensured proxy cache project 'ghcr-cache' → ghcr.io (CloudNativePG, Kyverno, External Secrets)",
|
||||
"Ensured proxy cache project 'quay-cache' → quay.io (cert-manager, trust-manager, Prometheus)",
|
||||
"Ensured proxy cache project 'k8s-cache' → registry.k8s.io (CoreDNS, metrics-server)",
|
||||
"Ensured proxy cache project 'gcr-cache' → gcr.io (Istio release images)",
|
||||
"Created Kyverno ClusterPolicy 'harbor-image-mirror' to rewrite image references",
|
||||
"Policy rewrites docker.io/* → registry.internal.corp.com/dockerhub-cache/*",
|
||||
"Policy rewrites ghcr.io/* → registry.internal.corp.com/ghcr-cache/*",
|
||||
"Policy rewrites quay.io/* → registry.internal.corp.com/quay-cache/*",
|
||||
"Policy rewrites registry.k8s.io/* → registry.internal.corp.com/k8s-cache/*",
|
||||
"Policy rewrites gcr.io/* → registry.internal.corp.com/gcr-cache/*",
|
||||
"All 12 platform services now pull through Harbor"
|
||||
}));
|
||||
|
||||
List<IComponentInstaller> installers = new() { harborInstaller.Object };
|
||||
ConfigureComponentHandler configHandler = new(repository, installers);
|
||||
|
||||
ConfigureComponentRequest configRequest = new(
|
||||
ComponentName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["wireAllServices"] = "true",
|
||||
["harborDomain"] = "registry.internal.corp.com"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await configHandler.HandleAsync(cluster.Id, configRequest);
|
||||
|
||||
// Assert — All registries cached and policy covers all platforms.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("docker.io") && a.Contains("MinIO"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("ghcr.io") && a.Contains("CloudNativePG"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("quay.io") && a.Contains("cert-manager"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("registry.k8s.io"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("gcr.io"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("ClusterPolicy"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("12 platform services"));
|
||||
|
||||
harborInstaller.Verify(i => i.ConfigureAsync(
|
||||
cluster,
|
||||
It.Is<ComponentConfiguration>(c =>
|
||||
c.Values["wireAllServices"] == "true" &&
|
||||
c.Values["harborDomain"] == "registry.internal.corp.com"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Install_WithAllServices_IncludesAllRegistryCaches()
|
||||
{
|
||||
// Arrange — When installing Harbor with wireAllServices=true, it should
|
||||
// automatically create proxy caches for all 5 upstream registries
|
||||
// needed by platform services, not just the default 3.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.InstallAsync(cluster, It.IsAny<ComponentInstallOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Harbor 2.12.0 installed with pull-through cache for all platform registries",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Ensured namespace 'harbor'",
|
||||
"Helm install harbor v2.12.0",
|
||||
"Created proxy cache project 'dockerhub-cache' → docker.io",
|
||||
"Created proxy cache project 'ghcr-cache' → ghcr.io",
|
||||
"Created proxy cache project 'quay-cache' → quay.io",
|
||||
"Created proxy cache project 'k8s-cache' → registry.k8s.io",
|
||||
"Created proxy cache project 'gcr-cache' → gcr.io",
|
||||
"Harbor registry available at registry.internal.corp.com"
|
||||
}));
|
||||
|
||||
DeployComponentHandler handler = new(repository, new List<IComponentInstaller>
|
||||
{
|
||||
harborInstaller.Object
|
||||
}, Microsoft.Extensions.Logging.Abstractions.NullLogger<DeployComponentHandler>.Instance);
|
||||
|
||||
DeployComponentRequest request = new("harbor", Version: "2.12.0",
|
||||
Parameters: new Dictionary<string, string>
|
||||
{
|
||||
["harborDomain"] = "registry.internal.corp.com",
|
||||
["adminPassword"] = "Harbor12345",
|
||||
["storageClass"] = "longhorn",
|
||||
["storageSize"] = "100Gi",
|
||||
["cacheRegistries"] = "docker.io,ghcr.io,quay.io,registry.k8s.io,gcr.io"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
|
||||
|
||||
// Assert — All 5 registries have proxy cache projects.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("dockerhub-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("ghcr-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("quay-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("k8s-cache"));
|
||||
result.Value.Actions.Should().Contain(a => a.Contains("gcr-cache"));
|
||||
}
|
||||
|
||||
// ─── Harbor Adoption Check ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Check_DetectsExistingHarborDeployment()
|
||||
{
|
||||
// Arrange — A cluster already has Harbor installed via Helm.
|
||||
// The adoption check should discover it, report version, and enumerate
|
||||
// existing proxy cache projects.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
Mock<IAdoptionCheck> harborCheck = new();
|
||||
harborCheck.Setup(c => c.ComponentName).Returns("harbor");
|
||||
harborCheck.Setup(c => c.DisplayName).Returns("Harbor (Container Registry & Helm Charts)");
|
||||
|
||||
harborCheck
|
||||
.Setup(c => c.CheckAsync(cluster, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ComponentCheckResult(
|
||||
"harbor",
|
||||
ComponentStatus.Installed,
|
||||
Details: new List<string>
|
||||
{
|
||||
"Harbor 2.12.0 detected (Helm release 'harbor' in namespace 'harbor')",
|
||||
"Registry endpoint: registry.internal.corp.com",
|
||||
"Proxy cache projects: dockerhub-cache, ghcr-cache, quay-cache",
|
||||
"Trivy scanner enabled",
|
||||
"ChartMuseum enabled"
|
||||
},
|
||||
MissingItems: new List<string>(),
|
||||
Configuration: new DiscoveredConfiguration(
|
||||
Version: "2.12.0",
|
||||
Namespace: "harbor",
|
||||
HelmReleaseName: "harbor",
|
||||
Values: new Dictionary<string, string>
|
||||
{
|
||||
["registryEndpoint"] = "registry.internal.corp.com",
|
||||
["proxyCacheProjects"] = "dockerhub-cache,ghcr-cache,quay-cache",
|
||||
["trivyEnabled"] = "true",
|
||||
["chartMuseumEnabled"] = "true",
|
||||
["storageClass"] = "longhorn",
|
||||
["storageSize"] = "100Gi"
|
||||
})));
|
||||
|
||||
// Act
|
||||
|
||||
ComponentCheckResult checkResult = await harborCheck.Object.CheckAsync(cluster);
|
||||
|
||||
// Assert — Harbor detected with full configuration discovery.
|
||||
|
||||
checkResult.Status.Should().Be(ComponentStatus.Installed);
|
||||
checkResult.Configuration.Should().NotBeNull();
|
||||
checkResult.Configuration!.Version.Should().Be("2.12.0");
|
||||
checkResult.Configuration.HelmReleaseName.Should().Be("harbor");
|
||||
checkResult.Configuration.Values["proxyCacheProjects"].Should().Contain("dockerhub-cache");
|
||||
checkResult.Details.Should().Contain(d => d.Contains("Trivy"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Check_DetectsPartialInstallation()
|
||||
{
|
||||
// Arrange — Harbor is installed but some pods are not healthy (e.g.,
|
||||
// the database pod is in CrashLoopBackOff). The check reports Degraded.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
Mock<IAdoptionCheck> harborCheck = new();
|
||||
harborCheck.Setup(c => c.ComponentName).Returns("harbor");
|
||||
harborCheck.Setup(c => c.DisplayName).Returns("Harbor (Container Registry & Helm Charts)");
|
||||
|
||||
harborCheck
|
||||
.Setup(c => c.CheckAsync(cluster, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ComponentCheckResult(
|
||||
"harbor",
|
||||
ComponentStatus.Degraded,
|
||||
Details: new List<string>
|
||||
{
|
||||
"Harbor Helm release found in namespace 'harbor'",
|
||||
"Core component: Running",
|
||||
"Registry component: Running",
|
||||
"Database component: CrashLoopBackOff"
|
||||
},
|
||||
MissingItems: new List<string>
|
||||
{
|
||||
"harbor-database pod unhealthy (CrashLoopBackOff)"
|
||||
}));
|
||||
|
||||
// Act
|
||||
|
||||
ComponentCheckResult checkResult = await harborCheck.Object.CheckAsync(cluster);
|
||||
|
||||
// Assert — Degraded status with details about what's wrong.
|
||||
|
||||
checkResult.Status.Should().Be(ComponentStatus.Degraded);
|
||||
checkResult.MissingItems.Should().Contain(m => m.Contains("database"));
|
||||
checkResult.Details.Should().Contain(d => d.Contains("CrashLoopBackOff"));
|
||||
}
|
||||
|
||||
private static KubernetesCluster CreateConnectedCluster()
|
||||
{
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"prod-cluster", "https://k8s.example.com:6443",
|
||||
"apiVersion: v1\nkind: Config\nclusters: []\nusers: []\ncontexts: []",
|
||||
Guid.NewGuid(), "prod-ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
return cluster;
|
||||
}
|
||||
|
||||
// ─── CNPG Database Integration ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HarborSchema_ContainsCnpgClusterNameParameter()
|
||||
{
|
||||
// Harbor should be able to reference the shared CNPG cluster for its
|
||||
// database instead of using the embedded PostgreSQL sub-chart.
|
||||
|
||||
ConfigurationSchema schema = ComponentSchemas.GetSchema("harbor");
|
||||
|
||||
schema.Parameters.Should().Contain(p =>
|
||||
p.Key == "cnpgClusterName" && p.Type == ParameterType.PostgresCluster);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarborSchema_ContainsCnpgNamespaceParameter()
|
||||
{
|
||||
// The CNPG cluster namespace is needed to resolve the primary service endpoint.
|
||||
|
||||
ConfigurationSchema schema = ComponentSchemas.GetSchema("harbor");
|
||||
|
||||
schema.Parameters.Should().Contain(p => p.Key == "cnpgNamespace");
|
||||
}
|
||||
|
||||
// ─── Harbor Database Credentials ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Harbor_Install_WithCnpg_StoresCredentialsInVault()
|
||||
{
|
||||
// Arrange — When Harbor is installed with a CNPG cluster, the installer
|
||||
// should store the generated database credentials in the vault at
|
||||
// infrastructure/{clusterId}/harbor/database/* so they're auditable
|
||||
// and recoverable.
|
||||
|
||||
KubernetesCluster cluster = CreateConnectedCluster();
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
// Track vault calls made by the installer.
|
||||
List<string> vaultPaths = new();
|
||||
|
||||
harborInstaller
|
||||
.Setup(i => i.InstallAsync(cluster, It.IsAny<ComponentInstallOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new InstallResult(
|
||||
Success: true,
|
||||
ComponentName: "harbor",
|
||||
Message: "Harbor installed with external CNPG database",
|
||||
Actions: new List<string>
|
||||
{
|
||||
"Created database 'harbor' on CNPG cluster 'shared-pg'",
|
||||
"Stored database credentials in Kubernetes Secret and vault",
|
||||
"Helm install harbor v1.16.0"
|
||||
}));
|
||||
|
||||
DeployComponentHandler handler = new(repository, new List<IComponentInstaller>
|
||||
{
|
||||
harborInstaller.Object
|
||||
}, Microsoft.Extensions.Logging.Abstractions.NullLogger<DeployComponentHandler>.Instance);
|
||||
|
||||
DeployComponentRequest request = new("harbor",
|
||||
Parameters: new Dictionary<string, string>
|
||||
{
|
||||
["harborDomain"] = "registry.dev.example.com",
|
||||
["cnpgClusterName"] = "shared-pg",
|
||||
["cnpgNamespace"] = "cnpg-system"
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<InstallResult> result = await handler.HandleAsync(cluster.Id, request);
|
||||
|
||||
// Assert — The install result should mention both the K8s Secret and vault.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value!.Actions.Should().Contain(a => a.Contains("Stored database credentials"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CnpgCredentialsSecret_ContainsAllRequiredKeys()
|
||||
{
|
||||
// When Harbor uses an external CNPG database, a K8s Secret is created
|
||||
// with the database connection details. This secret must contain all
|
||||
// four keys that Harbor expects: host, database, username, password.
|
||||
|
||||
V1Secret secret = CnpgDatabaseProvisioner.BuildCredentialsSecret(
|
||||
"harbor-cnpg-credentials", "harbor",
|
||||
"shared-pg-rw.cnpg-system.svc.cluster.local",
|
||||
"harbor", "harbor", "secret-password-123");
|
||||
|
||||
secret.StringData.Should().ContainKey("host");
|
||||
secret.StringData.Should().ContainKey("database");
|
||||
secret.StringData.Should().ContainKey("username");
|
||||
secret.StringData.Should().ContainKey("password");
|
||||
secret.StringData["host"].Should().Be("shared-pg-rw.cnpg-system.svc.cluster.local");
|
||||
secret.StringData["database"].Should().Be("harbor");
|
||||
secret.StringData["username"].Should().Be("harbor");
|
||||
secret.StringData["password"].Should().Be("secret-password-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CnpgCredentialsSecret_HasManagementLabels()
|
||||
{
|
||||
// The credentials secret should be labeled for EntKube management
|
||||
// so it can be discovered and cleaned up.
|
||||
|
||||
V1Secret secret = CnpgDatabaseProvisioner.BuildCredentialsSecret(
|
||||
"harbor-cnpg-credentials", "harbor",
|
||||
"host", "harbor", "harbor", "pass");
|
||||
|
||||
secret.Metadata.Labels["app.kubernetes.io/managed-by"].Should().Be("entkube");
|
||||
secret.Metadata.Labels["entkube.io/purpose"].Should().Be("database-credentials");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user