too much for one commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
using EntKube.Provisioning.Domain;
|
||||
using EntKube.Provisioning.Features.MinioTenants;
|
||||
using EntKube.Provisioning.Features.MinioTenants.CreateMinioTenant;
|
||||
using EntKube.Provisioning.Features.MinioTenants.ConfigureMinioTenant;
|
||||
using EntKube.Provisioning.Features.MinioTenants.DeleteMinioTenant;
|
||||
using EntKube.Provisioning.Infrastructure;
|
||||
using EntKube.SharedKernel.Domain;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace EntKube.Provisioning.Tests.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for MinIO Tenant provisioning — the full lifecycle of creating, configuring,
|
||||
/// and deleting MinIO tenants on a Kubernetes cluster. Unlike MinIO adoption (which
|
||||
/// discovers existing tenants), this provisions new tenants from scratch via the
|
||||
/// MinIO Operator's Tenant CRD.
|
||||
/// </summary>
|
||||
public class MinioTenantProvisioningTests
|
||||
{
|
||||
private readonly InMemoryMinioTenantRepository tenantRepository;
|
||||
private readonly Mock<IMinioTenantClient> tenantClient;
|
||||
|
||||
public MinioTenantProvisioningTests()
|
||||
{
|
||||
tenantRepository = new InMemoryMinioTenantRepository();
|
||||
tenantClient = new Mock<IMinioTenantClient>();
|
||||
}
|
||||
|
||||
// ─── Create Tenant ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithValidConfig_ProvisionsTenantOnCluster()
|
||||
{
|
||||
// Arrange — A platform admin wants a new MinIO tenant with 4 servers,
|
||||
// each with 4 disks of 100Gi, using the ceph-block storage class.
|
||||
|
||||
tenantClient.Setup(c => c.ApplyTenantAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MinioTenant>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
CreateMinioTenantRequest request = new(
|
||||
ClusterId: Guid.NewGuid(),
|
||||
KubeConfig: "kubeconfig-content",
|
||||
ContextName: "prod-context",
|
||||
Name: "platform-storage",
|
||||
Namespace: "minio-tenant-1",
|
||||
Pools: new List<PoolSpec>
|
||||
{
|
||||
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Tenant created in domain and applied to K8s cluster.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().NotBe(Guid.Empty);
|
||||
|
||||
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
||||
all.Should().HaveCount(1);
|
||||
all[0].Name.Should().Be("platform-storage");
|
||||
all[0].Namespace.Should().Be("minio-tenant-1");
|
||||
all[0].Status.Should().Be(MinioTenantStatus.Provisioning);
|
||||
all[0].Pools.Should().HaveCount(1);
|
||||
all[0].Pools[0].Servers.Should().Be(4);
|
||||
all[0].Pools[0].VolumesPerServer.Should().Be(4);
|
||||
|
||||
// Verify the K8s client was called to apply the Tenant CRD.
|
||||
|
||||
tenantClient.Verify(c => c.ApplyTenantAsync(
|
||||
"kubeconfig-content", "prod-context",
|
||||
It.Is<MinioTenant>(t => t.Name == "platform-storage"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithMultiplePools_AllPoolsApplied()
|
||||
{
|
||||
// Arrange — Heterogeneous pools: fast NVMe for hot data, HDD for warm.
|
||||
|
||||
tenantClient.Setup(c => c.ApplyTenantAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MinioTenant>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
CreateMinioTenantRequest request = new(
|
||||
ClusterId: Guid.NewGuid(),
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
Name: "multi-tier",
|
||||
Namespace: "minio-multi",
|
||||
Pools: new List<PoolSpec>
|
||||
{
|
||||
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "nvme-fast"),
|
||||
new(Servers: 2, VolumesPerServer: 8, StorageSize: "500Gi", StorageClass: "hdd-bulk")
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
||||
all[0].Pools.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithInvalidConfig_ReturnsFailure()
|
||||
{
|
||||
// Arrange — Zero servers is not valid.
|
||||
|
||||
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
CreateMinioTenantRequest request = new(
|
||||
ClusterId: Guid.NewGuid(),
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
Name: "bad-tenant",
|
||||
Namespace: "minio-bad",
|
||||
Pools: new List<PoolSpec>
|
||||
{
|
||||
new(Servers: 0, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Domain validation catches the bad config.
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("server");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_KubernetesClientFails_ReturnsFailure()
|
||||
{
|
||||
// Arrange — Network issue or RBAC problem prevents applying the CRD.
|
||||
|
||||
tenantClient.Setup(c => c.ApplyTenantAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MinioTenant>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("Forbidden: RBAC denied"));
|
||||
|
||||
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
CreateMinioTenantRequest request = new(
|
||||
ClusterId: Guid.NewGuid(),
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
Name: "tenant",
|
||||
Namespace: "minio-ns",
|
||||
Pools: new List<PoolSpec>
|
||||
{
|
||||
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Failure reported without tenant being persisted.
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("RBAC");
|
||||
|
||||
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
|
||||
all.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ─── Configure Tenant ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureTenant_UpdatesPools_AndReapplies()
|
||||
{
|
||||
// Arrange — Existing tenant needs scale-out: from 4 to 8 servers.
|
||||
|
||||
MinioTenant existing = MinioTenant.Create(
|
||||
Guid.NewGuid(), "platform-storage", "minio-tenant-1",
|
||||
new List<MinioTenantPool> { new(4, 4, "100Gi", "ceph-block") });
|
||||
existing.MarkRunning();
|
||||
await tenantRepository.AddAsync(existing);
|
||||
|
||||
tenantClient.Setup(c => c.ApplyTenantAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MinioTenant>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
ConfigureMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
ConfigureMinioTenantRequest request = new(
|
||||
TenantId: existing.Id,
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
Pools: new List<PoolSpec>
|
||||
{
|
||||
new(Servers: 8, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
|
||||
});
|
||||
|
||||
// Act
|
||||
|
||||
Result result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Pools updated, status back to Provisioning, K8s re-applied.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
MinioTenant? updated = await tenantRepository.GetByIdAsync(existing.Id);
|
||||
updated.Should().NotBeNull();
|
||||
updated!.Pools[0].Servers.Should().Be(8);
|
||||
updated.Status.Should().Be(MinioTenantStatus.Provisioning);
|
||||
|
||||
tenantClient.Verify(c => c.ApplyTenantAsync(
|
||||
"config", "ctx",
|
||||
It.Is<MinioTenant>(t => t.Pools[0].Servers == 8),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureTenant_NotFound_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
ConfigureMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
ConfigureMinioTenantRequest request = new(
|
||||
TenantId: Guid.NewGuid(),
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
Pools: new List<PoolSpec> { new(4, 4, "100Gi", "standard") });
|
||||
|
||||
// Act
|
||||
|
||||
Result result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
// ─── Delete Tenant ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTenant_RemovesFromClusterAndMarksDecommissioned()
|
||||
{
|
||||
// Arrange — Platform admin tears down a tenant.
|
||||
|
||||
MinioTenant existing = MinioTenant.Create(
|
||||
Guid.NewGuid(), "old-tenant", "minio-old",
|
||||
new List<MinioTenantPool> { new(4, 4, "100Gi", "standard") });
|
||||
existing.MarkRunning();
|
||||
await tenantRepository.AddAsync(existing);
|
||||
|
||||
tenantClient.Setup(c => c.DeleteTenantAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
DeleteMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
DeleteMinioTenantRequest request = new(
|
||||
TenantId: existing.Id,
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Marked decommissioned and deleted from K8s.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
MinioTenant? tenant = await tenantRepository.GetByIdAsync(existing.Id);
|
||||
tenant.Should().NotBeNull();
|
||||
tenant!.Status.Should().Be(MinioTenantStatus.Decommissioned);
|
||||
|
||||
tenantClient.Verify(c => c.DeleteTenantAsync(
|
||||
"config", "ctx", "old-tenant", "minio-old",
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTenant_NotFound_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
DeleteMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
|
||||
|
||||
DeleteMinioTenantRequest request = new(Guid.NewGuid(), "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user