using System.Text; using EntKube.Clusters.Domain; using k8s; using Microsoft.Extensions.Logging; namespace EntKube.Clusters.Features.AdoptCluster.Components.Grafana; /// /// Enables/installs Grafana on the cluster. In this platform, Grafana ships /// as part of the kube-prometheus-stack Helm release. This installer upgrades /// the existing monitoring stack with Grafana enabled. /// /// If the monitoring stack isn't installed yet, it delegates to the full monitoring /// install (which includes Grafana by default). /// /// Configuration mirrors Terraform: /// - Dashboard sidecar enabled, searches ALL namespaces /// - Admin credentials from Secret /// - Persistence enabled /// public class GrafanaInstaller : IComponentInstaller { private readonly ILogger logger; private const string DefaultNamespace = "monitoring"; private const string HelmRepo = "https://prometheus-community.github.io/helm-charts"; public string ComponentName => "grafana"; public GrafanaInstaller(ILogger logger) { this.logger = logger; } public async Task InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default) { string targetNamespace = options.Namespace ?? DefaultNamespace; List actions = new(); string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-grafana-{Guid.NewGuid()}.kubeconfig"); string valuesPath = Path.Combine(Path.GetTempPath(), $"entkube-grafana-values-{Guid.NewGuid()}.yaml"); try { await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct); // Check if kube-prometheus-stack is already installed // If yes, upgrade it with Grafana enabled // If no, install the full stack (Grafana is included by default) bool stackExists = await IsMonitoringStackInstalled(tempKubeConfig, cluster.ContextName, targetNamespace, ct); string values = GetGrafanaValues(); await File.WriteAllTextAsync(valuesPath, values, ct); if (stackExists) { // Upgrade existing monitoring stack to enable Grafana await RunCommand("helm", $"upgrade kube-prometheus-stack kube-prometheus-stack " + $"--repo {HelmRepo} " + $"--namespace {targetNamespace} " + $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " + $"--values \"{valuesPath}\" " + $"--reuse-values " + $"--wait --timeout 10m", ct); actions.Add("Upgraded kube-prometheus-stack with Grafana enabled"); } else { // The monitoring stack doesn't exist — inform the user to install monitoring first return new InstallResult( Success: false, ComponentName: ComponentName, Message: "Monitoring stack (kube-prometheus-stack) not found. Install the 'monitoring' component first — it includes Grafana.", Actions: new List { "Prerequisite: install 'monitoring' component first" }); } logger.LogInformation("Grafana enabled on cluster {Cluster}", cluster.Name); return new InstallResult( Success: true, ComponentName: ComponentName, Message: "Grafana enabled in monitoring stack", Actions: actions); } catch (Exception ex) { logger.LogError(ex, "Failed to enable Grafana on cluster {Cluster}", cluster.Name); actions.Add($"Error: {ex.Message}"); return new InstallResult( Success: false, ComponentName: ComponentName, Message: $"Failed to enable Grafana: {ex.Message}", Actions: actions); } finally { if (File.Exists(tempKubeConfig)) { File.Delete(tempKubeConfig); } if (File.Exists(valuesPath)) { File.Delete(valuesPath); } } } private async Task IsMonitoringStackInstalled(string kubeConfig, string contextName, string ns, CancellationToken ct) { try { string output = await RunCommand("helm", $"status kube-prometheus-stack --namespace {ns} --kubeconfig \"{kubeConfig}\" --kube-context \"{contextName}\"", ct); return output.Contains("STATUS: deployed"); } catch { return false; } } private static string GetGrafanaValues() { return """ grafana: enabled: true persistence: enabled: true size: 10Gi sidecar: dashboards: default: enabled: true label: grafana_dashboard searchNamespace: ALL datasources: default: enabled: true label: grafana_datasource searchNamespace: ALL resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi """; } private async Task RunCommand(string fileName, string arguments, CancellationToken ct) { System.Diagnostics.ProcessStartInfo psi = new(fileName, arguments) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; using System.Diagnostics.Process? process = System.Diagnostics.Process.Start(psi); if (process is null) { throw new InvalidOperationException($"Failed to start {fileName}"); } string output = await process.StandardOutput.ReadToEndAsync(ct); string error = await process.StandardError.ReadToEndAsync(ct); await process.WaitForExitAsync(ct); if (process.ExitCode != 0) { throw new InvalidOperationException($"{fileName} failed (exit {process.ExitCode}): {error}"); } return output; } /// /// Reconfigures Grafana within the monitoring stack. Supports: /// - "persistence": true/false to enable persistent storage /// - "persistenceSize": PVC size (e.g., "10Gi") /// - "enabled": true/false to enable/disable Grafana /// public async Task ConfigureAsync(KubernetesCluster cluster, ComponentConfiguration configuration, CancellationToken ct = default) { string targetNamespace = configuration.Namespace ?? DefaultNamespace; List actions = new(); string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-grafana-cfg-{Guid.NewGuid()}.kubeconfig"); try { await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct); List setFlags = new(); if (configuration.Values.TryGetValue("persistence", out string? persistence)) { setFlags.Add($"--set grafana.persistence.enabled={persistence}"); } if (configuration.Values.TryGetValue("persistenceSize", out string? size)) { setFlags.Add($"--set grafana.persistence.size={size}"); } if (configuration.Values.TryGetValue("enabled", out string? enabled)) { setFlags.Add($"--set grafana.enabled={enabled}"); } await RunCommand("helm", $"upgrade kube-prometheus-stack kube-prometheus-stack --repo {HelmRepo} " + $"--namespace {targetNamespace} " + $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " + $"--reuse-values {string.Join(" ", setFlags)} --wait --timeout 10m", ct); actions.Add($"Reconfigured Grafana: {string.Join(", ", configuration.Values.Select(kv => $"{kv.Key}={kv.Value}"))}"); return new InstallResult( Success: true, ComponentName: ComponentName, Message: "Grafana reconfigured", Actions: actions); } catch (Exception ex) { logger.LogError(ex, "Failed to reconfigure Grafana on cluster {Cluster}", cluster.Name); return new InstallResult( Success: false, ComponentName: ComponentName, Message: $"Failed to reconfigure Grafana: {ex.Message}", Actions: new List { $"Error: {ex.Message}" }); } finally { if (File.Exists(tempKubeConfig)) { File.Delete(tempKubeConfig); } } } /// /// Uninstalls Grafana by upgrading the kube-prometheus-stack release with /// Grafana disabled. Since Grafana is part of the monitoring stack, we /// cannot simply "helm uninstall" — we upgrade with grafana.enabled=false. /// public async Task UninstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default) { string targetNamespace = options.Namespace ?? DefaultNamespace; List actions = new(); string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-grafana-rm-{Guid.NewGuid()}.kubeconfig"); try { await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct); bool stackExists = await IsMonitoringStackInstalled(tempKubeConfig, cluster.ContextName, targetNamespace, ct); if (!stackExists) { return new InstallResult( Success: true, ComponentName: ComponentName, Message: "Monitoring stack not found — nothing to uninstall", Actions: new List()); } // Upgrade the monitoring stack with Grafana disabled. await RunCommand("helm", $"upgrade kube-prometheus-stack kube-prometheus-stack " + $"--repo {HelmRepo} " + $"--namespace {targetNamespace} " + $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " + $"--reuse-values --set grafana.enabled=false " + $"--wait --timeout 10m", ct); actions.Add("Disabled Grafana in kube-prometheus-stack"); logger.LogInformation("Grafana disabled on cluster {Cluster}", cluster.Name); return new InstallResult( Success: true, ComponentName: ComponentName, Message: "Grafana disabled in monitoring stack", Actions: actions); } catch (Exception ex) { logger.LogError(ex, "Failed to disable Grafana on cluster {Cluster}", cluster.Name); return new InstallResult( Success: false, ComponentName: ComponentName, Message: $"Failed to disable Grafana: {ex.Message}", Actions: new List { $"Error: {ex.Message}" }); } finally { if (File.Exists(tempKubeConfig)) { File.Delete(tempKubeConfig); } } } }