361 lines
12 KiB
C#
361 lines
12 KiB
C#
using EntKube.Provisioning.Domain;
|
|
using FluentAssertions;
|
|
|
|
namespace EntKube.Provisioning.Tests.Domain;
|
|
|
|
public class GrafanaInstanceTests
|
|
{
|
|
private readonly Guid environmentId = Guid.NewGuid();
|
|
private readonly Guid clusterId = Guid.NewGuid();
|
|
|
|
// ─── Creation ───────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Create_WithValidInputs_CreatesInstanceInPendingState()
|
|
{
|
|
// Arrange & Act — An operator provisions a Grafana instance for an environment.
|
|
// Grafana is per-environment: one centralized visualization for all clusters
|
|
// in that environment, deployed to a specific cluster.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(
|
|
environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Assert — The instance should be in Pending state with sensible defaults.
|
|
|
|
instance.Id.Should().NotBe(Guid.Empty);
|
|
instance.EnvironmentId.Should().Be(environmentId);
|
|
instance.ClusterId.Should().Be(clusterId);
|
|
instance.Name.Should().Be("prod-grafana");
|
|
instance.Namespace.Should().Be("monitoring");
|
|
instance.Status.Should().Be(GrafanaInstanceStatus.Pending);
|
|
instance.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
|
|
|
// Grafana defaults
|
|
|
|
instance.Persistence.Should().BeFalse();
|
|
instance.PersistenceSize.Should().Be("10Gi");
|
|
instance.Oidc.Should().BeNull();
|
|
instance.Ingress.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_WithEmptyEnvironmentId_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act — Grafana must belong to an environment.
|
|
|
|
Action act = () => GrafanaInstance.Create(Guid.Empty, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Assert
|
|
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("environmentId");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_WithEmptyClusterId_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act — Grafana must be deployed to a specific cluster.
|
|
|
|
Action act = () => GrafanaInstance.Create(environmentId, Guid.Empty, "prod-grafana", "monitoring");
|
|
|
|
// Assert
|
|
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("clusterId");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_WithEmptyName_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act
|
|
|
|
Action act = () => GrafanaInstance.Create(environmentId, clusterId, "", "monitoring");
|
|
|
|
// Assert
|
|
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("name");
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_WithEmptyNamespace_ThrowsArgumentException()
|
|
{
|
|
// Arrange & Act
|
|
|
|
Action act = () => GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "");
|
|
|
|
// Assert
|
|
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("ns");
|
|
}
|
|
|
|
// ─── Persistence Configuration ──────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ConfigurePersistence_UpdatesSettings()
|
|
{
|
|
// Arrange — An operator wants Grafana to keep dashboards across restarts.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act
|
|
|
|
instance.ConfigurePersistence(enabled: true, size: "20Gi");
|
|
|
|
// Assert
|
|
|
|
instance.Persistence.Should().BeTrue();
|
|
instance.PersistenceSize.Should().Be("20Gi");
|
|
}
|
|
|
|
// ─── OIDC Configuration ─────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ConfigureOidc_SetsOidcProvider()
|
|
{
|
|
// Arrange — Integrate Grafana with an OIDC provider (e.g., Entra ID)
|
|
// so users authenticate with corporate credentials.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act
|
|
|
|
OidcConfiguration oidc = new(
|
|
Provider: "generic_oauth",
|
|
ClientId: "grafana-client-id",
|
|
ClientSecretRef: "vault://monitoring/grafana-oidc-client-secret",
|
|
AuthUrl: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/authorize",
|
|
TokenUrl: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token",
|
|
ApiUrl: "https://graph.microsoft.com/oidc/userinfo",
|
|
Scopes: "openid profile email",
|
|
RoleAttributePath: "contains(groups[*], 'admin-group-id') && 'Admin' || 'Viewer'",
|
|
AutoLogin: true);
|
|
|
|
instance.ConfigureOidc(oidc);
|
|
|
|
// Assert
|
|
|
|
instance.Oidc.Should().NotBeNull();
|
|
instance.Oidc!.Provider.Should().Be("generic_oauth");
|
|
instance.Oidc.ClientId.Should().Be("grafana-client-id");
|
|
instance.Oidc.ClientSecretRef.Should().Be("vault://monitoring/grafana-oidc-client-secret");
|
|
instance.Oidc.AutoLogin.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigureOidc_WithEmptyClientId_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act & Assert
|
|
|
|
OidcConfiguration oidc = new(
|
|
Provider: "generic_oauth",
|
|
ClientId: "",
|
|
ClientSecretRef: "vault://secret",
|
|
AuthUrl: "https://example.com/auth",
|
|
TokenUrl: "https://example.com/token",
|
|
ApiUrl: "https://example.com/userinfo",
|
|
Scopes: "openid",
|
|
RoleAttributePath: null,
|
|
AutoLogin: false);
|
|
|
|
Action act = () => instance.ConfigureOidc(oidc);
|
|
|
|
act.Should().Throw<ArgumentException>();
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveOidc_ClearsOidcConfiguration()
|
|
{
|
|
// Arrange — A Grafana instance with OIDC configured.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
OidcConfiguration oidc = new(
|
|
Provider: "generic_oauth",
|
|
ClientId: "client-id",
|
|
ClientSecretRef: "vault://secret",
|
|
AuthUrl: "https://example.com/auth",
|
|
TokenUrl: "https://example.com/token",
|
|
ApiUrl: "https://example.com/userinfo",
|
|
Scopes: "openid",
|
|
RoleAttributePath: null,
|
|
AutoLogin: false);
|
|
|
|
instance.ConfigureOidc(oidc);
|
|
|
|
// Act — Remove OIDC, fall back to local auth.
|
|
|
|
instance.RemoveOidc();
|
|
|
|
// Assert
|
|
|
|
instance.Oidc.Should().BeNull();
|
|
}
|
|
|
|
// ─── Ingress ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ConfigureIngress_SetsGrafanaRouting()
|
|
{
|
|
// Arrange — Publish Grafana externally with its own hostname.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act
|
|
|
|
GrafanaIngressConfiguration ingress = new(
|
|
Enabled: true,
|
|
Provider: IngressProvider.GatewayApi,
|
|
Hostname: "grafana.prod01.example.com",
|
|
TlsEnabled: true,
|
|
TlsCertificateMode: TlsCertificateMode.CertManager,
|
|
TlsSecretName: null,
|
|
ClusterIssuer: "letsencrypt-prod");
|
|
|
|
instance.ConfigureIngress(ingress);
|
|
|
|
// Assert
|
|
|
|
instance.Ingress.Should().NotBeNull();
|
|
instance.Ingress!.Enabled.Should().BeTrue();
|
|
instance.Ingress.Provider.Should().Be(IngressProvider.GatewayApi);
|
|
instance.Ingress.Hostname.Should().Be("grafana.prod01.example.com");
|
|
instance.Ingress.TlsCertificateMode.Should().Be(TlsCertificateMode.CertManager);
|
|
instance.Ingress.ClusterIssuer.Should().Be("letsencrypt-prod");
|
|
}
|
|
|
|
[Fact]
|
|
public void DisableIngress_ClearsIngressConfig()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
GrafanaIngressConfiguration ingress = new(
|
|
Enabled: true,
|
|
Provider: IngressProvider.GatewayApi,
|
|
Hostname: "grafana.example.com",
|
|
TlsEnabled: true,
|
|
TlsCertificateMode: TlsCertificateMode.Manual,
|
|
TlsSecretName: "grafana-tls");
|
|
|
|
instance.ConfigureIngress(ingress);
|
|
|
|
// Act
|
|
|
|
instance.DisableIngress();
|
|
|
|
// Assert
|
|
|
|
instance.Ingress.Should().BeNull();
|
|
}
|
|
|
|
// ─── Dashboards ─────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void AddDashboard_StoresDashboardDefinition()
|
|
{
|
|
// Arrange — Dashboards are JSON definitions deployed as ConfigMaps
|
|
// with the grafana_dashboard=1 label so the sidecar picks them up.
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act — Add a Kubernetes cluster dashboard.
|
|
|
|
GrafanaDashboard dashboard = new(
|
|
Name: "k8s-cluster",
|
|
DisplayName: "Kubernetes Cluster",
|
|
Category: DashboardCategory.Kubernetes,
|
|
JsonContent: """{"dashboard": {"title": "K8s Cluster"}}""");
|
|
|
|
instance.AddDashboard(dashboard);
|
|
|
|
// Assert
|
|
|
|
instance.Dashboards.Should().HaveCount(1);
|
|
instance.Dashboards[0].Name.Should().Be("k8s-cluster");
|
|
instance.Dashboards[0].Category.Should().Be(DashboardCategory.Kubernetes);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddDashboard_WithDuplicateName_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
GrafanaDashboard dashboard = new(
|
|
Name: "k8s-cluster",
|
|
DisplayName: "Kubernetes Cluster",
|
|
Category: DashboardCategory.Kubernetes,
|
|
JsonContent: """{"dashboard": {}}""");
|
|
|
|
instance.AddDashboard(dashboard);
|
|
|
|
// Act & Assert — Can't add two dashboards with the same name.
|
|
|
|
Action act = () => instance.AddDashboard(dashboard);
|
|
|
|
act.Should().Throw<InvalidOperationException>();
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveDashboard_DeletesByName()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
instance.AddDashboard(new GrafanaDashboard("k8s-cluster", "Kubernetes Cluster", DashboardCategory.Kubernetes, "{}"));
|
|
instance.AddDashboard(new GrafanaDashboard("istio-mesh", "Istio Mesh", DashboardCategory.Istio, "{}"));
|
|
|
|
// Act
|
|
|
|
instance.RemoveDashboard("k8s-cluster");
|
|
|
|
// Assert
|
|
|
|
instance.Dashboards.Should().HaveCount(1);
|
|
instance.Dashboards[0].Name.Should().Be("istio-mesh");
|
|
}
|
|
|
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void MarkRunning_SetsStatusToRunning()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act
|
|
|
|
instance.MarkRunning();
|
|
|
|
// Assert
|
|
|
|
instance.Status.Should().Be(GrafanaInstanceStatus.Running);
|
|
}
|
|
|
|
[Fact]
|
|
public void MarkDegraded_SetsStatusToDegraded()
|
|
{
|
|
// Arrange
|
|
|
|
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
|
|
|
// Act
|
|
|
|
instance.MarkDegraded();
|
|
|
|
// Assert
|
|
|
|
instance.Status.Should().Be(GrafanaInstanceStatus.Degraded);
|
|
}
|
|
}
|