too much for one commit

This commit is contained in:
Nils Blomgren
2026-05-13 14:01:32 +02:00
parent a96dd33039
commit 328d494530
394 changed files with 98104 additions and 78 deletions

View 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");
}
}