257 lines
9.7 KiB
C#
257 lines
9.7 KiB
C#
using EntKube.Provisioning.Domain;
|
|
using EntKube.Provisioning.Features.MinioTenants;
|
|
using EntKube.Provisioning.Features.MinioTenants.AdoptMinioTenants;
|
|
using EntKube.Provisioning.Infrastructure;
|
|
using EntKube.SharedKernel.Domain;
|
|
using FluentAssertions;
|
|
using Moq;
|
|
|
|
namespace EntKube.Provisioning.Tests.Features;
|
|
|
|
/// <summary>
|
|
/// Tests for MinIO Tenant adoption — discovering existing MinIO Tenant CRDs on a
|
|
/// Kubernetes cluster and importing them into the platform's domain so they can be
|
|
/// viewed, managed, and monitored. This follows the same pattern as CNPG cluster
|
|
/// adoption: the Provisioning service scans the cluster, finds what's already running,
|
|
/// and creates domain aggregates for each tenant.
|
|
/// </summary>
|
|
public class MinioTenantAdoptionTests
|
|
{
|
|
private readonly InMemoryMinioTenantRepository tenantRepository;
|
|
private readonly Mock<IMinioTenantClient> tenantClient;
|
|
|
|
public MinioTenantAdoptionTests()
|
|
{
|
|
tenantRepository = new InMemoryMinioTenantRepository();
|
|
tenantClient = new Mock<IMinioTenantClient>();
|
|
}
|
|
|
|
// ─── Adopt Tenants ──────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task AdoptTenants_WithExistingTenants_ImportsThemAsRunning()
|
|
{
|
|
// Arrange — The Kubernetes cluster has two MinIO tenants already running:
|
|
// one for platform storage and one for application data. Neither of them
|
|
// exists in our repository yet, so adoption should discover and import both.
|
|
|
|
Guid clusterId = Guid.NewGuid();
|
|
|
|
tenantClient.Setup(c => c.DiscoverTenantsAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<DiscoveredMinioTenant>
|
|
{
|
|
new(
|
|
Name: "platform-storage",
|
|
Namespace: "minio-platform",
|
|
Pools: new List<DiscoveredMinioPool>
|
|
{
|
|
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
|
|
},
|
|
CurrentState: "Initialized",
|
|
Buckets: new List<string> { "cnpg-backups", "loki-logs" }),
|
|
new(
|
|
Name: "app-storage",
|
|
Namespace: "minio-apps",
|
|
Pools: new List<DiscoveredMinioPool>
|
|
{
|
|
new(Servers: 2, VolumesPerServer: 4, StorageSize: "50Gi", StorageClass: "standard")
|
|
},
|
|
CurrentState: "Initialized",
|
|
Buckets: new List<string> { "user-uploads" })
|
|
});
|
|
|
|
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
|
|
|
|
AdoptMinioTenantsRequest request = new(
|
|
ClusterId: clusterId,
|
|
KubeConfig: "kubeconfig-content",
|
|
ContextName: "prod-context");
|
|
|
|
// Act
|
|
|
|
Result<List<Guid>> result = await handler.HandleAsync(request);
|
|
|
|
// Assert — Both tenants adopted with correct details.
|
|
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value.Should().HaveCount(2);
|
|
|
|
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
|
all.Should().HaveCount(2);
|
|
|
|
MinioTenant platformTenant = all.First(t => t.Name == "platform-storage");
|
|
platformTenant.ClusterId.Should().Be(clusterId);
|
|
platformTenant.Namespace.Should().Be("minio-platform");
|
|
platformTenant.Status.Should().Be(MinioTenantStatus.Running);
|
|
platformTenant.Pools.Should().HaveCount(1);
|
|
platformTenant.Pools[0].Servers.Should().Be(4);
|
|
platformTenant.Pools[0].StorageSize.Should().Be("100Gi");
|
|
|
|
MinioTenant appTenant = all.First(t => t.Name == "app-storage");
|
|
appTenant.Status.Should().Be(MinioTenantStatus.Running);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AdoptTenants_NoTenantsOnCluster_ReturnsEmptyList()
|
|
{
|
|
// Arrange — The MinIO operator is installed but no tenants are deployed yet.
|
|
|
|
tenantClient.Setup(c => c.DiscoverTenantsAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<DiscoveredMinioTenant>());
|
|
|
|
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
|
|
|
|
AdoptMinioTenantsRequest request = new(Guid.NewGuid(), "config", "ctx");
|
|
|
|
// Act
|
|
|
|
Result<List<Guid>> result = await handler.HandleAsync(request);
|
|
|
|
// Assert — Success with empty list, not a failure.
|
|
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AdoptTenants_AlreadyAdopted_DoesNotDuplicate()
|
|
{
|
|
// Arrange — One tenant was already adopted. Running adoption again should
|
|
// skip it and only import new tenants, not create duplicates.
|
|
|
|
Guid clusterId = Guid.NewGuid();
|
|
|
|
MinioTenant existing = MinioTenant.Adopt(
|
|
clusterId: clusterId,
|
|
name: "platform-storage",
|
|
ns: "minio-platform",
|
|
pools: new List<MinioTenantPool>
|
|
{
|
|
new(4, 4, "100Gi", "ceph-block")
|
|
});
|
|
|
|
await tenantRepository.AddAsync(existing);
|
|
|
|
tenantClient.Setup(c => c.DiscoverTenantsAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<DiscoveredMinioTenant>
|
|
{
|
|
new(
|
|
Name: "platform-storage",
|
|
Namespace: "minio-platform",
|
|
Pools: new List<DiscoveredMinioPool>
|
|
{
|
|
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
|
|
},
|
|
CurrentState: "Initialized",
|
|
Buckets: new List<string> { "cnpg-backups" }),
|
|
new(
|
|
Name: "new-tenant",
|
|
Namespace: "minio-new",
|
|
Pools: new List<DiscoveredMinioPool>
|
|
{
|
|
new(Servers: 2, VolumesPerServer: 2, StorageSize: "50Gi", StorageClass: "standard")
|
|
},
|
|
CurrentState: "Initialized",
|
|
Buckets: new List<string>())
|
|
});
|
|
|
|
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
|
|
|
|
AdoptMinioTenantsRequest request = new(clusterId, "config", "ctx");
|
|
|
|
// Act
|
|
|
|
Result<List<Guid>> result = await handler.HandleAsync(request);
|
|
|
|
// Assert — Only one new tenant adopted, the existing one was skipped.
|
|
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value.Should().HaveCount(2);
|
|
|
|
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
|
all.Should().HaveCount(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AdoptTenants_DegradedTenant_MarkedDegraded()
|
|
{
|
|
// Arrange — A tenant with state "Decommissioning" or some non-healthy
|
|
// state should be imported but marked as Degraded, not Running.
|
|
|
|
tenantClient.Setup(c => c.DiscoverTenantsAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<DiscoveredMinioTenant>
|
|
{
|
|
new(
|
|
Name: "sick-tenant",
|
|
Namespace: "minio-sick",
|
|
Pools: new List<DiscoveredMinioPool>
|
|
{
|
|
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
|
|
},
|
|
CurrentState: "Decommissioning",
|
|
Buckets: new List<string>())
|
|
});
|
|
|
|
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
|
|
|
|
AdoptMinioTenantsRequest request = new(Guid.NewGuid(), "config", "ctx");
|
|
|
|
// Act
|
|
|
|
Result<List<Guid>> result = await handler.HandleAsync(request);
|
|
|
|
// Assert
|
|
|
|
result.IsSuccess.Should().BeTrue();
|
|
|
|
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
|
all[0].Status.Should().Be(MinioTenantStatus.Degraded);
|
|
}
|
|
|
|
// ─── Domain: MinioTenant.Adopt ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Adopt_WithValidConfig_CreatesRunningTenant()
|
|
{
|
|
// Arrange — We discover a healthy tenant with known pools.
|
|
|
|
List<MinioTenantPool> pools = new()
|
|
{
|
|
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
|
|
};
|
|
|
|
// Act
|
|
|
|
MinioTenant tenant = MinioTenant.Adopt(
|
|
clusterId: Guid.NewGuid(),
|
|
name: "discovered-tenant",
|
|
ns: "minio-discovered",
|
|
pools: pools);
|
|
|
|
// Assert — Adopted tenants start as Running since they're already serving traffic.
|
|
|
|
tenant.Id.Should().NotBe(Guid.Empty);
|
|
tenant.Name.Should().Be("discovered-tenant");
|
|
tenant.Namespace.Should().Be("minio-discovered");
|
|
tenant.Status.Should().Be(MinioTenantStatus.Running);
|
|
tenant.Pools.Should().HaveCount(1);
|
|
tenant.Pools[0].Servers.Should().Be(4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Adopt_WithEmptyName_Throws()
|
|
{
|
|
// A tenant with no name is not valid — discovery must provide a name.
|
|
|
|
Action act = () => MinioTenant.Adopt(
|
|
Guid.NewGuid(), "", "ns",
|
|
new List<MinioTenantPool> { new(1, 1, "10Gi", "standard") });
|
|
|
|
act.Should().Throw<ArgumentException>();
|
|
}
|
|
}
|