From 0235ed7d52ebb38907723bc1d856c6a6541546ea Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 28 Oct 2024 12:30:59 +0100 Subject: [PATCH] Create cert-manager (#238) * Cert-Manager created Co-authored-by: Aaron Frey --------- Co-authored-by: Aaron Frey Co-authored-by: Thomas Michael --- README.md | 16 +- .../certManager-helm-values.ftl.yaml | 106 ++++++++++ argocd/argocd/argocd/values.ftl.yaml | 4 + .../projects/cluster-resources.ftl.yaml | 2 + docs/configuration.schema.json | 54 +++++ docs/developers.md | 6 + scripts/downloadHelmCharts.sh | 14 +- .../gitops/cli/GitopsPlaygroundCli.groovy | 34 +++- .../GitopsPlaygroundCliMainScripted.groovy | 1 + .../config/ApplicationConfigurator.groovy | 32 ++- .../gitops/config/ConfigConstants.groovy | 9 + .../gitops/config/schema/Schema.groovy | 31 +++ .../gitops/features/CertManager.groovy | 109 ++++++++++ .../gitops/ApplicationConfiguratorTest.groovy | 6 +- .../cloudogu/gitops/ApplicationTest.groovy | 2 +- .../ConfigToConfigFileConverterTest.groovy | 21 ++ .../gitops/features/CertManagerTest.groovy | 189 ++++++++++++++++++ 17 files changed, 617 insertions(+), 19 deletions(-) create mode 100644 applications/cluster-resources/certManager-helm-values.ftl.yaml create mode 100644 src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy create mode 100644 src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy diff --git a/README.md b/README.md index 396c164a..40514278 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Creates a complete GitOps-based operational stack on your Kubernetes clusters: * Notifications/Alerts: Grafana and ArgoCD can be predefined with either an external mailserver or [MailHog](https://github.com/mailhog/MailHog) for demo purposes. * Pipelines: Example applications using [Jenkins](#jenkins) with the [gitops-build-lib](https://github.com/cloudogu/gitops-build-lib) and [SCM-Manager](#scm-manager) * Ingress Controller: [ingress-nginx](https://github.com/kubernetes/ingress-nginx/) -* Certificate Management: (planned) +* Certificate Management: [cert-manager](#certificate-management) * Runs on: * local cluster (try it [with only one command](#tldr)), * in the public cloud, @@ -551,6 +551,20 @@ Set the parameter `--vault=[dev|prod]` to enable deployment of secret management secrets operator. See [Secrets management tools](#secrets-managment-tools) for details. +##### Certificate Management +Is implemented by cert-manager. +Set the parameter `--cert-manager` to enable cert-manager. +For custom images use this parameters to override defaults: +- --cert-manager-image +- --cert-manager-webhook-image +- --cert-manager-cainjector-image +- --cert-manager-acme-solver-image +- --cert-manager-startup-api-check-image + +i.e. +``` +--cert-manager-image someRegistry/cert-manager-controller:latest +``` ### Remove playground For k3d, you can just `k3d cluster delete gitops-playground`. This will delete the whole cluster. diff --git a/applications/cluster-resources/certManager-helm-values.ftl.yaml b/applications/cluster-resources/certManager-helm-values.ftl.yaml new file mode 100644 index 00000000..3f304e0b --- /dev/null +++ b/applications/cluster-resources/certManager-helm-values.ftl.yaml @@ -0,0 +1,106 @@ +<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> +<#if config.registry.createImagePullSecrets == true> +global: + imagePullSecrets: + - name: proxy-registry + + +<#if config.application.podResources == true> +resources: + limits: + cpu: '1' + memory: 400Mi + requests: + cpu: 30m + memory: 400Mi + +<#if config.application.skipCrds != true> +crds: + enabled: true + + +<#if config.features.certManager.helm.image?has_content> +<#assign imageObject = DockerImageParser.parse(config.features.certManager.helm.image)> +image: + repository: ${imageObject.registryAndRepositoryAsString} + tag: ${imageObject.tag} + + +<#--webhookImage--> +<#if config.application.podResources == true || config.features.certManager.helm.webhookImage?has_content> +webhook: + <#if config.application.podResources == true> + resources: + limits: + cpu: '1' + memory: 30Mi + requests: + cpu: 20m + memory: 30Mi + + <#if config.features.certManager.helm.webhookImage?has_content> + <#assign imageObject = DockerImageParser.parse(config.features.certManager.helm.webhookImage)> + image: + repository: ${imageObject.registryAndRepositoryAsString} + tag: ${imageObject.tag} + + +<#--cainjectorImage--> +<#if config.application.podResources == true || config.features.certManager.helm.cainjectorImage?has_content> +cainjector: + <#if config.application.podResources > + resources: + limits: + cpu: '1' + memory: 400Mi + requests: + cpu: 30m + memory: 400Mi + + <#if config.features.certManager.helm.cainjectorImage?has_content> + <#assign imageObject = DockerImageParser.parse(config.features.certManager.helm.cainjectorImage)> + image: + repository: ${imageObject.registryAndRepositoryAsString} + tag: ${imageObject.tag} + + + +<#--acmeSolverImage--> +<#if config.application.podResources == true || config.features.certManager.helm.acmeSolverImage?has_content> +acmesolver: + <#if config.application.podResources > + resources: + limits: + cpu: '1' + memory: 400Mi + requests: + cpu: 30m + memory: 400Mi + + <#if config.features.certManager.helm.acmeSolverImage?has_content> + <#assign imageObject = DockerImageParser.parse(config.features.certManager.helm.acmeSolverImage)> + image: + repository: ${imageObject.registryAndRepositoryAsString} + tag: ${imageObject.tag} + + + +<#--startupAPICheckImage--> +<#if config.application.podResources == true || config.features.certManager.helm.startupAPICheckImage?has_content> +startupapicheck: + <#if config.application.podResources > + resources: + limits: + cpu: '1' + memory: 400Mi + requests: + cpu: 30m + memory: 400Mi + + <#if config.features.certManager.helm.startupAPICheckImage?has_content> + <#assign imageObject = DockerImageParser.parse(config.features.certManager.helm.startupAPICheckImage)> + image: + repository: ${imageObject.registryAndRepositoryAsString} + tag: ${imageObject.tag} + + diff --git a/argocd/argocd/argocd/values.ftl.yaml b/argocd/argocd/argocd/values.ftl.yaml index 0ad77062..2d9ec83e 100644 --- a/argocd/argocd/argocd/values.ftl.yaml +++ b/argocd/argocd/argocd/values.ftl.yaml @@ -105,6 +105,10 @@ argo-cd: name: codecentric type: helm url: https://codecentric.github.io/helm-charts + cert-manager: + name: cert-manager + type: helm + url: https://charts.jetstack.io argo-helm-repo: type: helm url: https://argoproj.github.io/argo-helm diff --git a/argocd/argocd/projects/cluster-resources.ftl.yaml b/argocd/argocd/projects/cluster-resources.ftl.yaml index 128fa0fb..1b2a70e7 100644 --- a/argocd/argocd/projects/cluster-resources.ftl.yaml +++ b/argocd/argocd/projects/cluster-resources.ftl.yaml @@ -22,12 +22,14 @@ spec: - ${scmm.baseUrl}/repo/3rd-party-dependencies/ingress-nginx - ${scmm.baseUrl}/repo/3rd-party-dependencies/external-secrets - ${scmm.baseUrl}/repo/3rd-party-dependencies/vault + - ${scmm.baseUrl}/repo/3rd-party-dependencies/cert-manager <#else> - https://prometheus-community.github.io/helm-charts - https://codecentric.github.io/helm-charts - https://kubernetes.github.io/ingress-nginx - https://helm.releases.hashicorp.com - https://charts.external-secrets.io + - https://charts.jetstack.io # allow to only see application resources from the specified namespace diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index ce9b2b5e..f0f3126e 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -169,6 +169,60 @@ "additionalProperties" : false, "description" : "Configuration Parameter for the ArgoCD Operator" }, + "certManager" : { + "type" : "object", + "properties" : { + "active" : { + "type" : "boolean", + "description" : "Sets and enables Cert Manager" + }, + "helm" : { + "type" : "object", + "properties" : { + "acmeSolverImage" : { + "type" : "string", + "description" : "Sets acmeSolver Image for Cert Manager" + }, + "cainjectorImage" : { + "type" : "string", + "description" : "Sets cainjector Image for Cert Manager" + }, + "chart" : { + "type" : "string", + "description" : "Name of the Helm chart" + }, + "image" : { + "type" : "string", + "description" : "Sets image for Cert Manager" + }, + "repoURL" : { + "type" : "string", + "description" : "Repository url from which the Helm chart should be obtained" + }, + "startupAPICheckImage" : { + "type" : "string", + "description" : "Sets startupAPICheck Image for Cert Manager" + }, + "values" : { + "$ref" : "#/$defs/Map(String,Object)", + "description" : "Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration" + }, + "version" : { + "type" : "string", + "description" : "The version of the Helm chart to be installed" + }, + "webhookImage" : { + "type" : "string", + "description" : "Sets webhook Image for Cert Manager" + } + }, + "additionalProperties" : false, + "description" : "Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors." + } + }, + "additionalProperties" : false, + "description" : "Config parameters for the Cert Manager" + }, "exampleApps" : { "type" : "object", "properties" : { diff --git a/docs/developers.md b/docs/developers.md index 37490140..7795f357 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -391,6 +391,7 @@ notary: Then install it like so: ```bash +helm repo add harbor https://helm.goharbor.io helm upgrade -i my-harbor harbor/harbor -f harbor-values.yaml --version 1.14.2 --namespace harbor --create-namespace ``` Once it's up and running either create your own private project or just set the existing `library` to private: @@ -479,6 +480,11 @@ skopeo copy docker://quay.io/prometheus/prometheus:v2.51.2 --dest-creds Proxy:Pr skopeo copy docker://quay.io/prometheus-operator/prometheus-config-reloader:v0.73.2 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/prometheus-config-reloader skopeo copy docker://grafana/grafana:10.4.1 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/grafana skopeo copy docker://quay.io/kiwigrid/k8s-sidecar:1.27.4 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/k8s-sidecar +# Cert Manager images +skopeo copy docker://quay.io/jetstack/cert-manager-controller:v1.16.1 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/cert-manager-controller +skopeo copy docker://quay.io/jetstack/cert-manager-cainjector:v1.16.1 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/cert-manager-cainjector +skopeo copy docker://quay.io/jetstack/cert-manager-webhook:v1.16.1 --dest-creds Proxy:Proxy12345 --dest-tls-verify=false docker://localhost:30000/proxy/cert-manager-webhook + ``` * Deploy playground: diff --git a/scripts/downloadHelmCharts.sh b/scripts/downloadHelmCharts.sh index c5ca9922..509f2cf9 100755 --- a/scripts/downloadHelmCharts.sh +++ b/scripts/downloadHelmCharts.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o errexit -o nounset -o pipefail -charts=( 'monitoring' 'externalSecrets' 'vault' 'mailhog' 'ingressNginx') +charts=( 'monitoring' 'externalSecrets' 'vault' 'mailhog' 'ingressNginx' 'certManager') APPLICATION_CONFIGURATOR_GROOVY="${1:-src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy}" tmpRepoFile="$(mktemp)" @@ -10,12 +10,18 @@ mkdir -p charts for chart in "${charts[@]}"; do chartDetails=$(grep -EA10 "${chart}.*:" "${APPLICATION_CONFIGURATOR_GROOVY}" \ - | grep -m1 -EA5 'helm.*:') - + | grep -m1 -EA5 'helm.*:' || true) + if [[ -z "$chartDetails" ]]; then + echo "Did not find chart details for chart $chart in file ${APPLICATION_CONFIGURATOR_GROOVY} " >&2 + exit 1 + fi repo=$(echo "$chartDetails" | grep -oP "repoURL\s*:\s*'\K[^']+") chart=$(echo "$chartDetails" | grep -oP "chart\s*:\s*'\K[^']+") version=$(echo "$chartDetails" | grep -oP "version\s*:\s*'\K[^']+") - + + # avoid Error: failed to untar: a file or directory with the name charts/$chart already exists + rm -rf "./charts/$chart" + helm repo add "$chart" "$repo" --repository-config="${tmpRepoFile}" helm pull --untar --untardir ./charts "$chart/$chart" --version "$version" --repository-config="${tmpRepoFile}" # Note that keeping charts as tgx would need only 1/10 of storage diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy index bd259c21..d4cc0054 100644 --- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy +++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy @@ -57,7 +57,7 @@ class GitopsPlaygroundCli implements Runnable { private String registryPasswordReadOnly @Option(names = ['--create-image-pull-secrets'], description = REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION) private Boolean createImagePullSecrets - + // args group jenkins @Option(names = ['--jenkins-url'], description = JENKINS_URL_DESCRIPTION) private String jenkinsUrl @@ -231,6 +231,28 @@ class GitopsPlaygroundCli implements Runnable { @Option(names = ['--ingress-nginx-image'], description = HELM_CONFIG_IMAGE_DESCRIPTION) private String ingressNginxImage + // args certManager + @Option(names = ['--cert-manager'], description = CERTMANAGER_ENABLE_DESCRIPTION) + private Boolean certManager + + @Option(names = ['--cert-manager-image'], description = CERTMANAGER_IMAGE_DESCRIPTION) + private String certManagerImage + + @Option(names = ['--cert-manager-webhook-image'], description = CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION) + private String webhookImage + + @Option(names = ['--cert-manager-cainjector-image'], description = CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION) + private String cainjectorImage + + @Option(names = ['--cert-manager-acme-solver-image'], description = CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION) + private String acmeSolverImage + + @Option(names = ['--cert-manager-startup-api-check-image'], description = CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION) + private String startupAPICheckImage + + + + @Override void run() { setLogging() @@ -508,6 +530,16 @@ class GitopsPlaygroundCli implements Runnable { image: ingressNginxImage ] ], + certManager: [ + active: certManager, + helm: [ + image: certManagerImage, + webhookImage: webhookImage, + cainjectorImage: cainjectorImage, + acmeSolverImage: acmeSolverImage, + startupAPICheckImage: startupAPICheckImage + ] + ], ] ] diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy index 3e1514e5..5abd09ff 100644 --- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy +++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy @@ -116,6 +116,7 @@ class GitopsPlaygroundCliMainScripted { new Content(config,k8sClient), new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, scmmRepoProvider), new IngressNginx(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), + new CertManager(config,fileSystemUtils, deployer, k8sClient, airGappedUtils), new Mailhog(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), new PrometheusStack(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, scmmRepoProvider), new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils), diff --git a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy index 2564eaed..207acf88 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy @@ -38,7 +38,7 @@ class ApplicationConfigurator { path : '', username : '', password : '', - // Alternative: Use different registries, e.g. in air-gapped envs + // Alternative: Use different registries, e.g. in air-gapped envs // "Proxy" registry for 3rd party images proxyUrl : '', proxyUsername : '', @@ -60,8 +60,8 @@ class ApplicationConfigurator { /* This is the URL configured in SCMM inside the Jenkins Plugin, e.g. at http://scmm.localhost/scm/admin/settings/jenkins We use the K8s service as default name here, because it is the only option: "jenkins.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9090) will not work on Windows and MacOS. - - For production we overwrite this when config.jenkins["url"] is set. + + For production we overwrite this when config.jenkins["url"] is set. See addJenkinsConfig() and the comment at scmm.urlForJenkins */ urlForScmm: "http://jenkins", // Set dynamically metricsUsername: 'metrics', @@ -87,12 +87,12 @@ class ApplicationConfigurator { /* This corresponds to the "Base URL" in SCMM Settings. We use the K8s service as default name here, to make the build on push feature (webhooks from SCMM to Jenkins that trigger builds) work in k3d. The webhook contains repository URLs that start with the "Base URL" Setting of SCMM. - Jenkins checks these repo URLs and triggers all builds that match repo URLs. + Jenkins checks these repo URLs and triggers all builds that match repo URLs. In k3d, we have to define the repos in Jenkins using the K8s Service name, because they are the only option. "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091) will not work on Windows and MacOS. So, we have to use the matching URL in SCMM as well. - - For production we overwrite this when config.scmm["url"] is set. + + For production we overwrite this when config.scmm["url"] is set. See addScmmConfig() */ urlForJenkins : 'http://scmm-scm-manager/scm', // set dynamically host : '', // Set dynamically @@ -170,7 +170,7 @@ class ApplicationConfigurator { smtpAddress: '', smtpPort : '', smtpUser : '', - smtpPassword : '', + smtpPassword : '', helm : [ chart : 'mailhog', repoURL: 'https://codecentric.github.io/helm-charts', @@ -227,7 +227,21 @@ class ApplicationConfigurator { version: '4.11.2', image: '', values: [:] - ], + ], + ], + certManager: [ + active: false, + helm : [ + chart: 'cert-manager', + repoURL: 'https://charts.jetstack.io', + version: '1.16.1', + values: [:], + image: '', + acmeSolverImage: '', + cainjectorImage: '', + startupAPICheckImage: '', + webhookImage: '' + ], ], exampleApps: [ petclinic: [ @@ -239,7 +253,7 @@ class ApplicationConfigurator { ] ] ]) - + private Map config private NetworkingUtils networkingUtils private FileSystemUtils fileSystemUtils diff --git a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy index 6e274b35..560f4e5f 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy @@ -139,6 +139,15 @@ interface ConfigConstants { String INGRESS_NGINX_DESCRIPTION = 'Config parameters for the NGINX Ingress Controller' String INGRESS_NGINX_ENABLE_DESCRIPTION = 'Sets and enables Nginx Ingress Controller' + // group CERTMANAGER + String CERTMANAGER_DESCRIPTION = 'Config parameters for the Cert Manager' + String CERTMANAGER_ENABLE_DESCRIPTION = 'Sets and enables Cert Manager' + String CERTMANAGER_IMAGE_DESCRIPTION = 'Sets image for Cert Manager' + String CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION = 'Sets webhook Image for Cert Manager' + String CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION = 'Sets cainjector Image for Cert Manager' + String CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION = 'Sets acmeSolver Image for Cert Manager' + String CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION = 'Sets startupAPICheck Image for Cert Manager' + // group helm String HELM_CONFIG_DESCRIPTION = 'Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors.' String HELM_CONFIG_CHART_DESCRIPTION = 'Name of the Helm chart' diff --git a/src/main/groovy/com/cloudogu/gitops/config/schema/Schema.groovy b/src/main/groovy/com/cloudogu/gitops/config/schema/Schema.groovy index 7ad1ed30..cdb5ed12 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/schema/Schema.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/schema/Schema.groovy @@ -1,6 +1,7 @@ //file:noinspection unused package com.cloudogu.gitops.config.schema + import com.fasterxml.jackson.annotation.JsonClassDescription import com.fasterxml.jackson.annotation.JsonPropertyDescription @@ -232,6 +233,8 @@ class Schema { SecretsSchema secrets @JsonPropertyDescription(INGRESS_NGINX_DESCRIPTION) IngressNginxSchema ingressNginx + @JsonPropertyDescription(CERTMANAGER_DESCRIPTION) + CertManagerSchema certManager @JsonPropertyDescription(EXAMPLE_APPS_DESCRIPTION) ExampleAppsSchema exampleApps } @@ -345,6 +348,34 @@ class Schema { } } + static class CertManagerSchema { + @JsonPropertyDescription(CERTMANAGER_ENABLE_DESCRIPTION) + Boolean active = false + + @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION) + CertManagerHelmSchema helm + static class CertManagerHelmSchema extends HelmConfig { + @JsonPropertyDescription(HELM_CONFIG_VALUES_DESCRIPTION) + Map values + + @JsonPropertyDescription(CERTMANAGER_IMAGE_DESCRIPTION) + String image = "" + + @JsonPropertyDescription(CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION) + String webhookImage = "" + + @JsonPropertyDescription(CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION) + String cainjectorImage = "" + + @JsonPropertyDescription(CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION) + String acmeSolverImage = "" + + @JsonPropertyDescription(CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION) + String startupAPICheckImage = "" + + } + } + static class ExampleAppsSchema { @JsonPropertyDescription(PETCLINIC_DESCRIPTION) ExampleAppSchema petclinic diff --git a/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy new file mode 100644 index 00000000..d8d61e32 --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy @@ -0,0 +1,109 @@ +package com.cloudogu.gitops.features + +import com.cloudogu.gitops.Feature +import com.cloudogu.gitops.FeatureWithImage +import com.cloudogu.gitops.config.Configuration +import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.utils.* +import freemarker.template.DefaultObjectWrapperBuilder +import groovy.util.logging.Slf4j +import groovy.yaml.YamlSlurper +import io.micronaut.core.annotation.Order +import jakarta.inject.Singleton + +import java.nio.file.Path + +@Slf4j +@Singleton +@Order(160) +class CertManager extends Feature implements FeatureWithImage{ + + static final String HELM_VALUES_PATH = "applications/cluster-resources/certManager-helm-values.ftl.yaml" + + private FileSystemUtils fileSystemUtils + private DeploymentStrategy deployer + private AirGappedUtils airGappedUtils + final K8sClient k8sClient + final Map config + final String namespace ="cert-manager" + + CertManager( + Configuration config, + FileSystemUtils fileSystemUtils, + DeploymentStrategy deployer, + K8sClient k8sClient, + AirGappedUtils airGappedUtils + ) { + this.deployer = deployer + this.config = config.getConfig() + this.fileSystemUtils = fileSystemUtils + this.k8sClient = k8sClient + this.airGappedUtils = airGappedUtils + } + + @Override + boolean isEnabled() { + return config.features['certManager']['active'] + } + + @Override + void enable() { + + def templatedMap = new YamlSlurper().parseText( + new TemplatingEngine().template(new File(HELM_VALUES_PATH), + [config: config, + // Allow for using static classes inside the templates + statics: new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build() + .getStaticModels(), + ])) as Map + + + + def valuesFromConfig = config['features']['certManager']['helm']['values'] as Map + + def mergedMap = MapUtils.deepMerge(valuesFromConfig, templatedMap) + + def tmpHelmValues = fileSystemUtils.createTempFile() + fileSystemUtils.writeYaml(mergedMap, tmpHelmValues.toFile()) + + //k8sClient.createNamespace("cert-manager") + + def helmConfig = config['features']['certManager']['helm'] + if (config.application['mirrorRepos']) { + log.debug("Mirroring repos: Deploying certManager from local git repo") + + def repoNamespaceAndName = airGappedUtils.mirrorHelmRepoToGit(config['features']['certManager']['helm'] as Map) + + String certManagerVersion = + new YamlSlurper().parse(Path.of("${config.application['localHelmChartFolder']}/${helmConfig['chart']}", + 'Chart.yaml'))['version'] + + deployer.deployFeature( + "${scmmUri}/repo/${repoNamespaceAndName}", + 'cert-manager', + '.', + certManagerVersion, + 'cert-manager', + 'cert-manager', + tmpHelmValues, DeploymentStrategy.RepoType.GIT) + } else { + deployer.deployFeature( + helmConfig['repoURL'] as String, + 'cert-manager', + helmConfig['chart'] as String, + helmConfig['version'] as String, + 'cert-manager', + 'cert-manager', + tmpHelmValues + ) + } + } + private URI getScmmUri() { + if (config.scmm['internal']) { + new URI('http://scmm-scm-manager.default.svc.cluster.local/scm') + } else { + new URI("${config.scmm['url']}/scm") + } + } + +} diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy index dc3b497d..f7b51623 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationConfiguratorTest.groovy @@ -263,11 +263,11 @@ images: @Test void "config file has only fields that are present in default values"() { - + // ⚠️ If you run into an endless loop in this test, you might have added a non-static class to Schema.grooy - + Map defaultConfig = applicationConfigurator.setConfig(almostEmptyConfig) - + def fields = getAllFieldNames(Schema.class).sort() def keys = getAllKeys(defaultConfig).sort() diff --git a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy index 2084f7ab..ffff316c 100644 --- a/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/ApplicationTest.groovy @@ -26,6 +26,6 @@ class ApplicationTest { .getBean(Application) def features = application.features.collect { it.class.simpleName } - assertThat(features).isEqualTo(["Registry", "ScmManager", "Jenkins", "Content", "ArgoCD", "IngressNginx", "Mailhog", "PrometheusStack", "ExternalSecretsOperator", "Vault"]) + assertThat(features).isEqualTo(["Registry", "ScmManager", "Jenkins", "Content", "ArgoCD", "IngressNginx", "CertManager", "Mailhog", "PrometheusStack", "ExternalSecretsOperator", "Vault"]) } } diff --git a/src/test/groovy/com/cloudogu/gitops/config/ConfigToConfigFileConverterTest.groovy b/src/test/groovy/com/cloudogu/gitops/config/ConfigToConfigFileConverterTest.groovy index 416364a4..81674643 100644 --- a/src/test/groovy/com/cloudogu/gitops/config/ConfigToConfigFileConverterTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/config/ConfigToConfigFileConverterTest.groovy @@ -169,6 +169,15 @@ class ConfigToConfigFileConverterTest { ] ], ], + certManager: [ + active: false, + helm : [ + chart: 'cert-manager', + repoURL: 'https://charts.jetstack.io', + version: '1.16.1', + values: [:] + ], + ], exampleApps: [ petclinic: [ baseDomain: 'base-domain' @@ -315,6 +324,18 @@ features: values: a: "b" image: "" + certManager: + active: false + helm: + chart: "cert-manager" + repoURL: "https://charts.jetstack.io" + version: "1.16.1" + values: {} + image: "" + webhookImage: "" + cainjectorImage: "" + acmeSolverImage: "" + startupAPICheckImage: "" exampleApps: petclinic: baseDomain: "base-domain" diff --git a/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy new file mode 100644 index 00000000..c3e73959 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/features/CertManagerTest.groovy @@ -0,0 +1,189 @@ +package com.cloudogu.gitops.features + +import com.cloudogu.gitops.config.Configuration +import com.cloudogu.gitops.features.deployment.DeploymentStrategy +import com.cloudogu.gitops.utils.AirGappedUtils +import com.cloudogu.gitops.utils.CommandExecutorForTest +import com.cloudogu.gitops.utils.FileSystemUtils +import com.cloudogu.gitops.utils.K8sClient +import groovy.yaml.YamlSlurper +import jakarta.inject.Provider +import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor + +import java.nio.file.Files +import java.nio.file.Path + +import static org.assertj.core.api.Assertions.assertThat +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.* + +class CertManagerTest { + String chartVersion = "1.16.1" + Map config = [ + application: [ + username : 'abc', + password : '123', + remote : false, + namePrefix : "foo-", + podResources: false, + mirrorRepos : false, + skipCrds : false + ], + registry : [ + createImagePullSecrets: false + ], + scmm : [ + internal: true, + ], + features : [ + monitoring : [ + active: false + ], + + certManager: [ + active: true, + helm : [ + chart : 'cert-manager', + repoURL : 'https://charts.jetstack.io', + version : chartVersion, + values : [:], + image : '', + webhookImage : '', + cainjectorImage : '', + acmeSolverImage : '', + startupAPICheckImage: '', + ], + ], + ], + ] + + CommandExecutorForTest k8sCommandExecutor = new CommandExecutorForTest() + Path temporaryYamlFile + FileSystemUtils fileSystemUtils = new FileSystemUtils() + DeploymentStrategy deploymentStrategy = mock(DeploymentStrategy) + AirGappedUtils airGappedUtils = mock(AirGappedUtils) + + @Test + void 'Helm release is installed'() { + createCertManager().install() + + verify(deploymentStrategy).deployFeature('https://charts.jetstack.io', 'cert-manager', + 'cert-manager', chartVersion, 'cert-manager', + 'cert-manager', temporaryYamlFile) + } + + @Test + void 'Sets pod resource limits and requests'() { + config.application['podResources'] = true + + createCertManager().install() + + assertThat(parseActualYaml()['resources'] as Map).containsKeys('limits', 'requests') + assertThat(parseActualYaml()['cainjector']['resources'] as Map).containsKeys('limits', 'requests') + assertThat(parseActualYaml()['webhook']['resources'] as Map).containsKeys('limits', 'requests') + } + + @Test + void "is disabled via active flag"() { + config['features']['certManager']['active'] = false + createCertManager().install() + assertThat(temporaryYamlFile).isNull() + } + + @Test + void 'helm release is installed in air-gapped mode'() { + config.application['mirrorRepos'] = true + when(airGappedUtils.mirrorHelmRepoToGit(any(Map))).thenReturn('a/b') + + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) + config.application['localHelmChartFolder'] = rootChartsFolder.toString() + + Path SourceChart = rootChartsFolder.resolve('cert-manager') + Files.createDirectories(SourceChart) + + Map ChartYaml = [version: chartVersion] + fileSystemUtils.writeYaml(ChartYaml, SourceChart.resolve('Chart.yaml').toFile()) + + createCertManager().install() + + def helmConfig = ArgumentCaptor.forClass(Map) + verify(airGappedUtils).mirrorHelmRepoToGit(helmConfig.capture()) + assertThat(helmConfig.value.chart).isEqualTo('cert-manager') + // check existing value, but its not used in deploy. + assertThat(helmConfig.value.repoURL).isEqualTo('https://charts.jetstack.io') + assertThat(helmConfig.value.version).isEqualTo(chartVersion) + // important check: repoUrl is overridden with our values. + verify(deploymentStrategy).deployFeature( + 'http://scmm-scm-manager.default.svc.cluster.local/scm/repo/a/b', + 'cert-manager', '.', chartVersion, 'cert-manager', + 'cert-manager', temporaryYamlFile, DeploymentStrategy.RepoType.GIT) + } + + @Test + void 'check images are overriddes'() { + + // Prep + config.application['mirrorRepos'] = true + // test values + config.features['certManager']['helm']['image'] = "this.is.my.registry:30000/this.is.my.repository/myImage:1" + config.features['certManager']['helm']['webhookImage'] = "this.is.my.registry:30000/this.is.my.repository/myWebhook:2" + config.features['certManager']['helm']['cainjectorImage'] = "this.is.my.registry:30000/this.is.my.repository/myCainjectorImage:3" + config.features['certManager']['helm']['acmeSolverImage'] = "this.is.my.registry:30000/this.is.my.repository/myAcmeSolverImage:4" + config.features['certManager']['helm']['startupAPICheckImage'] = "this.is.my.registry:30000/this.is.my.repository/myStartupAPICheckImage:5" + when(airGappedUtils.mirrorHelmRepoToGit(any(Map))).thenReturn('a/b') + Path rootChartsFolder = Files.createTempDirectory(this.class.getSimpleName()) + config.application['localHelmChartFolder'] = rootChartsFolder.toString() + + Path SourceChart = rootChartsFolder.resolve('cert-manager') + Files.createDirectories(SourceChart) + + Map ChartYaml = [version: chartVersion] + fileSystemUtils.writeYaml(ChartYaml, SourceChart.resolve('Chart.yaml').toFile()) + createCertManager().install() + + def templateFile = parseActualYaml() + + // Cert-Manager + assertThat(parseActualYaml()['image']['repository'] as String).isEqualTo('this.is.my.registry:30000/this.is.my.repository/myImage') + assertThat(parseActualYaml()['image']['tag'] as String).isEqualTo('1') + // myWebhook + assertThat(parseActualYaml()['webhook']['image']['repository'] as String).isEqualTo('this.is.my.registry:30000/this.is.my.repository/myWebhook') + assertThat(parseActualYaml()['webhook']['image']['tag'] as String).isEqualTo('2') + // cainjectorImage + assertThat(parseActualYaml()['cainjector']['image']['repository'] as String).isEqualTo('this.is.my.registry:30000/this.is.my.repository/myCainjectorImage') + assertThat(parseActualYaml()['cainjector']['image']['tag'] as String).isEqualTo('3') + // myWebhook + assertThat(parseActualYaml()['acmesolver']['image']['repository'] as String).isEqualTo('this.is.my.registry:30000/this.is.my.repository/myAcmeSolverImage') + assertThat(parseActualYaml()['acmesolver']['image']['tag'] as String).isEqualTo('4') + // myWebhook + assertThat(parseActualYaml()['startupapicheck']['image']['repository'] as String).isEqualTo('this.is.my.registry:30000/this.is.my.repository/myStartupAPICheckImage') + assertThat(parseActualYaml()['startupapicheck']['image']['tag'] as String).isEqualTo('5') + + } + + private CertManager createCertManager() { + // We use the real FileSystemUtils and not a mock to make sure file editing works as expected + + def configuration = new Configuration(config) + new CertManager(new Configuration(config), new FileSystemUtils() { + @Override + Path createTempFile() { + def ret = super.createTempFile() + temporaryYamlFile = Path.of(ret.toString().replace(".ftl", "")) // Path after template invocation + + return ret + } + }, deploymentStrategy, new K8sClient(k8sCommandExecutor, new FileSystemUtils(), new Provider() { + @Override + Configuration get() { + configuration + } + }), airGappedUtils) + } + + private Map parseActualYaml() { + def ys = new YamlSlurper() + return ys.parse(temporaryYamlFile) as Map + } +}