mirror of
https://github.com/dokku/dokku.git
synced 2026-05-18 05:05:46 +02:00
333 lines
9.5 KiB
Go
333 lines
9.5 KiB
Go
package scheduler_k3s
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
)
|
|
|
|
// TLSSecretValues contains the values for a TLS secret helm chart
|
|
type TLSSecretValues struct {
|
|
Global TLSSecretGlobalValues `yaml:"global"`
|
|
}
|
|
|
|
// TLSSecretGlobalValues contains the global values for TLS secret
|
|
type TLSSecretGlobalValues struct {
|
|
AppName string `yaml:"app_name"`
|
|
Namespace string `yaml:"namespace"`
|
|
CertChecksum string `yaml:"cert_checksum"`
|
|
TLSCrt string `yaml:"tls_crt"`
|
|
TLSKey string `yaml:"tls_key"`
|
|
}
|
|
|
|
// GetCertContent retrieves certificate content via the certs-get trigger
|
|
func GetCertContent(appName string, keyType string) (string, error) {
|
|
result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
Trigger: "certs-get",
|
|
Args: []string{appName, keyType},
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get cert content: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return "", fmt.Errorf("certs-get trigger returned non-zero exit code")
|
|
}
|
|
return result.StdoutContents(), nil
|
|
}
|
|
|
|
// CertsExist checks if certificates exist for an app
|
|
func CertsExist(appName string) bool {
|
|
result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
|
Trigger: "certs-exists",
|
|
Args: []string{appName},
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(result.StdoutContents()) == "true"
|
|
}
|
|
|
|
// ComputeCertChecksum computes sha224 of combined cert+key content
|
|
// SHA224 produces a 56-character hex string which fits within Kubernetes label limit of 63 chars
|
|
func ComputeCertChecksum(certContent, keyContent string) string {
|
|
combined := certContent + keyContent
|
|
hash := sha256.Sum224([]byte(combined))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// GetTLSSecretReleaseName returns the helm release name for TLS secret
|
|
func GetTLSSecretReleaseName(appName string) string {
|
|
return fmt.Sprintf("tls-%s", appName)
|
|
}
|
|
|
|
// CreateOrUpdateTLSSecret creates or updates a TLS secret helm chart for an app
|
|
func CreateOrUpdateTLSSecret(ctx context.Context, appName string) error {
|
|
if err := isKubernetesAvailable(); err != nil {
|
|
common.LogDebug("kubernetes not available, skipping TLS secret creation")
|
|
return nil
|
|
}
|
|
|
|
scheduler := common.PropertyGetDefault("scheduler", appName, "selected", "")
|
|
globalScheduler := common.PropertyGetDefault("scheduler", "--global", "selected", "docker-local")
|
|
if scheduler == "" {
|
|
scheduler = globalScheduler
|
|
}
|
|
if scheduler != "k3s" {
|
|
common.LogDebug("app does not use k3s scheduler, skipping TLS secret creation")
|
|
return nil
|
|
}
|
|
|
|
certContent, err := GetCertContent(appName, "crt")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get certificate: %w", err)
|
|
}
|
|
|
|
keyContent, err := GetCertContent(appName, "key")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get key: %w", err)
|
|
}
|
|
|
|
checksum := ComputeCertChecksum(certContent, keyContent)
|
|
|
|
chartDir, err := os.MkdirTemp("", "dokku-tls-chart-")
|
|
if err != nil {
|
|
return fmt.Errorf("error creating chart directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(chartDir)
|
|
|
|
if err := os.MkdirAll(filepath.Join(chartDir, "templates"), os.FileMode(0755)); err != nil {
|
|
return fmt.Errorf("error creating chart templates directory: %w", err)
|
|
}
|
|
|
|
namespace := getComputedNamespace(appName)
|
|
releaseName := GetTLSSecretReleaseName(appName)
|
|
|
|
chart := &Chart{
|
|
ApiVersion: "v2",
|
|
AppVersion: "1.0.0",
|
|
Name: releaseName,
|
|
Icon: "https://dokku.com/assets/dokku-logo.svg",
|
|
Version: "0.0.1",
|
|
}
|
|
|
|
err = writeYaml(WriteYamlInput{
|
|
Object: chart,
|
|
Path: filepath.Join(chartDir, "Chart.yaml"),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error writing chart: %w", err)
|
|
}
|
|
|
|
b, err := templates.ReadFile("templates/tls-secret-chart/templates/tls-secret.yaml")
|
|
if err != nil {
|
|
return fmt.Errorf("error reading tls-secret template: %w", err)
|
|
}
|
|
|
|
filename := filepath.Join(chartDir, "templates", "tls-secret.yaml")
|
|
err = os.WriteFile(filename, b, os.FileMode(0644))
|
|
if err != nil {
|
|
return fmt.Errorf("error writing tls-secret template: %w", err)
|
|
}
|
|
|
|
if os.Getenv("DOKKU_TRACE") == "1" {
|
|
common.CatFile(filename)
|
|
}
|
|
|
|
values := &TLSSecretValues{
|
|
Global: TLSSecretGlobalValues{
|
|
AppName: appName,
|
|
Namespace: namespace,
|
|
CertChecksum: checksum,
|
|
TLSCrt: base64.StdEncoding.EncodeToString([]byte(certContent)),
|
|
TLSKey: base64.StdEncoding.EncodeToString([]byte(keyContent)),
|
|
},
|
|
}
|
|
|
|
err = writeYaml(WriteYamlInput{
|
|
Object: values,
|
|
Path: filepath.Join(chartDir, "values.yaml"),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error writing values: %w", err)
|
|
}
|
|
|
|
if err := createKubernetesNamespace(ctx, namespace); err != nil {
|
|
return fmt.Errorf("error creating namespace: %w", err)
|
|
}
|
|
|
|
helmAgent, err := NewHelmAgent(namespace, DeployLogPrinter)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating helm agent: %w", err)
|
|
}
|
|
|
|
chartPath, err := filepath.Abs(chartDir)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting chart path: %w", err)
|
|
}
|
|
|
|
common.LogInfo1(fmt.Sprintf("Installing TLS certificate for %s", appName))
|
|
err = helmAgent.InstallOrUpgradeChart(ctx, ChartInput{
|
|
ChartPath: chartPath,
|
|
Namespace: namespace,
|
|
ReleaseName: releaseName,
|
|
Wait: true,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error installing TLS secret chart: %w", err)
|
|
}
|
|
|
|
if err := common.PropertyWrite("scheduler-k3s", appName, "tls-cert-imported", "true"); err != nil {
|
|
return fmt.Errorf("error setting tls-cert-imported property: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteTLSSecret deletes the TLS secret helm chart for an app
|
|
func DeleteTLSSecret(ctx context.Context, appName string) (bool, error) {
|
|
if err := isKubernetesAvailable(); err != nil {
|
|
common.LogDebug("kubernetes not available, skipping TLS secret deletion")
|
|
return false, nil
|
|
}
|
|
|
|
namespace := getComputedNamespace(appName)
|
|
releaseName := GetTLSSecretReleaseName(appName)
|
|
|
|
helmAgent, err := NewHelmAgent(namespace, DeployLogPrinter)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating helm agent: %w", err)
|
|
}
|
|
|
|
exists, err := helmAgent.ChartExists(releaseName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error checking if TLS secret chart exists: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
if err := common.PropertyDelete("scheduler-k3s", appName, "tls-cert-imported"); err != nil {
|
|
common.LogDebug(fmt.Sprintf("error clearing tls-cert-imported property: %v", err))
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
common.LogInfo1(fmt.Sprintf("Removing TLS certificate for %s", appName))
|
|
err = helmAgent.UninstallChart(releaseName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error uninstalling TLS secret chart: %w", err)
|
|
}
|
|
|
|
if err := common.PropertyDelete("scheduler-k3s", appName, "tls-cert-imported"); err != nil {
|
|
common.LogDebug(fmt.Sprintf("error clearing tls-cert-imported property: %v", err))
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// HasImportedTLSCert checks if an app has an imported TLS certificate
|
|
func HasImportedTLSCert(appName string) bool {
|
|
value := common.PropertyGetDefault("scheduler-k3s", appName, "tls-cert-imported", "")
|
|
return value == "true"
|
|
}
|
|
|
|
// TLSSecretExists checks if the TLS secret helm release exists in k8s
|
|
func TLSSecretExists(ctx context.Context, appName string) (bool, error) {
|
|
namespace := getComputedNamespace(appName)
|
|
releaseName := GetTLSSecretReleaseName(appName)
|
|
|
|
helmAgent, err := NewHelmAgent(namespace, DevNullPrinter)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating helm agent: %w", err)
|
|
}
|
|
|
|
return helmAgent.ChartExists(releaseName)
|
|
}
|
|
|
|
// TLSSecretNeedsUpdate checks if the TLS secret needs updating by comparing checksums
|
|
func TLSSecretNeedsUpdate(ctx context.Context, appName string) (bool, error) {
|
|
clientset, err := NewKubernetesClient()
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating kubernetes client: %w", err)
|
|
}
|
|
|
|
namespace := getComputedNamespace(appName)
|
|
secretName := fmt.Sprintf("tls-%s", appName)
|
|
|
|
secret, err := clientset.GetSecret(ctx, GetSecretInput{
|
|
Name: secretName,
|
|
Namespace: namespace,
|
|
})
|
|
if err != nil {
|
|
return true, nil
|
|
}
|
|
|
|
existingChecksum, ok := secret.Labels["dokku.com/cert-checksum"]
|
|
if !ok {
|
|
return true, nil
|
|
}
|
|
|
|
certContent, err := GetCertContent(appName, "crt")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
keyContent, err := GetCertContent(appName, "key")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
currentChecksum := ComputeCertChecksum(certContent, keyContent)
|
|
return existingChecksum != currentChecksum, nil
|
|
}
|
|
|
|
// syncExistingCertificates syncs certificates for all apps using k3s scheduler
|
|
func syncExistingCertificates() error {
|
|
if err := isKubernetesAvailable(); err != nil {
|
|
common.LogDebug("kubernetes not available, skipping certificate sync")
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
apps, err := common.DokkuApps()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list apps: %w", err)
|
|
}
|
|
|
|
for _, appName := range apps {
|
|
scheduler := common.PropertyGetDefault("scheduler", appName, "selected", "")
|
|
globalScheduler := common.PropertyGetDefault("scheduler", "--global", "selected", "docker-local")
|
|
if scheduler == "" {
|
|
scheduler = globalScheduler
|
|
}
|
|
if scheduler != "k3s" {
|
|
continue
|
|
}
|
|
|
|
if !CertsExist(appName) {
|
|
continue
|
|
}
|
|
|
|
needsUpdate, err := TLSSecretNeedsUpdate(ctx, appName)
|
|
if err != nil {
|
|
common.LogDebug(fmt.Sprintf("Error checking TLS secret for %s: %v", appName, err))
|
|
continue
|
|
}
|
|
|
|
if needsUpdate {
|
|
common.LogInfo1(fmt.Sprintf("Syncing TLS certificate for %s", appName))
|
|
if err := CreateOrUpdateTLSSecret(ctx, appName); err != nil {
|
|
common.LogWarn(fmt.Sprintf("Failed to sync TLS certificate for %s: %v", appName, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|