From 5d4b6ede75e0661d36fbb7168dcb690595022238 Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 13 Dec 2022 14:21:57 +0100 Subject: [PATCH] Helm: Support if statements at resource level Relates to https://github.com/quarkiverse/quarkus-helm/pull/146 --- .../helm/annotation/AddIfStatement.java | 35 ++++++++++++ .../dekorate/helm/annotation/HelmChart.java | 2 + .../listener/HelmWriterSessionListener.java | 56 ++++++++++++++++--- assets/config.md | 10 ++++ docs/configuration-guide.md | 10 ++++ docs/documentation/helm.md | 21 +++++++ .../src/main/resources/application.properties | 4 ++ .../src/test/resources/expected-ingress.yaml | 3 + 8 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/AddIfStatement.java diff --git a/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/AddIfStatement.java b/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/AddIfStatement.java new file mode 100644 index 000000000..422b225d0 --- /dev/null +++ b/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/AddIfStatement.java @@ -0,0 +1,35 @@ +/** + * Copyright 2018 The original authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + **/ +package io.dekorate.helm.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.CONSTRUCTOR, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AddIfStatement { + + String property(); + + String onResourceKind() default ""; + + String onResourceName() default ""; + + boolean withDefaultValue() default true; +} diff --git a/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/HelmChart.java b/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/HelmChart.java index 26ad7d78d..76f49f9a0 100644 --- a/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/HelmChart.java +++ b/annotations/helm-annotations/src/main/java/io/dekorate/helm/annotation/HelmChart.java @@ -86,4 +86,6 @@ ValueReference[] values() default {}; HelmExpression[] expressions() default {}; + + AddIfStatement[] addIfStatements() default {}; } diff --git a/annotations/helm-annotations/src/main/java/io/dekorate/helm/listener/HelmWriterSessionListener.java b/annotations/helm-annotations/src/main/java/io/dekorate/helm/listener/HelmWriterSessionListener.java index 31ff06d27..026a4b5e0 100644 --- a/annotations/helm-annotations/src/main/java/io/dekorate/helm/listener/HelmWriterSessionListener.java +++ b/annotations/helm-annotations/src/main/java/io/dekorate/helm/listener/HelmWriterSessionListener.java @@ -50,6 +50,7 @@ import io.dekorate.WithConfigReferences; import io.dekorate.WithProject; import io.dekorate.WithSession; +import io.dekorate.helm.config.AddIfStatement; import io.dekorate.helm.config.Annotation; import io.dekorate.helm.config.HelmChartConfig; import io.dekorate.helm.config.HelmExpression; @@ -77,11 +78,14 @@ public class HelmWriterSessionListener implements SessionListener, WithProject, private static final List ADDITIONAL_CHART_FILES = Arrays.asList("README.md", "LICENSE", "values.schema.json", "app-readme.md", "questions.yml", "questions.yaml", "requirements.yml", "requirements.yaml"); private static final String KIND = "kind"; + private static final String METADATA = "metadata"; + private static final String NAME = "name"; private static final String START_TAG = "{{"; private static final String END_TAG = "}}"; private static final String VALUES_START_TAG = START_TAG + " .Values."; private static final String VALUES_END_TAG = " " + END_TAG; private static final String EMPTY = ""; + private static final String IF_STATEMENT_START_TAG = "{{- if .Values.%s }}"; private static final String TEMPLATE_FUNCTION_START_TAG = "{{- define"; private static final String TEMPLATE_FUNCTION_END_TAG = "{{- end }}"; private static final String HELM_HELPER_PREFIX = "_"; @@ -129,7 +133,8 @@ public Map writeHelmFiles(Session session, Project project, Map artifacts = new HashMap<>(); if (helmConfig.isEnabled()) { validateHelmConfig(helmConfig); - List valuesReferences = mergeValuesReferencesFromDecorators(configReferences, session); + List valuesReferences = mergeValuesReferencesFromDecorators(configReferences, + helmConfig.getAddIfStatements(), session); try { LOGGER.info(String.format("Creating Helm Chart \"%s\"", helmConfig.getName())); @@ -240,10 +245,14 @@ private Map createEmptyChartFolder(HelmChartConfig helmConfig, P } private List mergeValuesReferencesFromDecorators(List configReferencesFromConfig, - Session session) { + AddIfStatement[] addIfStatements, Session session) { List configReferences = new LinkedList<>(); // From user configReferences.addAll(configReferencesFromConfig); + // From if statements: these are boolean values + for (AddIfStatement addIfStatement : addIfStatements) { + configReferences.add(new ConfigReference(addIfStatement.getProperty(), null, addIfStatement.getWithDefaultValue())); + } // From decorators for (WithConfigReferences decorator : session.getResourceRegistry().getConfigReferences()) { configReferences.addAll(decorator.getConfigReferences()); @@ -275,12 +284,7 @@ private Map createValuesYaml(HelmChartConfig helmConfig, List createValuesYaml(HelmChartConfig helmConfig, List mergeWithFileIfExists(Path inputDir, String file, Map data) { Map valuesAsMultiValueMap = toMultiValueMap(data); File templateValuesFile = inputDir.resolve(file).toFile(); @@ -411,6 +423,22 @@ private Map processTemplates(HelmChartConfig helmConfig, Path in adaptedString = functions + System.lineSeparator() + adaptedString; } + // Add if statements at resource level + for (AddIfStatement addIfStatement : helmConfig.getAddIfStatements()) { + if ((Strings.isNullOrEmpty(addIfStatement.getOnResourceKind()) + || Strings.equals(addIfStatement.getOnResourceKind(), kind)) + && (Strings.isNullOrEmpty(addIfStatement.getOnResourceName()) + || Strings.equals(addIfStatement.getOnResourceName(), getNameFromResource(resource)))) { + + adaptedString = String.format(IF_STATEMENT_START_TAG, deductProperty(helmConfig, addIfStatement.getProperty())) + + System.lineSeparator() + + adaptedString + + System.lineSeparator() + + TEMPLATE_FUNCTION_END_TAG + + System.lineSeparator(); + } + } + adaptedString = adaptedString .replaceAll(Pattern.quote("\"" + START_TAG), START_TAG) .replaceAll(Pattern.quote(END_TAG + "\""), END_TAG) @@ -428,6 +456,18 @@ private Map processTemplates(HelmChartConfig helmConfig, Path in return templates; } + private String getNameFromResource(Map resource) { + Object metadata = resource.get(METADATA); + if (metadata != null && metadata instanceof Map) { + Object name = ((Map) metadata).get(NAME); + if (name != null) { + return name.toString(); + } + } + + return null; + } + private Map processUserDefinedTemplates(Path inputDir, Map templates, Path templatesDir) throws IOException { Map functionsByResource = new HashMap<>(); diff --git a/assets/config.md b/assets/config.md index 2535c91ef..81c8717f0 100644 --- a/assets/config.md +++ b/assets/config.md @@ -481,6 +481,7 @@ The section below describes all the available subtypes. | dekorate.helm.extension | String | Extension of the Helm tarball file. | tar.gz | | dekorate.helm.tarFileClassifier | String | Classifier to be appended into the generated Helm tarball file name. | | | dekorate.helm.values | ValueReference[] | The configuration references to be mapped into the Helm values file. | | +| dekorate.helm.addIfStatements | AddIfStatement[] | The if statements to include in the generated resources. | | | dekorate.helm.notes | String | Template for the NOTES.txt file that will be included in the generated Helm Chart. It must be a classpath resource. | /NOTES.template.txt | | dekorate.helm.valuesRootAlias | String | Alias of the root element in the generated values file. | app | | dekorate.helm.inputFolder | String | The input folder in which to place the user-defined Helm files. These files will be used as inputs to populate the generated Helm files. At the moment, the only supported Helm files are the `values.yaml`, the `LICENSE`, the `NOTES.txt`, and the `README.md` files. The folder will be created in the path specified in the Dekorate input path property (`dekorate.options.input-path`, see the Dekorate options table [here](#generator-options)). | helm | @@ -523,6 +524,15 @@ The section below describes all the available subtypes. | value | String | The dependency repository. | (empty) | | expression | String | The complete Helm expression to be replaced with. If not provided, it will use `{{ .Values.. }}`. | (empty) | +### AddIfStatement + +| Property | Type | Description | Default Value | +|--------------------|----------|-------------------------------------------------------------------|---------------| +| property | String | The property to use in the if statement. | | +| onResourceKind | String | The resource kind where to include the if statement. | | +| onResourceName | String | The resource name where to include the if statement. | (empty) | +| withDefaultValue | Boolean | The default value of the property | true | + ## Tekton | Property | Type | Description | Default Value | |-------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md index e896b53fd..86be4dafa 100644 --- a/docs/configuration-guide.md +++ b/docs/configuration-guide.md @@ -547,6 +547,7 @@ The section below describes all the available subtypes. | dekorate.helm.extension | String | Extension of the Helm tarball file. | tar.gz | | dekorate.helm.tarFileClassifier | String | Classifier to be appended into the generated Helm tarball file name. | | | dekorate.helm.values | ValueReference[] | The configuration references to be mapped into the Helm values file. | | +| dekorate.helm.addIfStatements | AddIfStatement[] | The if statements to include in the generated resources. | | | dekorate.helm.notes | String | Template for the NOTES.txt file that will be included in the generated Helm Chart. It must be a classpath resource. | /NOTES.template.txt | | dekorate.helm.valuesRootAlias | String | Alias of the root element in the generated values file. | app | | dekorate.helm.inputFolder | String | The input folder in which to place the user-defined Helm files. These files will be used as inputs to populate the generated Helm files. At the moment, the only supported Helm files are the `values.yaml`, the `LICENSE`, the `NOTES.txt`, and the `README.md` files. The folder will be created in the path specified in the Dekorate input path property (`dekorate.options.input-path`, see the Dekorate options table [here](#generator-options)). | helm | @@ -589,6 +590,15 @@ The section below describes all the available subtypes. | value | String | The dependency repository. | (empty) | | expression | String | The complete Helm expression to be replaced with. If not provided, it will use `{{ .Values.. }}`. | (empty) | +### AddIfStatement + +| Property | Type | Description | Default Value | +|--------------------|----------|-------------------------------------------------------------------|---------------| +| property | String | The property to use in the if statement. | | +| onResourceKind | String | The resource kind where to include the if statement. | | +| onResourceName | String | The resource name where to include the if statement. | (empty) | +| withDefaultValue | Boolean | The default value of the property | true | + ## Tekton | Property | Type | Description | Default Value | diff --git a/docs/documentation/helm.md b/docs/documentation/helm.md index 955df99b5..1c0aab1ef 100644 --- a/docs/documentation/helm.md +++ b/docs/documentation/helm.md @@ -424,6 +424,27 @@ database: # the value in the `alias` property, or the `name` if unset. This configuration will be aggregated in the autogenerated values file at `./target/classes/META-INF/dekorate/helm//values.yaml`. +#### Conditionally enable/disable resources + +Based on a boolean property that is available part of the `values.yaml`, you can specify whether you want to install or not any resource. For example, we want to install the generated Ingress resource only if I pass the following property `app.ingress.enabled=true` when installing the chart. Let's see how to do this using the `dekorate.helm.addIfStatements` properties: + +```properties +dekorate.helm.addIfStatements[0].property=ingress.enabled +dekorate.helm.addIfStatements[0].onResourceKind=Ingress +dekorate.helm.addIfStatements[0].withDefaultValue=false +``` + +This configuration will add the `app.ingress.enabled` property in the `values.yaml` file: + +```yaml +app: + ingress: + enabled: false +``` + +So, when installing the chart, the Ingress resource won't be installed by default. +Now, to install it, you need to explicitly set the `app.ingress.enabled=true` property as `helm install app local/chart --set app.ingress.enabled=false` and then the Ingress resource would be installed. + #### Helm Profiles By default, all the properties are mapped to the same Helm values file `values.yaml`. However, Dekorate also supports the generation of Helm values by profiles. diff --git a/examples/helm-on-kubernetes-example/src/main/resources/application.properties b/examples/helm-on-kubernetes-example/src/main/resources/application.properties index af4f6a45a..1bc384954 100644 --- a/examples/helm-on-kubernetes-example/src/main/resources/application.properties +++ b/examples/helm-on-kubernetes-example/src/main/resources/application.properties @@ -53,6 +53,10 @@ dekorate.helm.expressions[3].path=(kind == ConfigMap && metadata.name == my-conf dekorate.helm.expressions[3].expression={{- range $key, $val := .Values.favorite }}\n\ {{ indent 2 $key }}: {{ $val | quote }}\n\ {{- end }} +# Condition as resource level +dekorate.helm.addIfStatements[0].property=ingress.enabled +dekorate.helm.addIfStatements[0].onResourceKind=Ingress + # Kubernetes configuration to assert the tests dekorate.kubernetes.replicas=3 dekorate.kubernetes.ingress.expose=true diff --git a/examples/helm-on-kubernetes-example/src/test/resources/expected-ingress.yaml b/examples/helm-on-kubernetes-example/src/test/resources/expected-ingress.yaml index 5077e7608..cf5d31e4a 100644 --- a/examples/helm-on-kubernetes-example/src/test/resources/expected-ingress.yaml +++ b/examples/helm-on-kubernetes-example/src/test/resources/expected-ingress.yaml @@ -1,3 +1,4 @@ +{{- if .Values.app.ingress.enabled }} --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -20,3 +21,5 @@ spec: name: http path: {{ .Values.app.path | default '/' }} pathType: Prefix + +{{- end }}