diff --git a/plugins/scheduler-k3s/functions.go b/plugins/scheduler-k3s/functions.go index 075fc2a34..d55398cb1 100644 --- a/plugins/scheduler-k3s/functions.go +++ b/plugins/scheduler-k3s/functions.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "os" + "path/filepath" "sort" "strconv" "strings" @@ -114,6 +115,123 @@ type WaitForPodToExistInput struct { LabelSelector string } +func applyClusterIssuers(ctx context.Context, appName string) (string, bool, error) { + chartDir, err := os.MkdirTemp("", "cluster-issuer-chart-") + if err != nil { + return "", false, fmt.Errorf("Error creating cluster-issuer chart directory: %w", err) + } + defer os.RemoveAll(chartDir) + + // create the chart.yaml + chart := &Chart{ + ApiVersion: "v2", + AppVersion: "1.0.0", + Name: appName, + Version: "0.0.1", + } + + err = writeYaml(WriteYamlInput{ + Object: chart, + Path: filepath.Join(chartDir, "Chart.yaml"), + }) + if err != nil { + return "", false, fmt.Errorf("Error writing cluster-issuer chart: %w", err) + } + + // create the values.yaml + issuerName := "letsencrypt-stag" + server := getComputedLetsencryptServer(appName) + if server == "prod" || server == "production" { + issuerName = "letsencrypt-prod" + } else if server != "stag" && server != "staging" { + return "", false, fmt.Errorf("Invalid letsencrypt server config: %s", server) + } + + tls := false + letsencryptEmailStag := getGlobalLetsencryptEmailStag() + letsencryptEmailProd := getGlobalLetsencryptEmailProd() + if issuerName == "letsencrypt-stag" { + tls = letsencryptEmailStag != "" + } + if issuerName == "letsencrypt-prod" { + tls = letsencryptEmailProd != "" + } + + clusterIssuerValues := ClusterIssuerValues{ + ClusterIssuers: map[string]ClusterIssuer{ + "letsencrypt-stag": { + Email: letsencryptEmailStag, + Enabled: letsencryptEmailStag != "", + Name: "letsencrypt-stag", + Server: "https://acme-staging-v02.api.letsencrypt.org/directory", + }, + "letsencrypt-prod": { + Email: letsencryptEmailProd, + Enabled: letsencryptEmailProd != "", + Name: "letsencrypt-prod", + Server: "https://acme-v02.api.letsencrypt.org/directory", + }, + }, + } + + if err := os.MkdirAll(filepath.Join(chartDir, "templates"), os.FileMode(0755)); err != nil { + return "", false, fmt.Errorf("Error creating cluster-issuer chart templates directory: %w", err) + } + + err = writeYaml(WriteYamlInput{ + Object: clusterIssuerValues, + Path: filepath.Join(chartDir, "values.yaml"), + }) + if err != nil { + return "", false, fmt.Errorf("Error writing chart: %w", err) + } + + // create the templates/cluster-issuer.yaml + b, err := templates.ReadFile("templates/chart/cluster-issuer.yaml") + if err != nil { + return "", false, fmt.Errorf("Error reading cluster-issuer template: %w", err) + } + + filename := filepath.Join(chartDir, "templates", "cluster-issuer.yaml") + err = os.WriteFile(filename, b, os.FileMode(0644)) + if err != nil { + return "", false, fmt.Errorf("Error writing cluster-issuer template: %w", err) + } + + if os.Getenv("DOKKU_TRACE") == "1" { + common.CatFile(filename) + } + + // install the chart + helmAgent, err := NewHelmAgent("cert-manager", DevNullPrinter) + if err != nil { + return issuerName, tls, fmt.Errorf("Error creating helm agent: %w", err) + } + + chartPath, err := filepath.Abs(chartDir) + if err != nil { + return issuerName, tls, fmt.Errorf("Error getting chart path: %w", err) + } + + timeoutDuration, err := time.ParseDuration("300s") + if err != nil { + return issuerName, tls, fmt.Errorf("Error parsing deploy timeout duration: %w", err) + } + + err = helmAgent.InstallOrUpgradeChart(ctx, ChartInput{ + ChartPath: chartPath, + Namespace: "cert-manager", + ReleaseName: "cluster-issuers", + RollbackOnFailure: true, + Timeout: timeoutDuration, + }) + if err != nil { + return issuerName, tls, fmt.Errorf("Error installing cluster-issuer chart: %w", err) + } + + return issuerName, tls, nil +} + func createKubernetesNamespace(ctx context.Context, namespaceName string) error { clientset, err := NewKubernetesClient() if err != nil { diff --git a/plugins/scheduler-k3s/template.go b/plugins/scheduler-k3s/template.go index 2dced9de1..766623326 100644 --- a/plugins/scheduler-k3s/template.go +++ b/plugins/scheduler-k3s/template.go @@ -6,9 +6,6 @@ import ( "os" "strings" - acmev1 "github.com/cert-manager/cert-manager/pkg/apis/acme/v1" - certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" - certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" "github.com/dokku/dokku/plugins/common" "gopkg.in/yaml.v3" batchv1 "k8s.io/api/batch/v1" @@ -25,7 +22,11 @@ type Chart struct { Version string `yaml:"version"` } -type Values struct { +type ClusterIssuerValues struct { + ClusterIssuers map[string]ClusterIssuer `yaml:"cluster_issuers"` +} + +type AppValues struct { Global GlobalValues `yaml:"global"` Processes map[string]ProcessValues `yaml:"processes"` } @@ -164,65 +165,11 @@ type ProcessTls struct { } type ClusterIssuer struct { - Email string - IngressClass string - Name string - Namespace string - Server string -} - -func templateKubernetesClusterIssuer(input ClusterIssuer) (certmanagerv1.ClusterIssuer, error) { - if input.Email == "" { - return certmanagerv1.ClusterIssuer{}, fmt.Errorf("Email cannot be empty") - } - if input.Server == "" { - return certmanagerv1.ClusterIssuer{}, fmt.Errorf("Server cannot be empty") - } - if input.Name == "" { - return certmanagerv1.ClusterIssuer{}, fmt.Errorf("Name cannot be empty") - } - if input.Namespace == "" { - return certmanagerv1.ClusterIssuer{}, fmt.Errorf("Namespace cannot be empty") - } - - if input.Server == "staging" { - input.Server = "https://acme-staging-v02.api.letsencrypt.org/directory" - } else if input.Server == "production" { - input.Server = "https://acme-v02.api.letsencrypt.org/directory" - } else { - return certmanagerv1.ClusterIssuer{}, fmt.Errorf("Server must be either staging or production") - } - - clusterIssuer := certmanagerv1.ClusterIssuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: input.Name, - Namespace: input.Namespace, - }, - Spec: certmanagerv1.IssuerSpec{ - IssuerConfig: certmanagerv1.IssuerConfig{ - ACME: &acmev1.ACMEIssuer{ - Email: input.Email, - Server: input.Server, - PrivateKey: certmanagermetav1.SecretKeySelector{ - LocalObjectReference: certmanagermetav1.LocalObjectReference{ - Name: input.Name, - }, - }, - Solvers: []acmev1.ACMEChallengeSolver{ - { - HTTP01: &acmev1.ACMEChallengeSolverHTTP01{ - Ingress: &acmev1.ACMEChallengeSolverHTTP01Ingress{ - Class: ptr.To(input.IngressClass), - }, - }, - }, - }, - }, - }, - }, - } - - return clusterIssuer, nil + Email string `yaml:"email"` + Enabled bool `yaml:"enabled"` + IngressClass string `yaml:"ingress_class"` + Name string `yaml:"name"` + Server string `yaml:"server"` } type Job struct { diff --git a/plugins/scheduler-k3s/templates/chart/cluster-issuer.yaml b/plugins/scheduler-k3s/templates/chart/cluster-issuer.yaml new file mode 100644 index 000000000..30fe0a2f7 --- /dev/null +++ b/plugins/scheduler-k3s/templates/chart/cluster-issuer.yaml @@ -0,0 +1,19 @@ +{{- range $name, $config := .Values.cluster_issuers }} +{{- if $config.enabled }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: {{ $config.name }} +spec: + acme: + email: {{ $config.email }} + server: {{ $config.server }} + privateKeySecretRef: + name: {{ $config.name }} + solvers: + - http01: + ingress: + class: {{ $config.ingress_class }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 78e7b310d..4be8e71d4 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -222,6 +222,11 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e return fmt.Errorf("Error loading environment for deployment: %w", err) } + issuerName, tlsEnabled, err := applyClusterIssuers(ctx, appName) + if err != nil { + return fmt.Errorf("Error applying cluster issuers: %w", err) + } + chartDir, err := os.MkdirTemp("", "dokku-chart-") if err != nil { return fmt.Errorf("Error creating chart directory: %w", err) @@ -273,22 +278,6 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e return fmt.Errorf("Error fetching cron entries: %w", err) } - issuerName := "letsencrypt-stag" - server := getComputedLetsencryptServer(appName) - if server == "prod" || server == "production" { - issuerName = "letsencrypt-prod" - } else if server != "stag" && server != "staging" { - return fmt.Errorf("Invalid letsencrypt server config: %s", server) - } - - tls := false - if issuerName == "letsencrypt-stag" { - tls = getGlobalLetsencryptEmailStag() != "" - } - if issuerName == "letsencrypt-prod" { - tls = getGlobalLetsencryptEmailProd() != "" - } - domains := []string{} if _, ok := processes["web"]; ok { err = common.PlugnTrigger("domains-vhost-enabled", []string{appName}...) @@ -322,7 +311,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e return fmt.Errorf("Error writing chart: %w", err) } - values := &Values{ + values := &AppValues{ Global: GlobalValues{ AppName: appName, DeploymentID: fmt.Sprint(deploymentId), @@ -379,7 +368,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e Domains: domains, PortMaps: []ProcessPortMap{}, TLS: ProcessTls{ - Enabled: tls, + Enabled: tlsEnabled, IssuerName: issuerName, }, }