319 lines
12 KiB
C#
319 lines
12 KiB
C#
using System.Text;
|
|
using EntKube.Clusters.Domain;
|
|
using k8s;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace EntKube.Clusters.Features.AdoptCluster.Components.Grafana;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public class GrafanaInstaller : IComponentInstaller
|
|
{
|
|
private readonly ILogger<GrafanaInstaller> logger;
|
|
|
|
private const string DefaultNamespace = "monitoring";
|
|
private const string HelmRepo = "https://prometheus-community.github.io/helm-charts";
|
|
|
|
public string ComponentName => "grafana";
|
|
|
|
public GrafanaInstaller(ILogger<GrafanaInstaller> logger)
|
|
{
|
|
this.logger = logger;
|
|
}
|
|
|
|
public async Task<InstallResult> InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
|
|
{
|
|
string targetNamespace = options.Namespace ?? DefaultNamespace;
|
|
List<string> 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<string> { "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<bool> 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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public async Task<InstallResult> ConfigureAsync(KubernetesCluster cluster, ComponentConfiguration configuration, CancellationToken ct = default)
|
|
{
|
|
string targetNamespace = configuration.Namespace ?? DefaultNamespace;
|
|
List<string> actions = new();
|
|
|
|
string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-grafana-cfg-{Guid.NewGuid()}.kubeconfig");
|
|
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
|
|
|
|
List<string> 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<string> { $"Error: {ex.Message}" });
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempKubeConfig))
|
|
{
|
|
File.Delete(tempKubeConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public async Task<InstallResult> UninstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
|
|
{
|
|
string targetNamespace = options.Namespace ?? DefaultNamespace;
|
|
List<string> 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<string>());
|
|
}
|
|
|
|
// 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<string> { $"Error: {ex.Message}" });
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempKubeConfig))
|
|
{
|
|
File.Delete(tempKubeConfig);
|
|
}
|
|
}
|
|
}
|
|
}
|