From 352dd72be75b92e4a5763807dc6f488d1263dd09 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 2 Jun 2020 20:27:55 +0200 Subject: [PATCH] feat: support extracting var from key from secret/configmap Fixes #9629 --- .../asciidoc/deploying-to-kubernetes.adoc | 120 ++++++++++--- .../image/s2i/deployment/S2iProcessor.java | 37 ++-- .../spi/KubernetesEnvBuildItem.java | 162 ++++++++++++++---- .../spi/KubernetesEnvBuildItemTest.java | 55 ++++++ .../kubernetes/deployment/EnvConverter.java | 6 +- .../EnvVarFromConfigMapKeyConfig.java | 35 ++++ .../deployment/EnvVarFromKeyConfig.java | 45 +++++ .../kubernetes/deployment/EnvVarHolder.java | 22 +-- .../deployment/EnvVarValidator.java | 88 +++++----- .../kubernetes/deployment/EnvVarsConfig.java | 7 + .../deployment/KubernetesProcessor.java | 87 ++-------- .../deployment/EnvVarValidatorTest.java | 123 +++++++++---- ...tesWithConflictingEnvFromResourceTest.java | 37 ++++ .../KubernetesWithConflictingEnvTest.java | 2 +- .../KubernetesWithEnvFromConfigMapTest.java | 70 ++++++++ .../KubernetesWithEnvFromSecretTest.java | 10 ++ .../KubernetesWithOldStyleEnvTest.java | 6 +- .../KubernetesWithWarningsEnvTest.java | 9 +- ...h-conflicting-env-from-resource.properties | 3 + ...ernetes-with-env-from-configmap.properties | 3 + ...kubernetes-with-env-from-secret.properties | 4 +- 21 files changed, 679 insertions(+), 252 deletions(-) create mode 100644 extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItemTest.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromConfigMapKeyConfig.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromKeyConfig.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvFromResourceTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromConfigMapTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-conflicting-env-from-resource.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-configmap.properties diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index a255f5d1b15d5..e261bb8593d82 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -214,7 +214,7 @@ NOTE: When using the `quarkus-container-image-jib` extension to build a containe Out of the box, the generated resources will be annotated with version control related information that can be used either by tooling, or by the user for troubleshooting purposes. -[source, json] +[source,json] ---- "annotations": { "app.quarkus.io/vcs-url" : "", @@ -224,8 +224,7 @@ Out of the box, the generated resources will be annotated with version control r ==== Custom Annotations -Custom annotations can be added in a way similar to labels. For example to add the annotation `foo=bar` and `app.quarkus/id=42` just apply the following configuration: - +Custom annotations can be added in a way similar to labels.For example to add the annotation `foo=bar` and `app.quarkus/id=42` just apply the following configuration: [source] ---- @@ -233,20 +232,21 @@ quarkus.kubernetes.annotations.foo=bar quarkus."app.quarkus/id"=42 ---- +[#env-vars] ==== Environment variables Kubernetes provides multiple ways of defining environment variables: - key/value pairs -- from a Secret -- from a ConfigMap -- from fields +- import all values from a Secret or ConfigMap +- interpolate a single value identified by a given field in a Secret or ConfigMap +- interpolate a value from a field within the same resource ===== Environment variables from key/value pairs To add a key/value pair as an environment variable in the generated resources: -[source] +[source,properties] ---- quarkus.kubernetes.env.vars.my-env-var=foobar ---- @@ -259,25 +259,93 @@ Please note that the key `my-env-var` will be converted to uppercase and dashes To add all key/value pairs of `Secret` as environment variables just apply the following configuration, separating each `Secret` to be used as source by a comma (`,`): -[source] +[source,properties] ---- quarkus.kubernetes.env.secrets=my-secret,my-other-secret ---- +which would generate the following in the container definition: + +[source,yaml] +---- +envFrom: + - secretRef: + name: my-secret + optional: false + - secretRef: + name: my-other-secret + optional: false +---- + +The following extracts a value identified by the `keyName` field from the `my-secret` Secret into a `foo` environment variable: + +[source,properties] +---- +quarkus.kubernetes.env.mapping.foo.from-secret=my-secret +quarkus.kubernetes.env.mapping.foo.with-key=keyName +---- + +This would generate the following in the `env` section of your container: + +[source,yaml] +---- +- env: + - name: FOO + valueFrom: + secretKeyRef: + key: keyName + name: my-secret + optional: false +---- + ===== Environment variables from ConfigMap To add all key/value pairs from `ConfigMap` as environment variables just apply the following configuration, separating each `ConfigMap` to be used as source by a comma (`,`): -[source] +[source,properties] ---- quarkus.kubernetes.env.configmaps=my-config-map,another-config-map ---- +which would generate the following in the container definition: + +[source,yaml] +---- +envFrom: + - configMapRef: + name: my-config-map + optional: false + - configMapRef: + name: another-config-map + optional: false +---- + +The following extracts a value identified by the `keyName` field from the `my-config-map` ConfigMap into a `foo` +environment variable: + +[source,properties] +---- +quarkus.kubernetes.env.mapping.foo.from-configmap=my-configmap +quarkus.kubernetes.env.mapping.foo.with-key=keyName +---- + +This would generate the following in the `env` section of your container: + +[source,yaml] +---- +- env: + - name: FOO + valueFrom: + configMapRefKey: + key: keyName + name: my-configmap + optional: false +---- + ===== Environment variables from fields -It's also possible to use the value from another field to add a new environment variable by specifying the path of the field to -be used as a source, as follows: +It's also possible to use the value from another field to add a new environment variable by specifying the path of the field to be used as a source, as follows: [source] ---- @@ -286,29 +354,30 @@ quarkus.kubernetes.env.fields.foo=metadata.name ===== Validation -A conflict between two definitions, e.g. mistakenly assigning both a value and specifying that a variable is derived -from a field, will result in an error being thrown at build time so that you get the opportunity to fix the issue before you -deploy your application to your cluster where it might be more difficult to diagnose the source of the issue. +A conflict between two definitions, e.g. mistakenly assigning both a value and specifying that a variable is derived from a field, will result in an error being thrown at build time so that you get the opportunity to fix the issue before you deploy your application to your cluster where it might be more difficult to diagnose the source of the issue. -Similarly, two redundant definitions, e.g. defining an injection from the same secret twice, will not cause an issue but will -indeed report a warning to let you know that you might not have intended to duplicate that definition. +Similarly, two redundant definitions, e.g. defining an injection from the same secret twice, will not cause an issue but will indeed report a warning to let you know that you might not have intended to duplicate that definition. +[#env-vars-backwards] ===== Backwards compatibility -Previous versions of the Kubernetes extension supported a different syntax to add environment variables. The older syntax is -still supported but is deprecated and it's advised that you migrate to the new syntax. +Previous versions of the Kubernetes extension supported a different syntax to add environment variables.The older syntax is still supported but is deprecated and it's advised that you migrate to the new syntax. .Old vs. new syntax |==== -| |Old | New | -| Plain variable |`quarkus.kubernetes.env-vars.my-env-var.value=foobar` | `quarkus.kubernetes.env.vars.my-env-var=foobar` | -| From field |`quarkus.kubernetes.env-vars.my-env-var.field=foobar` | `quarkus.kubernetes.env.fields.my-env-var=foobar` | -| All from `ConfigMap` |`quarkus.kubernetes.env-vars.xxx.configmap=foobar` | `quarkus.kubernetes.env.configmaps=foobar` | -| All from `Secret` |`quarkus.kubernetes.env-vars.xxx.secret=foobar` | `quarkus.kubernetes.env.secrets=foobar` | +| |Old | New | +| Plain variable |`quarkus.kubernetes.env-vars.my-env-var.value=foobar` | `quarkus.kubernetes.env.vars.my-env-var=foobar` | +| From field |`quarkus.kubernetes.env-vars.my-env-var.field=foobar` | `quarkus.kubernetes.env.fields.my-env-var=foobar` | +| All from `ConfigMap` |`quarkus.kubernetes.env-vars.xxx.configmap=foobar` | `quarkus.kubernetes.env.configmaps=foobar` | +| All from `Secret` |`quarkus.kubernetes.env-vars.xxx.secret=foobar` | `quarkus.kubernetes.env.secrets=foobar` | +| From one `Secret` field |`quarkus.kubernetes.env-vars.foo.secret=foobar` | `quarkus.kubernetes.env.mapping.foo.from-secret=foobar` | +| |`quarkus.kubernetes.env-vars.foo.value=field` | `quarkus.kubernetes.env.mapping.foo.with-key=field` | +| From one `ConfigMap` field |`quarkus.kubernetes.env-vars.foo.configmap=foobar` | `quarkus.kubernetes.env.mapping.foo.from-configmap=foobar` | +| |`quarkus.kubernetes.env-vars.foo.value=field` | `quarkus.kubernetes.env.mapping.foo.with-key=field` | |==== NOTE: If you redefine the same variable using the new syntax while keeping the old syntax, **ONLY** the new version will be kept -and a warning will be issued to alert you of the problem. For example, if you define both +and a warning will be issued to alert you of the problem.For example, if you define both `quarkus.kubernetes.env-vars.my-env-var.value=foobar` and `quarkus.kubernetes.env.vars.my-env-var=newValue`, the extension will only generate an environment variable `MY_ENV_VAR=newValue` and issue a warning. @@ -829,7 +898,10 @@ kubernetes.env-vars[0].configmap=my-configmap quarkus.kubernetes.env-vars.foo.configmap=myconfigmap ---- +==== `env-vars` properties +`quarkus.kubernetes.env-vars` are deprecated (though still currently supported as of this writing) and the new declaration style should be used instead. +See <<#env-vars>> and more specifically <> for more details. == Deployment diff --git a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java index 0ebfe9600cf4b..bfbc8e127e844 100644 --- a/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java +++ b/extensions/container-image/container-image-s2i/deployment/src/main/java/io/quarkus/container/image/s2i/deployment/S2iProcessor.java @@ -1,21 +1,10 @@ package io.quarkus.container.image.s2i.deployment; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -55,11 +44,7 @@ import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.GeneratedFileSystemResourceBuildItem; import io.quarkus.deployment.pkg.PackageConfig; -import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.pkg.builditem.JarBuildItem; -import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; -import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.*; import io.quarkus.deployment.pkg.steps.NativeBuild; import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHanlder; import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; @@ -108,11 +93,11 @@ public void s2iRequirementsJvm(S2iConfig s2iConfig, builderImageProducer.produce(new BaseImageInfoBuildItem(s2iConfig.baseJvmImage)); Optional baseImage = S2iBaseJavaImage.findMatching(s2iConfig.baseJvmImage); baseImage.ifPresent(b -> { - envProducer.produce(new KubernetesEnvBuildItem(b.getJarEnvVar(), pathToJar, OPENSHIFT)); - envProducer.produce(new KubernetesEnvBuildItem(b.getJarLibEnvVar(), + envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJarEnvVar(), pathToJar, OPENSHIFT)); + envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJarLibEnvVar(), concatUnixPaths(jarDirectory, "lib"), OPENSHIFT)); - envProducer.produce(new KubernetesEnvBuildItem(b.getClasspathEnvVar(), classpath, OPENSHIFT)); - envProducer.produce(new KubernetesEnvBuildItem(b.getJvmOptionsEnvVar(), + envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getClasspathEnvVar(), classpath, OPENSHIFT)); + envProducer.produce(KubernetesEnvBuildItem.createSimpleVar(b.getJvmOptionsEnvVar(), String.join(" ", s2iConfig.jvmArguments), OPENSHIFT)); }); @@ -150,9 +135,11 @@ public void s2iRequirementsNative(S2iConfig s2iConfig, builderImageProducer.produce(new BaseImageInfoBuildItem(s2iConfig.baseNativeImage)); Optional baseImage = S2iBaseNativeImage.findMatching(s2iConfig.baseNativeImage); baseImage.ifPresent(b -> { - envProducer.produce(new KubernetesEnvBuildItem(b.getHomeDirEnvVar(), s2iConfig.nativeBinaryDirectory, OPENSHIFT)); - envProducer.produce(new KubernetesEnvBuildItem(b.getOptsEnvVar(), - String.join(" ", s2iConfig.nativeArguments), OPENSHIFT)); + envProducer.produce( + KubernetesEnvBuildItem.createSimpleVar(b.getHomeDirEnvVar(), s2iConfig.nativeBinaryDirectory, OPENSHIFT)); + envProducer.produce( + KubernetesEnvBuildItem.createSimpleVar(b.getOptsEnvVar(), String.join(" ", s2iConfig.nativeArguments), + OPENSHIFT)); }); if (!baseImage.isPresent()) { diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItem.java index 316b24904a11c..4709c45d90aec 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItem.java @@ -1,15 +1,20 @@ package io.quarkus.kubernetes.spi; +import org.jboss.logging.Logger; + import io.quarkus.builder.item.MultiBuildItem; public final class KubernetesEnvBuildItem extends MultiBuildItem { + private static final Logger log = Logger.getLogger(KubernetesEnvBuildItem.class); public enum EnvType { var(false), field(false), secret(true), - configmap(true); + configmap(true), + keyFromConfigmap(false), + keyFromSecret(false); public final boolean allowMultipleDefinitions; @@ -24,13 +29,17 @@ public boolean mightConflictWith(EnvType type) { switch (this) { case field: - return type == var; + return type == var || type == keyFromConfigmap || type == keyFromSecret; case var: - return type == field; + return type == field || type == keyFromConfigmap || type == keyFromSecret; case secret: return type == configmap; case configmap: return type == secret; + case keyFromConfigmap: + return type == field || type == var || type == keyFromSecret; + case keyFromSecret: + return type == field || type == var || type == keyFromConfigmap; default: return false; } @@ -39,64 +48,127 @@ public boolean mightConflictWith(EnvType type) { private final String name; private final String value; + private final String configmap; + private final String secret; + private final String field; private final EnvType type; private final String target; private final boolean oldStyle; - public static EnvType getEnvType(String secret, String configmap, String field) { + public static KubernetesEnvBuildItem createFromField(String name, String targetField, String target, + boolean... oldStyle) { + return create(name, null, null, null, targetField, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createFromConfigMap(String configMapName, String target, boolean... oldStyle) { + return create(configMapName, null, null, configMapName, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createFromSecret(String secretName, String target, boolean... oldStyle) { + return create(secretName, null, secretName, null, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createSimpleVar(String name, String value, String target, + boolean... oldStyle) { + return create(name, value, null, null, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createFromConfigMapKey(String varName, String key, String configmap, String target, + boolean... oldStyle) { + return create(varName, key, null, configmap, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createFromSecretKey(String varName, String key, String secret, String target, + boolean... oldStyle) { + return create(varName, key, secret, null, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem createFromResourceKey(String varName, String key, String secret, + String configmap, String target, boolean... oldStyle) { + return create(varName, key, secret, configmap, null, target, isOldStyle(oldStyle)); + } + + public static KubernetesEnvBuildItem create(String name, String value, String secret, String configmap, String field, + String target, boolean... oldStyle) throws IllegalArgumentException { + final boolean secretPresent = secret != null; + final boolean configmapPresent = configmap != null; + final boolean valuePresent = value != null; + final boolean fieldPresent = field != null; + if (valuePresent) { + if (secretPresent && configmapPresent) { + throw new IllegalArgumentException(String.format( + "'%s' env var can't simultaneously take its value from '%s' configmap & '%s' secret", + name, configmap, secret)); + } + if (fieldPresent) { + throw new IllegalArgumentException(String.format( + "'%s' env var can't simultaneously have a '%s' value & take is value from the '%s' field", + name, value, field)); + } + } + if (secretPresent && configmapPresent) { + log.warn(String.format("The '%s' name was used to try to import both from '%s' secret & '%s' configmap. " + + "Only values from '%s' secret will be imported.\nIf you want to import from both, use a " + + "different property name for either.", + name, secret, + configmap, + secret)); + } final EnvType type; - if (secret != null) { - type = EnvType.secret; - } else if (configmap != null) { - type = EnvType.configmap; + if (secretPresent) { + if (valuePresent) { + type = EnvType.keyFromSecret; + } else { + name = secret; + type = EnvType.secret; + } + } else if (configmapPresent) { + if (valuePresent) { + type = EnvType.keyFromConfigmap; + } else { + name = configmap; + type = EnvType.configmap; + } } else if (field != null) { type = EnvType.field; } else { type = EnvType.var; } - return type; + return new KubernetesEnvBuildItem(name, value, configmap, secret, field, type, target, isOldStyle(oldStyle)); } - public KubernetesEnvBuildItem(String name, String value, String target) { - this(EnvType.var, name, value, target); + private static boolean isOldStyle(boolean[] oldStyle) { + return oldStyle.length >= 1 && oldStyle[0]; } - public KubernetesEnvBuildItem(EnvType type, String name, String value, String target, boolean oldStyle) { + KubernetesEnvBuildItem(String name, String value, String configmap, String secret, String field, EnvType type, + String target, boolean oldStyle) { this.name = name; this.value = value; + this.configmap = configmap; + this.secret = secret; + this.field = field; this.type = type; this.target = target; this.oldStyle = oldStyle; } - public KubernetesEnvBuildItem(EnvType type, String name, String value, String target) { - this(type, name, value, target, false); - } - public String getConfigMap() { - return getValueIfMatching(EnvType.configmap); + return configmap; } public String getSecret() { - return getValueIfMatching(EnvType.secret); + return secret; } public String getField() { - return getValueIfMatching(EnvType.field); - } - - public String getVar() { - return getValueIfMatching(EnvType.var); + return field; } public boolean isOldStyle() { return oldStyle; } - private String getValueIfMatching(EnvType type) { - return this.type == type ? value : null; - } - public String getName() { return name; } @@ -113,6 +185,25 @@ public String getTarget() { return target; } + public String toString() { + switch (type) { + case var: + return String.format("'%s' env var with value '%s'", name, value); + case field: + return String.format("'%s' env var with value from field '%s'", name, field); + case secret: + return "all values from '" + secret + "' secret"; + case configmap: + return "all values from '" + configmap + "' configmap"; + case keyFromConfigmap: + return String.format("'%s' env var with value from '%s' key of '%s' configmap", name, value, configmap); + case keyFromSecret: + return String.format("'%s' env var with value from '%s' key of '%s' secret", name, value, secret); + default: + return "unknown type '" + type + "'"; + } + } + @Override public boolean equals(Object o) { if (this == o) @@ -124,20 +215,25 @@ public boolean equals(Object o) { if (!name.equals(that.name)) return false; - if (!value.equals(that.value)) + if (value != null ? !value.equals(that.value) : that.value != null) + return false; + if (configmap != null ? !configmap.equals(that.configmap) : that.configmap != null) return false; - if (type != that.type) + if (secret != null ? !secret.equals(that.secret) : that.secret != null) return false; - return target.equals(that.target); + if (field != null ? !field.equals(that.field) : that.field != null) + return false; + return type == that.type; } @Override public int hashCode() { int result = name.hashCode(); - result = 31 * result + value.hashCode(); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (configmap != null ? configmap.hashCode() : 0); + result = 31 * result + (secret != null ? secret.hashCode() : 0); + result = 31 * result + (field != null ? field.hashCode() : 0); result = 31 * result + type.hashCode(); - result = 31 * result + target.hashCode(); return result; } - } diff --git a/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItemTest.java b/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItemTest.java new file mode 100644 index 0000000000000..106185fe5188a --- /dev/null +++ b/extensions/kubernetes/spi/src/test/java/io/quarkus/kubernetes/spi/KubernetesEnvBuildItemTest.java @@ -0,0 +1,55 @@ +/** + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + *

