too much for one commit
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using EntKube.Clusters.Domain;
|
||||
using EntKube.Clusters.Features.AdoptCluster;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Clusters.Tests.Domain;
|
||||
@@ -8,20 +9,29 @@ public class KubernetesClusterTests
|
||||
[Fact]
|
||||
public void Register_WithValidInputs_CreatesClusterInPendingState()
|
||||
{
|
||||
// Arrange & Act — A tenant admin registers a new cluster with its name and API URL.
|
||||
// Arrange & Act — A tenant admin registers a new cluster with its name, API URL,
|
||||
// kubeconfig, tenant, and environment it belongs to.
|
||||
|
||||
Guid tenantId = Guid.NewGuid();
|
||||
Guid environmentId = Guid.NewGuid();
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"production-eu",
|
||||
"https://k8s.example.com:6443",
|
||||
"secret-ref-123");
|
||||
"apiVersion: v1\nkind: Config",
|
||||
tenantId,
|
||||
"prod-context",
|
||||
environmentId);
|
||||
|
||||
// Assert — The cluster should be created with a unique ID and start in Pending state
|
||||
// because we haven't verified connectivity yet.
|
||||
// Assert — The cluster should be created with a unique ID, linked to the tenant
|
||||
// and environment, and start in Pending state because we haven't verified connectivity yet.
|
||||
|
||||
cluster.Id.Should().NotBe(Guid.Empty);
|
||||
cluster.Name.Should().Be("production-eu");
|
||||
cluster.ApiServerUrl.Should().Be("https://k8s.example.com:6443");
|
||||
cluster.KubeConfigSecret.Should().Be("secret-ref-123");
|
||||
cluster.KubeConfig.Should().Contain("apiVersion");
|
||||
cluster.TenantId.Should().Be(tenantId);
|
||||
cluster.EnvironmentId.Should().Be(environmentId);
|
||||
cluster.ContextName.Should().Be("prod-context");
|
||||
cluster.Status.Should().Be(ClusterStatus.Pending);
|
||||
cluster.RegisteredAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
@@ -32,7 +42,7 @@ public class KubernetesClusterTests
|
||||
// Arrange & Act — Attempting to register a cluster without a name should fail
|
||||
// because every cluster needs a human-readable identifier.
|
||||
|
||||
Action act = () => KubernetesCluster.Register("", "https://k8s.example.com", null);
|
||||
Action act = () => KubernetesCluster.Register("", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -45,7 +55,7 @@ public class KubernetesClusterTests
|
||||
{
|
||||
// Arrange & Act — Without an API URL, we can't communicate with the cluster.
|
||||
|
||||
Action act = () => KubernetesCluster.Register("my-cluster", "", null);
|
||||
Action act = () => KubernetesCluster.Register("my-cluster", "", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -58,7 +68,7 @@ public class KubernetesClusterTests
|
||||
{
|
||||
// Arrange — A freshly registered cluster in Pending state.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", null);
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
// Act — The health check confirms the cluster is reachable.
|
||||
|
||||
@@ -75,7 +85,7 @@ public class KubernetesClusterTests
|
||||
{
|
||||
// Arrange
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", null);
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Act — The health check fails.
|
||||
@@ -87,4 +97,355 @@ public class KubernetesClusterTests
|
||||
cluster.Status.Should().Be(ClusterStatus.Unreachable);
|
||||
cluster.LastHealthCheckAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WithEnvironmentId_StoresEnvironmentId()
|
||||
{
|
||||
// Arrange & Act — A cluster is registered and immediately assigned
|
||||
// to an environment (e.g. dev01 → "dev" environment).
|
||||
|
||||
Guid tenantId = Guid.NewGuid();
|
||||
Guid environmentId = Guid.NewGuid();
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"dev01", "https://api.dev", "kubeconfig", tenantId, "dev-ctx", environmentId);
|
||||
|
||||
// Assert — The environment should be stored on the cluster.
|
||||
|
||||
cluster.EnvironmentId.Should().Be(environmentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WithoutEnvironmentId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act — Attempting to register a cluster without an environment
|
||||
// should fail because every cluster must belong to an environment.
|
||||
|
||||
Action act = () => KubernetesCluster.Register(
|
||||
"orphan", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.Empty);
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("environmentId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignToEnvironment_ReassignsToNewEnvironment()
|
||||
{
|
||||
// Arrange — A cluster registered in one environment can be reassigned.
|
||||
|
||||
Guid originalEnvId = Guid.NewGuid();
|
||||
Guid newEnvId = Guid.NewGuid();
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test01", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", originalEnvId);
|
||||
|
||||
// Act — Admin reassigns it to a different environment.
|
||||
|
||||
cluster.AssignToEnvironment(newEnvId);
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.EnvironmentId.Should().Be(newEnvId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignToEnvironment_WithEmptyGuid_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test01", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
|
||||
Action act = () => cluster.AssignToEnvironment(Guid.Empty);
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("environmentId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_CreatesClusterWithEmptyComponents()
|
||||
{
|
||||
// Arrange & Act — A freshly registered cluster has no known components yet.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Components.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponents_WithCheckResults_PersistsComponents()
|
||||
{
|
||||
// Arrange — After an adoption scan, we record the discovered components
|
||||
// on the cluster so they survive without needing to re-scan.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
List<ComponentCheckResult> results = new()
|
||||
{
|
||||
new ComponentCheckResult("kyverno", ComponentStatus.Installed,
|
||||
new List<string> { "Pod running" }, new List<string>(),
|
||||
new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary<string, string> { ["replicas"] = "3" })),
|
||||
new ComponentCheckResult("cert-manager", ComponentStatus.NotInstalled,
|
||||
new List<string>(), new List<string> { "No pods found" })
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
cluster.UpdateComponents(results);
|
||||
|
||||
// Assert — Both components stored with their status and configuration.
|
||||
|
||||
cluster.Components.Should().HaveCount(2);
|
||||
|
||||
ClusterComponent kyverno = cluster.Components.First(c => c.ComponentName == "kyverno");
|
||||
kyverno.Status.Should().Be(ComponentStatus.Installed);
|
||||
kyverno.Version.Should().Be("3.3.4");
|
||||
kyverno.Namespace.Should().Be("kyverno");
|
||||
kyverno.Configuration["replicas"].Should().Be("3");
|
||||
kyverno.LastCheckedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
|
||||
ClusterComponent certManager = cluster.Components.First(c => c.ComponentName == "cert-manager");
|
||||
certManager.Status.Should().Be(ComponentStatus.NotInstalled);
|
||||
certManager.Configuration.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponents_ReplacesExistingComponents()
|
||||
{
|
||||
// Arrange — A second scan should replace the first scan's results entirely.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
List<ComponentCheckResult> firstScan = new()
|
||||
{
|
||||
new ComponentCheckResult("kyverno", ComponentStatus.NotInstalled, new List<string>(), new List<string> { "Missing" })
|
||||
};
|
||||
|
||||
List<ComponentCheckResult> secondScan = new()
|
||||
{
|
||||
new ComponentCheckResult("kyverno", ComponentStatus.Installed,
|
||||
new List<string> { "Running" }, new List<string>(),
|
||||
new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary<string, string>()))
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
cluster.UpdateComponents(firstScan);
|
||||
cluster.UpdateComponents(secondScan);
|
||||
|
||||
// Assert — Only the latest scan results should remain.
|
||||
|
||||
cluster.Components.Should().HaveCount(1);
|
||||
cluster.Components[0].Status.Should().Be(ComponentStatus.Installed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponentConfiguration_UpdatesExistingComponentValues()
|
||||
{
|
||||
// Arrange — After a configuration change is deployed, we update the
|
||||
// stored configuration so it reflects the current state.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
List<ComponentCheckResult> results = new()
|
||||
{
|
||||
new ComponentCheckResult("monitoring", ComponentStatus.Installed,
|
||||
new List<string>(), new List<string>(),
|
||||
new DiscoveredConfiguration("45.0.0", "monitoring", "kube-prometheus-stack",
|
||||
new Dictionary<string, string> { ["retention"] = "30d", ["replicas"] = "2" }))
|
||||
};
|
||||
cluster.UpdateComponents(results);
|
||||
|
||||
// Act — Change retention to 90d.
|
||||
|
||||
Dictionary<string, string> newValues = new() { ["retention"] = "90d" };
|
||||
cluster.UpdateComponentConfiguration("monitoring", newValues);
|
||||
|
||||
// Assert — Retention updated, replicas unchanged.
|
||||
|
||||
ClusterComponent monitoring = cluster.Components.First(c => c.ComponentName == "monitoring");
|
||||
monitoring.Configuration["retention"].Should().Be("90d");
|
||||
monitoring.Configuration["replicas"].Should().Be("2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponentConfiguration_ForUnknownComponent_DoesNotThrow()
|
||||
{
|
||||
// Arrange — If a component hasn't been scanned yet, updating its config
|
||||
// is a no-op. This shouldn't break anything.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Act — No components recorded yet.
|
||||
|
||||
Action act = () => cluster.UpdateComponentConfiguration("unknown", new Dictionary<string, string> { ["k"] = "v" });
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().NotThrow();
|
||||
cluster.Components.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponentVersion_ForInstalledComponent_UpdatesVersion()
|
||||
{
|
||||
// Arrange — A cluster has Harbor installed at version 1.16.0. After a
|
||||
// successful upgrade, we need to track the new version.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
List<ComponentCheckResult> results = new()
|
||||
{
|
||||
new ComponentCheckResult("harbor", ComponentStatus.Installed,
|
||||
new List<string> { "Running" }, new List<string>(),
|
||||
new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary<string, string>()))
|
||||
};
|
||||
|
||||
cluster.UpdateComponents(results);
|
||||
|
||||
// Act — Upgrade to 1.17.0.
|
||||
|
||||
cluster.UpdateComponentVersion("harbor", "1.17.0");
|
||||
|
||||
// Assert — The tracked version now reflects the upgrade.
|
||||
|
||||
ClusterComponent harbor = cluster.Components.First(c => c.ComponentName == "harbor");
|
||||
harbor.Version.Should().Be("1.17.0");
|
||||
harbor.LastCheckedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponentVersion_ForUnknownComponent_DoesNotThrow()
|
||||
{
|
||||
// Arrange — If the component hasn't been scanned, updating its version
|
||||
// is a safe no-op — no exception, no side effects.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Act
|
||||
|
||||
Action act = () => cluster.UpdateComponentVersion("nonexistent", "1.0.0");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().NotThrow();
|
||||
cluster.Components.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateComponentVersion_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange — Component names should match case-insensitively, just like
|
||||
// MarkComponentInstalled and UpdateComponentConfiguration.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
cluster.MarkConnected();
|
||||
|
||||
List<ComponentCheckResult> results = new()
|
||||
{
|
||||
new ComponentCheckResult("Harbor", ComponentStatus.Installed,
|
||||
new List<string>(), new List<string>(),
|
||||
new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary<string, string>()))
|
||||
};
|
||||
|
||||
cluster.UpdateComponents(results);
|
||||
|
||||
// Act — Use different casing.
|
||||
|
||||
cluster.UpdateComponentVersion("harbor", "1.17.0");
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Components.First().Version.Should().Be("1.17.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DefaultsToNoneProvider()
|
||||
{
|
||||
// When a cluster is first registered, no cloud provider is set.
|
||||
// The operator can assign one later when they know where it's hosted.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
cluster.Provider.Should().Be(CloudProvider.None);
|
||||
cluster.ProviderCredentials.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProvider_WithCleura_StoresProviderAndCredentials()
|
||||
{
|
||||
// A cluster admin links their cluster to Cleura, providing their
|
||||
// Cleura Cloud username and password so EntKube can call the REST API.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"cleura-cluster", "https://k8s.cleura.cloud", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
ProviderCredentials creds = new("my-user", "my-password", "Sto2");
|
||||
|
||||
// Act
|
||||
|
||||
cluster.SetProvider(CloudProvider.Cleura, creds);
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Provider.Should().Be(CloudProvider.Cleura);
|
||||
cluster.ProviderCredentials.Should().NotBeNull();
|
||||
cluster.ProviderCredentials!.Username.Should().Be("my-user");
|
||||
cluster.ProviderCredentials!.Region.Should().Be("Sto2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProvider_ToNone_ClearsCredentials()
|
||||
{
|
||||
// Unlinking a provider removes the stored credentials.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
cluster.SetProvider(CloudProvider.Cleura, new ProviderCredentials("u", "p", "Sto2"));
|
||||
|
||||
// Act
|
||||
|
||||
cluster.SetProvider(CloudProvider.None, null);
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Provider.Should().Be(CloudProvider.None);
|
||||
cluster.ProviderCredentials.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProvider_CleuraWithoutCredentials_Throws()
|
||||
{
|
||||
// Setting Cleura as the provider requires credentials — you can't
|
||||
// call the Cleura API without a username and password.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid());
|
||||
|
||||
Action act = () => cluster.SetProvider(CloudProvider.Cleura, null);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user