From 8ea58a014069047e9cf41f250a09b9457632a759 Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Mon, 8 Feb 2021 14:00:52 +0200 Subject: [PATCH] feat: support k8s secret/configmaps as smallrye config locations (cherry picked from commit 278500d5dcf7c8cb4597c1092daee6f5f6eb14d8) --- .../asciidoc/deploying-to-kubernetes.adoc | 34 ++++++++ .../kubernetes/deployment/KnativeConfig.java | 21 +++++ .../deployment/KubernetesCommonHelper.java | 50 ++++++++++++ .../deployment/KubernetesConfig.java | 22 +++++ .../deployment/OpenshiftConfig.java | 22 +++++ .../deployment/PlatformConfiguration.java | 5 ++ .../OpenshiftWithAppConfigMapTest.java | 81 +++++++++++++++++++ .../OpenshiftWithAppSecretTest.java | 81 +++++++++++++++++++ .../openshift-with-app-config-map.properties | 1 + .../openshift-with-app-secret.properties | 1 + 10 files changed, 318 insertions(+) create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppConfigMapTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppSecretTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-config-map.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-secret.properties diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 869ee6d3b7ff5..315983f7d8f79 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -448,6 +448,34 @@ quarkus.kubernetes.secret-volumes.my-volume.secret-name=my-secret quarkus.kubernetes.config-map-volumes.my-volume.config-map-name=my-secret ---- +==== Passing application configuration + +Quarkus supports passing configuration from external locations (via Smallrye Config). This usually requires setting an additional environment variable or system propertiy. +When you need to use a secret or a config map for the purpose of application configuration, you need to: + +- define a volume +- mount the volume +- create an environment variable for `SMALLRYE_CONFIG_LOCATIONS` + +To simplify things, quarkus provides single step alternative: + +[source,properties] +---- +quarkus.kubernetes.app-secert= +---- + +or + +[source,properties] +---- +quarkus.kubernetes.app-config-map= +---- + +When these properties are used, the generated manifests will contain everything required. +The application config volumes will be created using path: `/mnt/app-secret` and `/mnt/app-config-map` for secrets and configmaps respectively. + +Note: Users may use both properties at the same time. + === Changing the number of replicas: To change the number of replicas from 1 to 3: @@ -606,6 +634,8 @@ The table below describe all the available configuration options. | quarkus.kubernetes.namespace | String | | | quarkus.kubernetes.labels | Map | | | quarkus.kubernetes.annotations | Map | | +| quarkus.kubernetes.app-secret | String | | +| quarkus.kubernetes.app-config-map | String | | | quarkus.kubernetes.env-vars | Map | | | quarkus.kubernetes.working-dir | String | | | quarkus.kubernetes.command | String[] | | @@ -844,6 +874,8 @@ The OpenShift resources can be customized in a similar approach with Kubernetes. | quarkus.openshift.init-containers | Map | | | quarkus.openshift.labels | Map | | | quarkus.openshift.annotations | Map | | +| quarkus.openshift.app-secret | String | | +| quarkus.openshift.app-config-map | String | | | quarkus.openshift.env-vars | Map | | | quarkus.openshift.working-dir | String | | | quarkus.openshift.command | String[] | | @@ -940,6 +972,8 @@ The generated service can be customized using the following properties: | quarkus.knative.init-containers | Map | | | quarkus.knative.labels | Map | | | quarkus.knative.annotations | Map | | +| quarkus.knative.app-secret | String | | +| quarkus.knative.app-config-map | String | | | quarkus.knative.env-vars | Map | | | quarkus.knative.working-dir | String | | | quarkus.knative.command | String[] | | diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java index 0b8dfb99706e0..de3ed1af05778 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeConfig.java @@ -420,4 +420,25 @@ public EnvVarsConfig getEnv() { * Traffic configuration. */ Map traffic; + + /** + * If set, the secret will mounted to the application container and its contents will be used for application configuration. + */ + @ConfigItem + Optional appSecret; + + /** + * If set, the config amp will mounted to the application container and its contents will be used for application + * configuration. + */ + @ConfigItem + Optional appConfigMap; + + public Optional getAppSecret() { + return this.appSecret; + } + + public Optional getAppConfigMap() { + return this.appConfigMap; + } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 4ddda13d9300b..f21a8402a1762 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -16,15 +16,21 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import io.dekorate.kubernetes.config.Annotation; +import io.dekorate.kubernetes.config.ConfigMapVolumeBuilder; +import io.dekorate.kubernetes.config.EnvBuilder; +import io.dekorate.kubernetes.config.MountBuilder; import io.dekorate.kubernetes.config.PortBuilder; +import io.dekorate.kubernetes.config.SecretVolumeBuilder; 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.AddHostAliasesDecorator; import io.dekorate.kubernetes.decorator.AddImagePullSecretDecorator; import io.dekorate.kubernetes.decorator.AddInitContainerDecorator; @@ -36,6 +42,7 @@ import io.dekorate.kubernetes.decorator.AddRoleBindingResourceDecorator; import io.dekorate.kubernetes.decorator.AddSecretVolumeDecorator; import io.dekorate.kubernetes.decorator.AddServiceAccountResourceDecorator; +import io.dekorate.kubernetes.decorator.ApplicationContainerDecorator; import io.dekorate.kubernetes.decorator.ApplyArgsDecorator; import io.dekorate.kubernetes.decorator.ApplyCommandDecorator; import io.dekorate.kubernetes.decorator.ApplyLimitsCpuDecorator; @@ -143,6 +150,7 @@ public static List createDecorators(Optional projec result.addAll(createPodDecorators(project, target, name, config)); result.addAll(createContainerDecorators(project, target, name, config)); result.addAll(createMountAndVolumeDecorators(project, target, name, config)); + result.addAll(createAppConfigVolumeAndEnvDecorators(project, target, name, config)); //Handle Command and arguments command.ifPresent(c -> { @@ -246,10 +254,52 @@ private static List createPodDecorators(Optional pr return result; } + private static List createAppConfigVolumeAndEnvDecorators(Optional project, String target, + String name, + PlatformConfiguration config) { + + List result = new ArrayList<>(); + Set paths = new HashSet<>(); + + config.getAppSecret().ifPresent(s -> { + result.add(new DecoratorBuildItem(target, new AddSecretVolumeDecorator(new SecretVolumeBuilder() + .withSecretName(s) + .withNewVolumeName("app-secret") + .build()))); + result.add(new DecoratorBuildItem(target, new AddMountDecorator(new MountBuilder() + .withName("app-secret") + .withPath("/mnt/app-secret") + .build()))); + paths.add("/mnt/app-secret"); + }); + + config.getAppConfigMap().ifPresent(s -> { + result.add(new DecoratorBuildItem(target, new AddConfigMapVolumeDecorator(new ConfigMapVolumeBuilder() + .withConfigMapName(s) + .withNewVolumeName("app-config-map") + .build()))); + result.add(new DecoratorBuildItem(target, new AddMountDecorator(new MountBuilder() + .withName("app-config-map") + .withPath("/mnt/app-config-map") + .build()))); + paths.add("/mnt/app-config-map"); + }); + + if (!paths.isEmpty()) { + result.add(new DecoratorBuildItem(target, + new AddEnvVarDecorator(ApplicationContainerDecorator.ANY, name, new EnvBuilder() + .withName("SMALLRYE_CONFIG_LOCATIONS") + .withValue(paths.stream().collect(Collectors.joining(","))) + .build()))); + } + return result; + } + private static List createMountAndVolumeDecorators(Optional project, String target, String name, PlatformConfiguration config) { List result = new ArrayList<>(); + config.getMounts().entrySet().forEach(e -> { result.add(new DecoratorBuildItem(target, new AddMountDecorator(MountConverter.convert(e)))); }); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java index 7822c349ad4d2..3c704885f7ec9 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesConfig.java @@ -249,6 +249,19 @@ public class KubernetesConfig implements PlatformConfiguration { @ConfigItem(defaultValue = "false") boolean deploy; + /** + * If set, the secret will mounted to the application container and its contents will be used for application configuration. + */ + @ConfigItem + Optional appSecret; + + /** + * If set, the config amp will mounted to the application container and its contents will be used for application + * configuration. + */ + @ConfigItem + Optional appConfigMap; + public Optional getPartOf() { return partOf; } @@ -425,4 +438,13 @@ public ResourcesConfig getResources() { public boolean isExpose() { return expose; } + + public Optional getAppSecret() { + return appSecret; + } + + public Optional getAppConfigMap() { + return appConfigMap; + } + } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java index 6e8fcf2872c17..ba56bc54404e3 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftConfig.java @@ -438,4 +438,26 @@ public Map getEnvVars() { public EnvVarsConfig getEnv() { return env; } + + /** + * If set, the secret will mounted to the application container and its contents will be used for application configuration. + */ + @ConfigItem + Optional appSecret; + + /** + * If set, the config amp will mounted to the application container and its contents will be used for application + * configuration. + */ + @ConfigItem + Optional appConfigMap; + + public Optional getAppSecret() { + return this.appSecret; + } + + public Optional getAppConfigMap() { + return this.appConfigMap; + } + } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java index bb3c09c551700..783168a93ebdc 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/PlatformConfiguration.java @@ -79,4 +79,9 @@ default boolean isExpose() { default String getConfigName() { return getClass().getSimpleName().replaceAll("Config$", "").toLowerCase(); } + + public Optional getAppSecret(); + + public Optional getAppConfigMap(); + } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppConfigMapTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppConfigMapTest.java new file mode 100644 index 0000000000000..fc372c14c3367 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppConfigMapTest.java @@ -0,0 +1,81 @@ +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.Collections; +import java.util.List; + +import org.assertj.core.api.AbstractObjectAssert; +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.EnvVar; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithAppConfigMapTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("openshift-with-app-config-map") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("openshift-with-app-config-map.properties") + .setForcedDependencies(Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-openshift", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List openshiftList = DeserializationUtil.deserializeAsList( + kubernetesDir.resolve("openshift.yml")); + + assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> { + assertThat(h.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("openshift-with-app-config-map"); + assertThat(m.getLabels().get("app.openshift.io/runtime")).isEqualTo("quarkus"); + }); + + AbstractObjectAssert specAssert = assertThat(h).extracting("spec"); + specAssert.extracting("template").extracting("spec").isInstanceOfSatisfying(PodSpec.class, + podSpec -> { + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + List envVars = container.getEnv(); + assertThat(envVars).anySatisfy(envVar -> { + assertThat(envVar.getName()).isEqualTo("SMALLRYE_CONFIG_LOCATIONS"); + assertThat(envVar.getValue()).isEqualTo("/mnt/app-config-map"); + }); + + List mounts = container.getVolumeMounts(); + assertThat(mounts).anySatisfy(mount -> { + assertThat(mount.getName()).isEqualTo("app-config-map"); + assertThat(mount.getMountPath()).isEqualTo("/mnt/app-config-map"); + }); + }); + List volumes = podSpec.getVolumes(); + assertThat(volumes).anySatisfy(volume -> { + assertThat(volume.getName()).isEqualTo("app-config-map"); + assertThat(volume.getConfigMap().getName()).isEqualTo("my-config-map"); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppSecretTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppSecretTest.java new file mode 100644 index 0000000000000..4ecaf821202a5 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithAppSecretTest.java @@ -0,0 +1,81 @@ +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.Collections; +import java.util.List; + +import org.assertj.core.api.AbstractObjectAssert; +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.EnvVar; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithAppSecretTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("openshift-with-app-secret") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("openshift-with-app-secret.properties") + .setForcedDependencies(Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-openshift", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List openshiftList = DeserializationUtil.deserializeAsList( + kubernetesDir.resolve("openshift.yml")); + + assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> { + assertThat(h.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("openshift-with-app-secret"); + assertThat(m.getLabels().get("app.openshift.io/runtime")).isEqualTo("quarkus"); + }); + + AbstractObjectAssert specAssert = assertThat(h).extracting("spec"); + specAssert.extracting("template").extracting("spec").isInstanceOfSatisfying(PodSpec.class, + podSpec -> { + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + List envVars = container.getEnv(); + assertThat(envVars).anySatisfy(envVar -> { + assertThat(envVar.getName()).isEqualTo("SMALLRYE_CONFIG_LOCATIONS"); + assertThat(envVar.getValue()).isEqualTo("/mnt/app-secret"); + }); + + List mounts = container.getVolumeMounts(); + assertThat(mounts).anySatisfy(mount -> { + assertThat(mount.getName()).isEqualTo("app-secret"); + assertThat(mount.getMountPath()).isEqualTo("/mnt/app-secret"); + }); + }); + List volumes = podSpec.getVolumes(); + assertThat(volumes).anySatisfy(volume -> { + assertThat(volume.getName()).isEqualTo("app-secret"); + assertThat(volume.getSecret().getSecretName()).isEqualTo("my-secret"); + }); + }); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-config-map.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-config-map.properties new file mode 100644 index 0000000000000..4d15570dc5fc3 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-config-map.properties @@ -0,0 +1 @@ +quarkus.openshift.app-config-map=my-config-map \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-secret.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-secret.properties new file mode 100644 index 0000000000000..e2c3ada9506f0 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-app-secret.properties @@ -0,0 +1 @@ +quarkus.openshift.app-secret=my-secret \ No newline at end of file