Mark subproject as dirty in CMKS - Infra
This commit is contained in:
495
tests/EntKube.Web.Tests/ComponentLifecycleServiceTests.cs
Normal file
495
tests/EntKube.Web.Tests/ComponentLifecycleServiceTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user