+ * 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.quarkus.kubernetes.spi; + +import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.create; +import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.configmap; +import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.var; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +/** + * @author Christophe Laprun + */ +public class KubernetesEnvBuildItemTest { + + private static final String TARGET = "target"; + private static final String VALUE = "value"; + private static final String NAME = "name"; + + @Test + public void testCreateSimpleVarFromEnvConfig() { + final KubernetesEnvBuildItem item = create(NAME, VALUE, null, null, null, TARGET); + assertEquals(var, item.getType()); + assertEquals(NAME, item.getName()); + assertEquals(VALUE, item.getValue()); + assertEquals(TARGET, item.getTarget()); + assertNull(item.getConfigMap()); + assertNull(item.getSecret()); + assertNull(item.getField()); + } + + @Test + public void testCreateLoadFromConfigMapFromEnvConfig() { + final KubernetesEnvBuildItem item = create(NAME, null, null, VALUE, null, TARGET); + assertEquals(configmap, item.getType()); + assertEquals(VALUE, item.getName()); + assertNull(item.getValue()); + assertEquals(VALUE, item.getConfigMap()); + assertNull(item.getSecret()); + assertNull(item.getField()); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvConverter.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvConverter.java index b5ea25e082636..2c46fcc763b14 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvConverter.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvConverter.java @@ -31,8 +31,12 @@ public static List convert(EnvVarsConfig e) { e.vars.forEach((k, v) -> envs.add(new EnvBuilder().withName(convertName(k)).withValue(v).build())); e.fields.forEach((k, v) -> { // env vars from fields need to have their name set in addition to their field field :) - envs.add(new EnvBuilder().withName(convertName(k)).withField(convertName(k)).withValue(v).build()); + final String field = convertName(k); + envs.add(new EnvBuilder().withName(field).withField(field).withValue(v).build()); }); + e.mapping.forEach( + (k, v) -> envs.add(new EnvBuilder().withName(convertName(k)).withSecret(v.fromSecret.orElse(null)) + .withConfigmap(v.fromConfigmap.orElse(null)).withValue(v.withKey).build())); return envs; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromConfigMapKeyConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromConfigMapKeyConfig.java new file mode 100644 index 0000000000000..3d7093a2fe1ab --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromConfigMapKeyConfig.java @@ -0,0 +1,35 @@ +/** + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + *

+ * 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.quarkus.kubernetes.deployment; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * The configuration of environment variables taking their value from a ConfigMap field identified by its key. + */ +@ConfigGroup +public class EnvVarFromConfigMapKeyConfig { + /** + * The name of the ConfigMap from which a value is to be extracted. + */ + @ConfigItem + String configmap; + + /** + * The key identifying the field from which the value is extracted. + */ + @ConfigItem + String key; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromKeyConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromKeyConfig.java new file mode 100644 index 0000000000000..010d27b8295c5 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarFromKeyConfig.java @@ -0,0 +1,45 @@ +/** + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + *

+ * 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.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * The configuration of environment variables taking their value from a Secret or ConfigMap field identified by its key. + */ +@ConfigGroup +public class EnvVarFromKeyConfig { + /** + * The optional name of the Secret from which a value is to be extracted. + * Mutually exclusive with {@link #fromConfigmap}. + */ + @ConfigItem + Optional fromSecret; + + /** + * The optional name of the ConfigMap from which a value is to be extracted. + * Mutually exclusive with {@link #fromSecret}. + */ + @ConfigItem + Optional fromConfigmap; + + /** + * The key identifying the field from which the value is extracted. + */ + @ConfigItem + String withKey; +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarHolder.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarHolder.java index b3b94340a39b6..01a86ba2e8357 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarHolder.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarHolder.java @@ -1,10 +1,5 @@ package io.quarkus.kubernetes.deployment; -import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.configmap; -import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.field; -import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.secret; -import static io.quarkus.kubernetes.spi.KubernetesEnvBuildItem.EnvType.var; - import java.util.Collection; import java.util.Map; @@ -47,19 +42,20 @@ default Collection convertToBuildItems() { // first process old-style configuration, this relies on each configuration having a name final String target = getTargetPlatformName(); getEnvVars().forEach((key, envConfig) -> { - envConfig.secret.ifPresent(s -> validator.process(new KubernetesEnvBuildItem(secret, s, s, target, true))); - envConfig.configmap.ifPresent(cm -> validator.process(new KubernetesEnvBuildItem(configmap, cm, cm, target, true))); - envConfig.field.ifPresent(f -> validator.process(new KubernetesEnvBuildItem(field, key, f, target, true))); - envConfig.value.ifPresent(v -> validator.process(new KubernetesEnvBuildItem(var, key, v, target, true))); + validator.process(key, envConfig.value, envConfig.secret, envConfig.configmap, envConfig.field, target, + true); }); // override old-style with newer versions if present final EnvVarsConfig c = getEnv(); - c.vars.forEach((k, v) -> validator.process(new KubernetesEnvBuildItem(var, k, v, target))); - c.fields.forEach((k, v) -> validator.process(new KubernetesEnvBuildItem(field, k, v, target))); + c.vars.forEach((k, v) -> validator.process(KubernetesEnvBuildItem.createSimpleVar(k, v, target))); + c.fields.forEach((k, v) -> validator.process(KubernetesEnvBuildItem.createFromField(k, v, target))); c.configmaps - .ifPresent(cl -> cl.forEach(cm -> validator.process(new KubernetesEnvBuildItem(configmap, cm, cm, target)))); - c.secrets.ifPresent(sl -> sl.forEach(s -> validator.process(new KubernetesEnvBuildItem(secret, s, s, target)))); + .ifPresent(cl -> cl.forEach(cm -> validator.process(KubernetesEnvBuildItem.createFromConfigMap(cm, target)))); + c.secrets.ifPresent(sl -> sl.forEach(s -> validator.process(KubernetesEnvBuildItem.createFromSecret(s, target)))); + c.mapping.forEach( + (varName, config) -> validator.process(KubernetesEnvBuildItem.createFromResourceKey(varName, config.withKey, + config.fromSecret.orElse(null), config.fromConfigmap.orElse(null), target))); return validator.getBuildItems(); } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarValidator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarValidator.java index d4fb8b08f1120..6d4cbf74e0f2c 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarValidator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarValidator.java @@ -1,11 +1,6 @@ package io.quarkus.kubernetes.deployment; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import org.jboss.logging.Logger; @@ -19,7 +14,19 @@ public class EnvVarValidator { private static final Logger log = Logger.getLogger(EnvVarValidator.class); private final Map items = new HashMap<>(); private final Set knownNames = new HashSet<>(); - private final Map> errors = new HashMap<>(); + private final Map> conflicting = new HashMap<>(); + private final Set errors = new HashSet<>(); + + void process(String name, Optional value, Optional secret, Optional configmap, + Optional field, String target, boolean... oldStyle) { + try { + final KubernetesEnvBuildItem kebi = KubernetesEnvBuildItem.create(name, value.orElse(null), + secret.orElse(null), configmap.orElse(null), field.orElse(null), target, oldStyle); + process(kebi); + } catch (IllegalArgumentException e) { + errors.add(e.getMessage()); + } + } /** * Processes the specified {@link KubernetesEnvBuildItem} to check whether it's valid with respect to the set of already @@ -29,12 +36,12 @@ public class EnvVarValidator { */ void process(KubernetesEnvBuildItem item) { final String name = item.getName(); - final KEBIWrapper wrapper = new KEBIWrapper(item); + final ShouldAddHolder wrapper = new ShouldAddHolder(); // check if we already have defined a build item with the same name if (knownNames.contains(name)) { final KubernetesEnvBuildItem.EnvType type = item.getType(); // as the item might not get added, reset the wrapper's state - wrapper.setNeedingAdding(false); + wrapper.setShouldAdd(false); // then go through already added items to check if the item needs to be added, warning logged or error added items.values().stream().filter(kebi -> name.equals(kebi.getName())).forEach(existing -> { final KubernetesEnvBuildItem.EnvType existingType = existing.getType(); @@ -49,35 +56,35 @@ void process(KubernetesEnvBuildItem item) { // only keep definition using new style and output warning log.warn("Duplicate definition of '" + name + "' environment variable. ONLY the quarkus.kubernetes.env prefixed version will be kept: " - + (currentIsNew ? describe(item) : describe(existing))); + + (currentIsNew ? item : existing)); if (currentIsNew) { // replace existing, old-style value by current, new-style one - wrapper.setNeedingAdding(true); + wrapper.setShouldAdd(true); } } else { addError(item, existing); } } else { // we're not dealing with a potentially conflicting var so add the new item - wrapper.setNeedingAdding(true); + wrapper.setShouldAdd(true); } } else { if (existingType.mightConflictWith(type)) { - log.warn("Ignoring duplicate definition of " + describe(item)); + log.warn("Ignoring duplicate definition of " + item); } - wrapper.setNeedingAdding(true); + wrapper.setShouldAdd(true); } }); } - if (wrapper.isNeedingAdding()) { + if (wrapper.shouldAdd()) { items.put(ItemKey.keyFor(item), item); } knownNames.add(name); } private void addError(KubernetesEnvBuildItem item, KubernetesEnvBuildItem existing) { - final Set inError = errors.computeIfAbsent(item.getName(), k -> new LinkedHashSet<>()); + final Set inError = conflicting.computeIfAbsent(item.getName(), k -> new LinkedHashSet<>()); inError.add(existing); inError.add(item); } @@ -89,44 +96,45 @@ private void addError(KubernetesEnvBuildItem item, KubernetesEnvBuildItem existi * @throws IllegalArgumentException if the processed items result in an invalid configuration */ Collection getBuildItems() { - if (errors.isEmpty()) { + if (conflicting.isEmpty() && errors.isEmpty()) { return items.values(); } throw new IllegalArgumentException(getError()); } private String getError() { - String error = "Found conflicts in environment variable definitions:\n"; - error += errors.entrySet().stream() - .map(e -> { - final String conflicting = e.getValue().stream() - .map(this::describe) - .collect(Collectors.joining(" redefined as ")); - return String.format("\t\t- '%s': first defined as %s", e.getKey(), conflicting); - }) - .collect(Collectors.joining("\n")); + String error = "\n"; + if (!conflicting.isEmpty()) { + error += "\t+ Conflicts in environment variable definitions:\n"; + error += conflicting.entrySet().stream() + .map(e -> { + final String conflicting = e.getValue().stream() + .map(Object::toString) + .collect(Collectors.joining(" redefined as ")); + return String.format("\t\t- '%s': first defined as %s", e.getKey(), conflicting); + }) + .collect(Collectors.joining("\n")); + } + if (!errors.isEmpty()) { + error += "\t+ Invalid declarations:\n"; + error += errors.stream().map(s -> "\t\t- " + s).collect(Collectors.joining("\n")); + } return error; } - private String describe(KubernetesEnvBuildItem kebi) { - return String.format("'%s' env var with value '%s'", kebi.getType().name(), kebi.getValue()); - } - - private static final class KEBIWrapper { - private final KubernetesEnvBuildItem item; - private boolean needingAdding; + private static final class ShouldAddHolder { + private boolean shouldAdd; - KEBIWrapper(KubernetesEnvBuildItem item) { - this.item = item; - needingAdding = true; + ShouldAddHolder() { + shouldAdd = true; } - public boolean isNeedingAdding() { - return needingAdding; + public boolean shouldAdd() { + return shouldAdd; } - public void setNeedingAdding(boolean needingAdding) { - this.needingAdding = needingAdding; + public void setShouldAdd(boolean shouldAdd) { + this.shouldAdd = shouldAdd; } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java index 0949746b280f7..340fc6aae4101 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/EnvVarsConfig.java @@ -35,4 +35,11 @@ public class EnvVarsConfig { */ @ConfigItem Map vars; + + /** + * The map recording the configuration of environment variable taking their value from resource (Secret or + * ConfigMap) keys + */ + @ConfigItem + Map mapping; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java index 781ec5532bcc9..ee9508587a126 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java @@ -1,21 +1,6 @@ package io.quarkus.kubernetes.deployment; import static io.quarkus.kubernetes.deployment.Constants.*; -import static io.quarkus.kubernetes.deployment.Constants.DEFAULT_HTTP_PORT; -import static io.quarkus.kubernetes.deployment.Constants.DEFAULT_S2I_IMAGE_NAME; -import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT; -import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_CONFIG; -import static io.quarkus.kubernetes.deployment.Constants.HTTP_PORT; -import static io.quarkus.kubernetes.deployment.Constants.KNATIVE; -import static io.quarkus.kubernetes.deployment.Constants.KUBERNETES; -import static io.quarkus.kubernetes.deployment.Constants.MINIKUBE; -import static io.quarkus.kubernetes.deployment.Constants.OPENSHIFT; -import static io.quarkus.kubernetes.deployment.Constants.OPENSHIFT_APP_RUNTIME; -import static io.quarkus.kubernetes.deployment.Constants.QUARKUS; -import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_BUILD_TIMESTAMP; -import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_COMMIT_ID; -import static io.quarkus.kubernetes.deployment.Constants.QUARKUS_ANNOTATIONS_VCS_URL; -import static io.quarkus.kubernetes.deployment.Constants.SERVICE; import static io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem.*; import java.io.File; @@ -28,16 +13,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import org.jboss.logging.Logger; @@ -52,29 +28,7 @@ import io.dekorate.kubernetes.config.Label; import io.dekorate.kubernetes.config.PortBuilder; import io.dekorate.kubernetes.configurator.AddPort; -import io.dekorate.kubernetes.decorator.AddAnnotationDecorator; -import io.dekorate.kubernetes.decorator.AddAwsElasticBlockStoreVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddAzureDiskVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddAzureFileVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddConfigMapVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddEnvVarDecorator; -import io.dekorate.kubernetes.decorator.AddImagePullSecretDecorator; -import io.dekorate.kubernetes.decorator.AddInitContainerDecorator; -import io.dekorate.kubernetes.decorator.AddLabelDecorator; -import io.dekorate.kubernetes.decorator.AddLivenessProbeDecorator; -import io.dekorate.kubernetes.decorator.AddMountDecorator; -import io.dekorate.kubernetes.decorator.AddPvcVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddReadinessProbeDecorator; -import io.dekorate.kubernetes.decorator.AddRoleBindingResourceDecorator; -import io.dekorate.kubernetes.decorator.AddSecretVolumeDecorator; -import io.dekorate.kubernetes.decorator.AddServiceAccountResourceDecorator; -import io.dekorate.kubernetes.decorator.AddSidecarDecorator; -import io.dekorate.kubernetes.decorator.ApplyArgsDecorator; -import io.dekorate.kubernetes.decorator.ApplyCommandDecorator; -import io.dekorate.kubernetes.decorator.ApplyImagePullPolicyDecorator; -import io.dekorate.kubernetes.decorator.ApplyServiceAccountNamedDecorator; -import io.dekorate.kubernetes.decorator.ApplyWorkingDirDecorator; -import io.dekorate.kubernetes.decorator.RemoveAnnotationDecorator; +import io.dekorate.kubernetes.decorator.*; import io.dekorate.processor.SimpleFileReader; import io.dekorate.processor.SimpleFileWriter; import io.dekorate.project.BuildInfo; @@ -101,15 +55,7 @@ import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.util.FileUtil; -import io.quarkus.kubernetes.spi.KubernetesAnnotationBuildItem; -import io.quarkus.kubernetes.spi.KubernetesCommandBuildItem; -import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; -import io.quarkus.kubernetes.spi.KubernetesEnvBuildItem; -import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; -import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; -import io.quarkus.kubernetes.spi.KubernetesLabelBuildItem; -import io.quarkus.kubernetes.spi.KubernetesPortBuildItem; -import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.*; class KubernetesProcessor { @@ -537,26 +483,13 @@ private void applyBuildItems(Session session, }); kubernetesEnvs.forEach(e -> { - final String value = e.getValue(); - final EnvBuilder envBuilder = new EnvBuilder() - .withValue(value); - switch (e.getType()) { - case var: - envBuilder.withName(EnvConverter.convertName(e.getName())); - break; - case field: - // env vars from fields need to have their name set in addition to their field field :) - final String name = EnvConverter.convertName(e.getName()); - envBuilder.withField(value).withName(name); - break; - case secret: - envBuilder.withSecret(value); - break; - case configmap: - envBuilder.withConfigmap(value); - break; - } - session.resources().decorate(e.getTarget(), new AddEnvVarDecorator(envBuilder.build())); + session.resources().decorate(e.getTarget(), new AddEnvVarDecorator(new EnvBuilder() + .withName(EnvConverter.convertName(e.getName())) + .withValue(e.getValue()) + .withSecret(e.getSecret()) + .withConfigmap(e.getConfigMap()) + .withField(e.getField()) + .build())); }); //Handle Command and arguments diff --git a/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/EnvVarValidatorTest.java b/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/EnvVarValidatorTest.java index ea380ad0db9f2..b0fa7bfcbf901 100644 --- a/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/EnvVarValidatorTest.java +++ b/extensions/kubernetes/vanilla/deployment/src/test/java/io/quarkus/kubernetes/deployment/EnvVarValidatorTest.java @@ -1,10 +1,9 @@ package io.quarkus.kubernetes.deployment; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; import java.util.Collection; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -13,6 +12,7 @@ class EnvVarValidatorTest { + private static final String TARGET = "kubernetes"; private EnvVarValidator validator; @BeforeEach @@ -27,8 +27,7 @@ void getBuildItemsShouldReturnEmptyOnNoItems() { @Test void getBuildItemsOneItemShouldWork() { - final KubernetesEnvBuildItem initial = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, "name", "value", - "kubernetes"); + final KubernetesEnvBuildItem initial = KubernetesEnvBuildItem.createSimpleVar("name", "value", TARGET); validator.process(initial); final Collection items = validator.getBuildItems(); assertEquals(1, items.size()); @@ -40,10 +39,8 @@ void getBuildItemsTwoConflictingItemsShouldFail() { final String name = "name"; final String value1 = "foo"; final String value2 = "bar"; - final KubernetesEnvBuildItem first = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, name, value1, - "kubernetes"); - final KubernetesEnvBuildItem second = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.field, name, value2, - "kubernetes"); + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createSimpleVar(name, value1, TARGET); + final KubernetesEnvBuildItem second = KubernetesEnvBuildItem.createFromField(name, value2, TARGET); validator.process(first); validator.process(second); try { @@ -58,11 +55,8 @@ void getBuildItemsTwoConflictingItemsShouldFail() { @Test void getBuildItemsTwoRedundantItemsShouldResultInOnlyOneItem() { final String name = "name"; - final String value1 = "foo"; - final KubernetesEnvBuildItem first = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.configmap, name, value1, - "kubernetes"); - final KubernetesEnvBuildItem second = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.configmap, name, value1, - "kubernetes"); + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createFromConfigMap(name, TARGET); + final KubernetesEnvBuildItem second = KubernetesEnvBuildItem.createFromConfigMap(name, TARGET); validator.process(first); validator.process(second); final Collection items = validator.getBuildItems(); @@ -74,12 +68,9 @@ void getBuildItemsTwoRedundantItemsShouldResultInOnlyOneItem() { void getBuildItemsSimilarlyNamedCompatibleItemsShouldWork() { final String name = "name"; final String value1 = "foo"; - final KubernetesEnvBuildItem first = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, name, value1, - "kubernetes"); - final KubernetesEnvBuildItem second = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.secret, name, name, - "kubernetes"); - final KubernetesEnvBuildItem third = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.configmap, name, name, - "kubernetes"); + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createSimpleVar(name, value1, TARGET); + final KubernetesEnvBuildItem second = KubernetesEnvBuildItem.createFromSecret(name, TARGET); + final KubernetesEnvBuildItem third = KubernetesEnvBuildItem.createFromConfigMap(name, TARGET); validator.process(first); validator.process(second); validator.process(third); @@ -101,11 +92,8 @@ void getBuildItemsSimilarlyNamedCompatibleItemsShouldWork() { void getBuildItemsSameItemsOldAndNewShouldWork() { final String name = "name"; final String value1 = "foo"; - final KubernetesEnvBuildItem oldStyleVar = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, name, value1, - "kubernetes", true); - final KubernetesEnvBuildItem newStyleVar = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, name, - "newValue", - "kubernetes", false); + final KubernetesEnvBuildItem oldStyleVar = KubernetesEnvBuildItem.createSimpleVar(name, value1, TARGET, true); + final KubernetesEnvBuildItem newStyleVar = KubernetesEnvBuildItem.createSimpleVar(name, "newValue", TARGET); validator.process(oldStyleVar); validator.process(newStyleVar); Collection items = validator.getBuildItems(); @@ -121,15 +109,35 @@ void getBuildItemsSameItemsOldAndNewShouldWork() { assertTrue(items.contains(newStyleVar)); } + @Test + void getBuildItemsOldConflictShouldNotPreventNewToWork() { + /* + * quarkus.kubernetes.env.configmaps=configMap + * quarkus.kubernetes.env-vars.xxx.configmap=configMap + * quarkus.kubernetes.env.secrets=secret + * quarkus.kubernetes.env-vars.xxx.secret=secret + */ + final KubernetesEnvBuildItem newCM = KubernetesEnvBuildItem.createFromConfigMap("configmap", TARGET); + final KubernetesEnvBuildItem newS = KubernetesEnvBuildItem.createFromSecret("secret", TARGET); + validator.process("foo", Optional.empty(), Optional.empty(), Optional.of("configmap"), Optional.empty(), + TARGET, true); + validator.process(newS); + validator.process(newCM); + validator.process("foo", Optional.empty(), Optional.of("secret"), Optional.empty(), Optional.empty(), + TARGET, true); + Collection items = validator.getBuildItems(); + assertEquals(2, items.size()); + assertTrue(items.contains(newCM)); + assertTrue(items.contains(newS)); + } + @Test void getBuildItemsTwoConflictingItemsUsingDifferentStylesShouldFail() { final String name = "name"; final String value1 = "foo"; final String value2 = "bar"; - final KubernetesEnvBuildItem first = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.var, name, value1, - "kubernetes", true); - final KubernetesEnvBuildItem second = new KubernetesEnvBuildItem(KubernetesEnvBuildItem.EnvType.field, name, value2, - "kubernetes"); + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createSimpleVar(name, value1, TARGET, true); + final KubernetesEnvBuildItem second = KubernetesEnvBuildItem.createFromField(name, value2, TARGET); validator.process(first); validator.process(second); try { @@ -152,4 +160,61 @@ void getBuildItemsTwoConflictingItemsUsingDifferentStylesShouldFail() { assertTrue(message.contains(name) && message.contains(value1) && message.contains(value2)); } } + + @Test + void getBuildItemsDirectAndFromSecretShouldConflict() { + final String name = "name"; + final String value1 = "foo"; + final String configmap = "configmap"; + final String key = "key"; + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createSimpleVar(name, value1, TARGET); + final KubernetesEnvBuildItem second = KubernetesEnvBuildItem.createFromConfigMapKey(name, key, configmap, + TARGET); + validator.process(first); + validator.process(second); + try { + validator.getBuildItems(); + fail(); + } catch (Exception e) { + final String message = e.getMessage(); + assertTrue( + message.contains(name) && message.contains(value1) && message.contains(configmap) && message.contains(key)); + } + + // check different order + validator = new EnvVarValidator(); + validator.process(second); + validator.process(first); + try { + validator.getBuildItems(); + fail(); + } catch (Exception e) { + final String message = e.getMessage(); + assertTrue( + message.contains(name) && message.contains(value1) && message.contains(configmap) && message.contains(key)); + } + } + + @Test + void getBuildItemsUsingOldStyleProcessAndNewStyleCreateForSameItemShouldKeepNewStyle() { + final String name = "name"; + final String configmap = "configmap"; + final String key = "key"; + final KubernetesEnvBuildItem first = KubernetesEnvBuildItem.createFromConfigMapKey(name, key, configmap, TARGET); + validator.process(first); + validator.process(name, Optional.of("oldKey"), Optional.empty(), Optional.of(configmap), Optional.empty(), + TARGET, true); + Collection buildItems = validator.getBuildItems(); + assertEquals(1, buildItems.size()); + assertTrue(buildItems.contains(first)); + + // check different order + validator = new EnvVarValidator(); + validator.process(name, Optional.of("oldKey"), Optional.empty(), Optional.of(configmap), Optional.empty(), + TARGET, true); + validator.process(first); + buildItems = validator.getBuildItems(); + assertEquals(1, buildItems.size()); + assertTrue(buildItems.contains(first)); + } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvFromResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvFromResourceTest.java new file mode 100644 index 0000000000000..63a163a770a97 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvFromResourceTest.java @@ -0,0 +1,37 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildException; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithConflictingEnvFromResourceTest { + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationVersion("0.1-SNAPSHOT") + .assertBuildException(e -> assertThat(e) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(BuildException.class) + .hasMessageContaining( + "'db-password' env var can't simultaneously take its value from 'db-configmap' configmap & 'db-secret' secret")) + .withConfigurationResource("kubernetes-with-conflicting-env-from-resource.properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void buildShouldFail() throws IOException { + fail("Build should have failed and therefore this method should not have been called"); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvTest.java index 85a61fa3490f8..80ece704dc2ab 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithConflictingEnvTest.java @@ -27,7 +27,7 @@ public class KubernetesWithConflictingEnvTest { .isInstanceOf(RuntimeException.class) .hasCauseInstanceOf(BuildException.class) .hasMessageContaining( - "- 'envvar': first defined as 'var' env var with value 'value' redefined as 'field' env var with value 'field'")) + "- 'envvar': first defined as 'envvar' env var with value 'value' redefined as 'envvar' env var with value from field 'field'")) .withConfigurationResource("kubernetes-with-" + APPLICATION_NAME + "-env.properties"); @ProdBuildResults diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromConfigMapTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromConfigMapTest.java new file mode 100644 index 0000000000000..2a6f3c8038dc1 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromConfigMapTest.java @@ -0,0 +1,70 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithEnvFromConfigMapTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("env-from-configmap") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-with-env-from-configmap.properties"); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("env-from-configmap"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getContainers()).hasOnlyOneElementSatisfying(container -> { + assertThat(container.getEnvFrom()).hasOnlyOneElementSatisfying(env -> { + assertThat(env.getConfigMapRef()).satisfies(ref -> { + assertThat(ref.getName()).isEqualTo("my-configmap"); + }); + }); + + assertThat(container.getEnv()).filteredOn(env -> "DB_DATABASE".equals(env.getName())) + .hasOnlyOneElementSatisfying(env -> { + assertThat(env.getValueFrom()).satisfies(valueFrom -> { + assertThat(valueFrom.getConfigMapKeyRef()).satisfies(configMapKeyRef -> { + assertThat(configMapKeyRef.getKey()).isEqualTo("database.name"); + assertThat(configMapKeyRef.getName()).isEqualTo("db-config"); + }); + }); + }); + }); + }); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromSecretTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromSecretTest.java index e3adfb46efb89..48836c53f1df5 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromSecretTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithEnvFromSecretTest.java @@ -51,6 +51,16 @@ public void assertGeneratedResources() throws IOException { assertThat(secretRef.getName()).isEqualTo("my-secret"); }); }); + + assertThat(container.getEnv()).filteredOn(env -> "DB_PASSWORD".equals(env.getName())) + .hasOnlyOneElementSatisfying(env -> { + assertThat(env.getValueFrom()).satisfies(valueFrom -> { + assertThat(valueFrom.getSecretKeyRef()).satisfies(secretKeyRef -> { + assertThat(secretKeyRef.getKey()).isEqualTo("database.password"); + assertThat(secretKeyRef.getName()).isEqualTo("db-secret"); + }); + }); + }); }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithOldStyleEnvTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithOldStyleEnvTest.java index 6f2ce2d29c170..c7270541928df 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithOldStyleEnvTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithOldStyleEnvTest.java @@ -58,15 +58,11 @@ public void assertGeneratedResources() throws IOException { .filteredOn(env -> "ENVVAR".equals(env.getName())) .hasOnlyOneElementSatisfying(env -> assertThat(env.getValue()).isEqualTo("value")); final List envFrom = container.getEnvFrom(); - assertThat(envFrom).hasSize(2); + assertThat(envFrom).hasSize(1); assertThat(envFrom) .filteredOn(e -> e.getSecretRef() != null) .hasOnlyOneElementSatisfying( e -> assertThat(e.getSecretRef().getName()).isEqualTo("secretName")); - assertThat(envFrom) - .filteredOn(e -> e.getConfigMapRef() != null) - .hasOnlyOneElementSatisfying( - e -> assertThat(e.getConfigMapRef().getName()).isEqualTo("configName")); }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithWarningsEnvTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithWarningsEnvTest.java index 67bb6b7a1fcec..cd6857981c71f 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithWarningsEnvTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithWarningsEnvTest.java @@ -27,7 +27,8 @@ public class KubernetesWithWarningsEnvTest { .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) .setApplicationName(APPLICATION_NAME) .setApplicationVersion("0.1-SNAPSHOT") - .setLogRecordPredicate(r -> "io.quarkus.kubernetes.deployment.EnvVarValidator".equals(r.getLoggerName())) + .setLogRecordPredicate(r -> "io.quarkus.kubernetes.deployment.EnvVarValidator".equals(r.getLoggerName()) + || "io.quarkus.kubernetes.spi.KubernetesEnvBuildItem".equals(r.getLoggerName())) .withConfigurationResource("kubernetes-with-" + APPLICATION_NAME + "-env.properties"); @ProdBuildResults @@ -75,6 +76,7 @@ public void ensureProperQuarkusPropertiesLogged() throws IOException { }); List buildLogRecords = prodModeTestResults.getRetainedBuildLogRecords(); + buildLogRecords.forEach(l -> System.out.println("l = " + l.getMessage())); assertThat(buildLogRecords).hasSize(4); assertThat(buildLogRecords) .filteredOn(r -> r.getMessage().contains("my-field")) @@ -84,9 +86,10 @@ public void ensureProperQuarkusPropertiesLogged() throws IOException { .hasOnlyOneElementSatisfying(r -> assertThat(r.getMessage()).contains("newVariable")); assertThat(buildLogRecords) .filteredOn(r -> r.getMessage().contains("configMap")) - .hasSize(1); + .hasOnlyOneElementSatisfying( + r -> assertThat(r.getMessage().contains("'xxx'") && r.getMessage().contains("'secret'"))); assertThat(buildLogRecords) .filteredOn(r -> r.getMessage().contains("secret")) - .hasSize(1); + .hasSize(2); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-conflicting-env-from-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-conflicting-env-from-resource.properties new file mode 100644 index 0000000000000..89de21ed721d7 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-conflicting-env-from-resource.properties @@ -0,0 +1,3 @@ +quarkus.kubernetes.env.mapping.db-password.from-secret=db-secret +quarkus.kubernetes.env.mapping.db-password.with-key=database.password +quarkus.kubernetes.env.mapping.db-password.from-configmap=db-configmap diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-configmap.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-configmap.properties new file mode 100644 index 0000000000000..c1ddc0175bf7c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-configmap.properties @@ -0,0 +1,3 @@ +quarkus.kubernetes.env.configmaps=my-configmap +quarkus.kubernetes.env.mapping.db-database.from-configmap=db-config +quarkus.kubernetes.env.mapping.db-database.with-key=database.name diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-secret.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-secret.properties index 7fdf6c6a8117f..6dbebc49f4ec7 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-secret.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-env-from-secret.properties @@ -1 +1,3 @@ -quarkus.kubernetes.env-vars.ignored.secret=my-secret +quarkus.kubernetes.env.secrets=my-secret +quarkus.kubernetes.env.mapping.db-password.from-secret=db-secret +quarkus.kubernetes.env.mapping.db-password.with-key=database.password