271 lines
10 KiB
C#
271 lines
10 KiB
C#
using System.Text;
|
|
using EntKube.Clusters.Domain;
|
|
using k8s;
|
|
using k8s.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace EntKube.Clusters.Features.AdoptCluster.Components.CertManager;
|
|
|
|
/// <summary>
|
|
/// Installs cert-manager via Helm. cert-manager provides the CRD runtime for
|
|
/// Certificate, Issuer, and ClusterIssuer resources. Let's Encrypt ClusterIssuer
|
|
/// configuration is handled separately by the LetsEncryptInstaller.
|
|
///
|
|
/// What gets deployed:
|
|
/// 1. cert-manager namespace with controller, webhook, and cainjector
|
|
/// 2. CRDs for Certificate, Issuer, ClusterIssuer, etc.
|
|
///
|
|
/// Configuration keys:
|
|
/// - "replicas": cert-manager controller replicas (default: 1)
|
|
/// </summary>
|
|
public class CertManagerInstaller : IComponentInstaller
|
|
{
|
|
private readonly ILogger<CertManagerInstaller> logger;
|
|
|
|
private const string DefaultVersion = "1.16.3";
|
|
private const string DefaultNamespace = "cert-manager";
|
|
private const string HelmRepo = "https://charts.jetstack.io";
|
|
|
|
public string ComponentName => "cert-manager";
|
|
|
|
public CertManagerInstaller(ILogger<CertManagerInstaller> logger)
|
|
{
|
|
this.logger = logger;
|
|
}
|
|
|
|
public async Task<InstallResult> InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
|
|
{
|
|
string version = options.Version ?? DefaultVersion;
|
|
string targetNamespace = options.Namespace ?? DefaultNamespace;
|
|
List<string> actions = new();
|
|
|
|
string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-certmgr-{Guid.NewGuid()}.kubeconfig");
|
|
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
|
|
|
|
// Ensure the namespace exists.
|
|
|
|
Kubernetes client = BuildClient(cluster);
|
|
await EnsureNamespace(client, targetNamespace, ct);
|
|
actions.Add($"Ensured namespace '{targetNamespace}'");
|
|
|
|
// Install cert-manager with CRDs via Helm.
|
|
|
|
await RunCommand("helm",
|
|
$"upgrade --install cert-manager cert-manager " +
|
|
$"--repo {HelmRepo} --version v{version} " +
|
|
$"--namespace {targetNamespace} " +
|
|
$"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
|
|
$"--set crds.enabled=true " +
|
|
$"--set replicaCount=1 " +
|
|
$"--set webhook.replicaCount=1 " +
|
|
$"--set cainjector.replicaCount=1 " +
|
|
$"--set resources.requests.cpu=50m " +
|
|
$"--set resources.requests.memory=128Mi " +
|
|
$"--set resources.limits.cpu=200m " +
|
|
$"--set resources.limits.memory=256Mi " +
|
|
$"--set prometheus.enabled=true " +
|
|
$"--set prometheus.servicemonitor.enabled=false " +
|
|
$"--wait --timeout 10m",
|
|
ct);
|
|
actions.Add($"Helm install cert-manager v{version} with CRDs");
|
|
|
|
logger.LogInformation("cert-manager {Version} installed on cluster {Cluster}",
|
|
version, cluster.Name);
|
|
|
|
return new InstallResult(
|
|
Success: true,
|
|
ComponentName: ComponentName,
|
|
Message: $"cert-manager {version} installed",
|
|
Actions: actions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to install cert-manager on cluster {Cluster}", cluster.Name);
|
|
actions.Add($"Error: {ex.Message}");
|
|
|
|
return new InstallResult(
|
|
Success: false,
|
|
ComponentName: ComponentName,
|
|
Message: $"Failed to install cert-manager: {ex.Message}",
|
|
Actions: actions);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempKubeConfig))
|
|
{
|
|
File.Delete(tempKubeConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reconfigures cert-manager. Supports:
|
|
/// - "replicas": controller replicas
|
|
/// </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-certmgr-cfg-{Guid.NewGuid()}.kubeconfig");
|
|
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
|
|
|
|
// Handle Helm-level configuration (replicas, resources).
|
|
|
|
List<string> setFlags = new();
|
|
|
|
if (configuration.Values.TryGetValue("replicas", out string? replicas))
|
|
{
|
|
setFlags.Add($"--set replicaCount={replicas}");
|
|
}
|
|
|
|
if (setFlags.Count > 0)
|
|
{
|
|
await RunCommand("helm",
|
|
$"upgrade cert-manager cert-manager --repo {HelmRepo} " +
|
|
$"--namespace {targetNamespace} " +
|
|
$"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
|
|
$"--reuse-values {string.Join(" ", setFlags)} --wait --timeout 5m",
|
|
ct);
|
|
actions.Add($"Updated cert-manager Helm release: {string.Join(", ", setFlags)}");
|
|
}
|
|
|
|
return new InstallResult(
|
|
Success: true,
|
|
ComponentName: ComponentName,
|
|
Message: "cert-manager reconfigured",
|
|
Actions: actions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to reconfigure cert-manager on cluster {Cluster}", cluster.Name);
|
|
return new InstallResult(
|
|
Success: false,
|
|
ComponentName: ComponentName,
|
|
Message: $"Failed to reconfigure cert-manager: {ex.Message}",
|
|
Actions: new List<string> { $"Error: {ex.Message}" });
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempKubeConfig))
|
|
{
|
|
File.Delete(tempKubeConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uninstalls cert-manager from the cluster by removing the Helm release.
|
|
/// Warning: this removes all CRDs too — any Certificate, Issuer, and
|
|
/// ClusterIssuer resources on the cluster will be deleted.
|
|
/// </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-certmgr-rm-{Guid.NewGuid()}.kubeconfig");
|
|
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
|
|
|
|
await RunCommand("helm",
|
|
$"uninstall cert-manager --namespace {targetNamespace} " +
|
|
$"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
|
|
$"--wait --timeout 5m",
|
|
ct);
|
|
actions.Add("Helm uninstall cert-manager");
|
|
|
|
logger.LogInformation("cert-manager uninstalled from cluster {Cluster}", cluster.Name);
|
|
|
|
return new InstallResult(
|
|
Success: true,
|
|
ComponentName: ComponentName,
|
|
Message: "cert-manager uninstalled successfully",
|
|
Actions: actions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to uninstall cert-manager from cluster {Cluster}", cluster.Name);
|
|
return new InstallResult(
|
|
Success: false,
|
|
ComponentName: ComponentName,
|
|
Message: $"Failed to uninstall cert-manager: {ex.Message}",
|
|
Actions: new List<string> { $"Error: {ex.Message}" });
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempKubeConfig))
|
|
{
|
|
File.Delete(tempKubeConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task EnsureNamespace(Kubernetes client, string namespaceName, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
await client.CoreV1.ReadNamespaceAsync(namespaceName, cancellationToken: ct);
|
|
}
|
|
catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
{
|
|
V1Namespace ns = new()
|
|
{
|
|
Metadata = new V1ObjectMeta
|
|
{
|
|
Name = namespaceName,
|
|
Labels = new Dictionary<string, string>
|
|
{
|
|
["app.kubernetes.io/managed-by"] = "entkube"
|
|
}
|
|
}
|
|
};
|
|
await client.CoreV1.CreateNamespaceAsync(ns, cancellationToken: ct);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private static Kubernetes BuildClient(KubernetesCluster cluster)
|
|
{
|
|
byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
|
|
using MemoryStream stream = new(kubeConfigBytes);
|
|
KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
|
|
return new Kubernetes(config);
|
|
}
|
|
}
|