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);
}
}
}
}