Mark subproject as dirty in CMKS - Infra

This commit is contained in:
Nils Blomgren
2026-05-18 08:37:09 +02:00
parent 328d494530
commit 8b94fb8826
635 changed files with 81145 additions and 100551 deletions

View File

@@ -0,0 +1,495 @@
using EntKube.Web.Data;
using EntKube.Web.Services;
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace EntKube.Web.Tests;
/// <summary>
/// Tests for ComponentLifecycleService — the service that manages the full
/// lifecycle of cluster components (install, configure, upgrade, uninstall).
/// Tests cover the data-layer operations and validation; actual Helm CLI
/// execution requires a live cluster + helm binary.
/// </summary>
public class ComponentLifecycleServiceTests : IDisposable
{
private readonly SqliteConnection connection;
private readonly ApplicationDbContext db;
private readonly TestDbContextFactory dbFactory;
private readonly ComponentLifecycleService sut;
private readonly VaultService vaultService;
private readonly Guid tenantId = Guid.NewGuid();
private readonly Guid envId = Guid.NewGuid();
private readonly Guid clusterId = Guid.NewGuid();
public ComponentLifecycleServiceTests()
{
connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
DbContextOptions<ApplicationDbContext> options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
db = new ApplicationDbContext(options);
dbFactory = new TestDbContextFactory(connection);
db.Database.EnsureCreated();
// Seed a tenant, environment, and cluster for component lifecycle tests.
Tenant tenant = new() { Id = tenantId, Name = "LifecycleTenant", Slug = "lifecycle" };
Data.Environment env = new() { Id = envId, TenantId = tenantId, Name = "production" };
KubernetesCluster cluster = new()
{
Id = clusterId,
TenantId = tenantId,
EnvironmentId = envId,
Name = "lifecycle-cluster",
ApiServerUrl = "https://k8s.example.com",
Kubeconfig = "apiVersion: v1\nkind: Config\nclusters:\n- cluster:\n server: https://k8s.example.com\n name: test\ncontexts:\n- context:\n cluster: test\n user: test\n name: test\ncurrent-context: test\nusers:\n- name: test\n user:\n token: fake-token"
};
db.Set<Tenant>().Add(tenant);
db.Set<Data.Environment>().Add(env);
db.KubernetesClusters.Add(cluster);
db.SaveChanges();
byte[] testRootKey = Convert.FromBase64String("dGhpcyBpcyBhIDMyIGJ5dGUga2V5ISEhMTIzNDU2Nzg=");
VaultEncryptionService encryption = new(testRootKey);
vaultService = new VaultService(dbFactory, encryption);
sut = new ComponentLifecycleService(dbFactory, vaultService);
}
public void Dispose()
{
db.Dispose();
connection.Dispose();
}
// ──────── RegisterComponentAsync ────────
[Fact]
public async Task RegisterComponentAsync_ValidInput_CreatesWithNotInstalledStatus()
{
// Arrange — a new Helm component registration for kube-prometheus-stack.
ComponentRegistration registration = new()
{
Name = "kube-prometheus-stack",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmRepoUrl = "https://prometheus-community.github.io/helm-charts",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
// Act
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, registration);
// Assert — component exists in DB with NotInstalled status.
component.Should().NotBeNull();
component.Status.Should().Be(ComponentStatus.NotInstalled);
component.Namespace.Should().Be("monitoring");
component.HelmRepoUrl.Should().Be("https://prometheus-community.github.io/helm-charts");
component.HelmChartName.Should().Be("kube-prometheus-stack");
component.HelmChartVersion.Should().Be("65.1.0");
component.ReleaseName.Should().Be("kube-prometheus-stack");
}
[Fact]
public async Task RegisterComponentAsync_DuplicateName_Throws()
{
// Arrange — register a component, then try to register another with the same name.
ComponentRegistration registration = new()
{
Name = "duplicate-comp",
ComponentType = "HelmChart",
Namespace = "default"
};
await sut.RegisterComponentAsync(clusterId, registration);
// Act & Assert
Func<Task> act = () => sut.RegisterComponentAsync(clusterId, registration);
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*already exists*");
}
// ──────── UpdateConfigurationAsync ────────
[Fact]
public async Task UpdateConfigurationAsync_ExistingComponent_UpdatesHelmValues()
{
// Arrange — register a component, then update its values.
ComponentRegistration reg = new()
{
Name = "config-test",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
string newValues = "alertmanager:\n enabled: true\ngrafana:\n enabled: true";
// Act
ClusterComponent updated = await sut.UpdateConfigurationAsync(
component.Id, newValues, chartVersion: "65.2.0");
// Assert
updated.HelmValues.Should().Be(newValues);
updated.HelmChartVersion.Should().Be("65.2.0");
}
[Fact]
public async Task UpdateConfigurationAsync_NonExistentComponent_Throws()
{
// Act & Assert
Func<Task> act = () => sut.UpdateConfigurationAsync(Guid.NewGuid(), "values: true");
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*not found*");
}
// ──────── PrepareInstallAsync — validates before install ────────
[Fact]
public async Task PrepareInstallAsync_ValidComponent_SetsInstallingStatus()
{
// Arrange — a properly configured component ready to install.
ComponentRegistration reg = new()
{
Name = "ready-to-install",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmRepoUrl = "https://prometheus-community.github.io/helm-charts",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Act
ClusterComponent prepared = await sut.PrepareInstallAsync(component.Id);
// Assert — status should transition to Installing.
prepared.Status.Should().Be(ComponentStatus.Installing);
}
[Fact]
public async Task PrepareInstallAsync_MissingHelmChart_ReturnsFailure()
{
// Arrange — component without chart info can't be installed.
ComponentRegistration reg = new()
{
Name = "no-chart",
ComponentType = "HelmChart",
Namespace = "default"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Act & Assert
Func<Task> act = () => sut.PrepareInstallAsync(component.Id);
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*chart name*");
}
[Fact]
public async Task PrepareInstallAsync_AlreadyInstalled_Throws()
{
// Arrange — component that's already in Installed state.
ComponentRegistration reg = new()
{
Name = "already-installed",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Manually set status to Installed to simulate.
// Re-fetch from test db since the returned entity is detached.
ClusterComponent tracked = (await db.Set<ClusterComponent>().FindAsync(component.Id))!;
tracked.Status = ComponentStatus.Installed;
await db.SaveChangesAsync();
// Act & Assert — can't install something that's already installed (use upgrade instead).
Func<Task> act = () => sut.PrepareInstallAsync(component.Id);
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*already installed*");
}
// ──────── MarkInstallResultAsync ────────
[Fact]
public async Task MarkInstallResultAsync_Success_SetsInstalledStatus()
{
// Arrange
ComponentRegistration reg = new()
{
Name = "mark-success",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
await sut.PrepareInstallAsync(component.Id);
// Act
ClusterComponent result = await sut.MarkInstallResultAsync(component.Id, success: true);
// Assert
result.Status.Should().Be(ComponentStatus.Installed);
result.InstalledAt.Should().NotBeNull();
result.LastError.Should().BeNull();
}
[Fact]
public async Task MarkInstallResultAsync_Failure_SetsFailedStatusWithError()
{
// Arrange
ComponentRegistration reg = new()
{
Name = "mark-failure",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
await sut.PrepareInstallAsync(component.Id);
// Act
ClusterComponent result = await sut.MarkInstallResultAsync(
component.Id, success: false, error: "timeout waiting for resources");
// Assert
result.Status.Should().Be(ComponentStatus.Failed);
result.LastError.Should().Contain("timeout");
result.InstalledAt.Should().BeNull();
}
// ──────── PrepareUninstallAsync ────────
[Fact]
public async Task PrepareUninstallAsync_InstalledComponent_SetsUninstallingStatus()
{
// Arrange — an installed component ready to be removed.
ComponentRegistration reg = new()
{
Name = "to-uninstall",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Re-fetch from test db since the returned entity is detached.
ClusterComponent tracked = (await db.Set<ClusterComponent>().FindAsync(component.Id))!;
tracked.Status = ComponentStatus.Installed;
tracked.InstalledAt = DateTime.UtcNow;
await db.SaveChangesAsync();
// Act
ClusterComponent prepared = await sut.PrepareUninstallAsync(component.Id);
// Assert
prepared.Status.Should().Be(ComponentStatus.Uninstalling);
}
[Fact]
public async Task PrepareUninstallAsync_NotInstalled_Throws()
{
// Arrange — can't uninstall something that's not installed.
ComponentRegistration reg = new()
{
Name = "not-installed-yet",
ComponentType = "HelmChart",
Namespace = "default",
HelmChartName = "some-chart"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Act & Assert
Func<Task> act = () => sut.PrepareUninstallAsync(component.Id);
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*not installed*");
}
// ──────── GetInstallCommandAsync — builds helm CLI command ────────
[Fact]
public async Task GetInstallCommandAsync_WithAllFields_ReturnsCorrectCommand()
{
// Arrange — a fully configured component.
ComponentRegistration reg = new()
{
Name = "full-cmd",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmRepoUrl = "https://prometheus-community.github.io/helm-charts",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0",
ReleaseName = "prom-stack"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
await sut.UpdateConfigurationAsync(component.Id, "grafana:\n enabled: true");
// Act
HelmCommand command = await sut.GetInstallCommandAsync(component.Id);
// Assert — verify the command structure is correct.
command.Operation.Should().Be("upgrade --install");
command.ReleaseName.Should().Be("prom-stack");
command.ChartReference.Should().Contain("kube-prometheus-stack");
command.Namespace.Should().Be("monitoring");
command.RepoUrl.Should().Be("https://prometheus-community.github.io/helm-charts");
command.Version.Should().Be("65.1.0");
command.HasValues.Should().BeTrue();
}
[Fact]
public async Task GetUninstallCommandAsync_InstalledComponent_ReturnsCorrectCommand()
{
// Arrange
ComponentRegistration reg = new()
{
Name = "uninstall-cmd",
ComponentType = "HelmChart",
Namespace = "monitoring",
HelmChartName = "kube-prometheus-stack",
ReleaseName = "prom-stack"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, reg);
// Re-fetch from test db since the returned entity is detached.
ClusterComponent tracked = (await db.Set<ClusterComponent>().FindAsync(component.Id))!;
tracked.Status = ComponentStatus.Installed;
await db.SaveChangesAsync();
// Act
HelmCommand command = await sut.GetUninstallCommandAsync(component.Id);
// Assert
command.Operation.Should().Be("uninstall");
command.ReleaseName.Should().Be("prom-stack");
command.Namespace.Should().Be("monitoring");
}
// ──────── Secret Injection ────────
[Fact]
public async Task GetInstallCommandAsync_WithVaultSecret_InjectsSecretIntoValues()
{
// Arrange — register kube-prometheus-stack and store a Grafana admin
// password in the vault. The password should NOT be in the plain YAML
// but SHOULD appear in the final install command values.
ComponentRegistration registration = new()
{
Name = "kube-prometheus-stack",
ComponentType = "Helm",
Namespace = "monitoring",
HelmRepoUrl = "https://prometheus-community.github.io/helm-charts",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0",
ReleaseName = "kube-prometheus-stack",
HelmValues = "grafana:\n enabled: true\n"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, registration);
// Store the admin password in the vault under the secret name
// matching the catalog's SecretName for grafana-password.
await vaultService.InitializeVaultAsync(tenantId);
await vaultService.SetComponentSecretAsync(tenantId, component.Id, "GRAFANA_ADMIN_PASSWORD", "SuperSecret123!");
await sut.PrepareInstallAsync(component.Id);
// Act
HelmCommand command = await sut.GetInstallCommandAsync(component.Id);
// Assert — the values YAML should contain the injected secret.
command.ValuesYaml.Should().Contain("SuperSecret123!");
command.ValuesYaml.Should().Contain("adminPassword");
command.HasValues.Should().BeTrue();
}
[Fact]
public async Task GetInstallCommandAsync_WithoutVaultSecret_DoesNotInjectAnything()
{
// Arrange — register kube-prometheus-stack but DON'T store any secret.
// The values YAML should remain unchanged.
ComponentRegistration registration = new()
{
Name = "kube-prometheus-stack",
ComponentType = "Helm",
Namespace = "monitoring",
HelmRepoUrl = "https://prometheus-community.github.io/helm-charts",
HelmChartName = "kube-prometheus-stack",
HelmChartVersion = "65.1.0",
ReleaseName = "prom-no-secret",
HelmValues = "grafana:\n enabled: true\n"
};
ClusterComponent component = await sut.RegisterComponentAsync(clusterId, registration);
await sut.PrepareInstallAsync(component.Id);
// Act
HelmCommand command = await sut.GetInstallCommandAsync(component.Id);
// Assert — no secret injection means original YAML is preserved.
command.ValuesYaml.Should().Be("grafana:\n enabled: true\n");
command.HasValues.Should().BeTrue();
}
}