From cebc52386d3a3fd5114c9089650f2c5359aa8079 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 3 Apr 2020 13:47:41 +0300 Subject: [PATCH 1/4] Make it easy to configure Kubernetes Mock Server before the Quarkus application starts --- .../client/KubernetesMockServerTestResource.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java index b9be2c0e18a40..5ddb355fc5b61 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java @@ -27,9 +27,19 @@ public Map start() { systemProps.put(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, client.getConfiguration().getMasterUrl()); } + configureMockServer(mockServer); + return systemProps; } + /** + * Can be used by subclasses of {@code KubernetesMockServerTestResource} in order to + * setup the mock server before the Quarkus application starts + */ + public void configureMockServer(KubernetesMockServer mockServer) { + + } + @Override public void stop() { mockServer.destroy(); @@ -61,4 +71,4 @@ public void inject(Object testInstance) { private boolean useHttps() { return Boolean.getBoolean("quarkus.kubernetes-client.test.https"); } -} \ No newline at end of file +} From 45768764081d1708f904b252979cdb40fd0a831d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 3 Apr 2020 13:47:47 +0300 Subject: [PATCH 2/4] Add support for reading configuration from a ConfigMap Fixes: #6745 --- .../deployment/KubernetesConfigProcessor.java | 20 +++ extensions/kubernetes-client/runtime/pom.xml | 15 ++ .../ConfigMapConfigSourceProvider.java | 63 ++++++++ .../client/runtime/ConfigMapUtil.java | 147 ++++++++++++++++++ .../runtime/KubernetesConfigRecorder.java | 41 +++++ .../runtime/KubernetesConfigSourceConfig.java | 31 ++++ .../client/runtime/ConfigMapUtilTest.java | 126 +++++++++++++++ .../client/ConfigMapProperties.java | 55 +++++++ .../src/main/resources/application.properties | 2 + .../client/ConfigMapPropertiesIT.java | 7 + .../client/ConfigMapPropertiesTest.java | 31 ++++ ...ustomKubernetesMockServerTestResource.java | 31 ++++ .../client/KubernetesClientTest.java | 3 +- 13 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java create mode 100644 extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java create mode 100644 integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/ConfigMapProperties.java create mode 100644 integration-tests/kubernetes-client/src/main/resources/application.properties create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesIT.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java new file mode 100644 index 0000000000000..aafe6b19a321d --- /dev/null +++ b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java @@ -0,0 +1,20 @@ +package io.quarkus.kubernetes.client.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.RunTimeConfigurationSourceValueBuildItem; +import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig; +import io.quarkus.kubernetes.client.runtime.KubernetesConfigRecorder; +import io.quarkus.kubernetes.client.runtime.KubernetesConfigSourceConfig; + +public class KubernetesConfigProcessor { + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecorder recorder, + KubernetesConfigSourceConfig config, KubernetesClientBuildConfig clientConfig) { + return new RunTimeConfigurationSourceValueBuildItem( + recorder.configMaps(config, clientConfig)); + } +} diff --git a/extensions/kubernetes-client/runtime/pom.xml b/extensions/kubernetes-client/runtime/pom.xml index b014e3fd624b4..a5eb70f74a7bd 100644 --- a/extensions/kubernetes-client/runtime/pom.xml +++ b/extensions/kubernetes-client/runtime/pom.xml @@ -60,6 +60,21 @@ org.jboss.spec.javax.xml.bind jboss-jaxb-api_2.3_spec + + io.smallrye.config + smallrye-config-source-yaml + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java new file mode 100644 index 0000000000000..8bc6d3b748457 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java @@ -0,0 +1,63 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.jboss.logging.Logger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; + +class ConfigMapConfigSourceProvider implements ConfigSourceProvider { + + private static final Logger log = Logger.getLogger(ConfigMapConfigSourceProvider.class); + + private final KubernetesConfigSourceConfig config; + private final KubernetesClient client; + + public ConfigMapConfigSourceProvider(KubernetesConfigSourceConfig config, KubernetesClient client) { + this.config = config; + this.client = client; + } + + @Override + public Iterable getConfigSources(ClassLoader forClassLoader) { + if (!config.configMaps.isPresent()) { + log.debug("No ConfigMaps were configured for config source lookup"); + return Collections.emptyList(); + } + + List configMapNames = config.configMaps.orElse(Collections.emptyList()); + List result = new ArrayList<>(configMapNames.size()); + + try { + for (String configMapName : configMapNames) { + if (log.isDebugEnabled()) { + log.debug("Attempting to read ConfigMap " + configMapName); + } + ConfigMap configMap = client.configMaps().withName(configMapName).get(); + if (configMap == null) { + String message = "ConfigMap '" + configMap + "' not found in namespace '" + + client.getConfiguration().getNamespace() + "'"; + if (config.failOnMissingConfig) { + throw new RuntimeException(message); + } else { + log.info(message); + } + } else { + result.addAll(ConfigMapUtil.toConfigSources(configMap)); + if (log.isDebugEnabled()) { + log.debug("Done reading ConfigMap " + configMap); + } + } + } + return result; + } catch (Exception e) { + throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: " + + client.getConfiguration().getMasterUrl(), e); + } + } +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java new file mode 100644 index 0000000000000..a743f12213c14 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java @@ -0,0 +1,147 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.jboss.logging.Logger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.smallrye.config.common.MapBackedConfigSource; +import io.smallrye.config.source.yaml.YamlConfigSource; + +final class ConfigMapUtil { + + private static final Logger log = Logger.getLogger(ConfigMapUtil.class); + + private static final String APPLICATION_YML = "application.yml"; + private static final String APPLICATION_YAML = "application.yaml"; + private static final String APPLICATION_PROPERTIES = "application.properties"; + + private static final int ORDINAL = 270; // this is higher than the file system or jar ordinals, but lower than env vars + + private ConfigMapUtil() { + } + + /** + * Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap + * and for the application.{properties|yaml|yml} files that might be contained in it as well + * + * All the {@code ConfigSource} objects use the same ordinal which is higher than the ordinal + * of normal configuration files, but lower than that of environment variables + */ + static List toConfigSources(ConfigMap configMap) { + String configMapName = configMap.getMetadata().getName(); + if (log.isDebugEnabled()) { + log.debug("Attempting to convert data in ConfigMap '" + configMapName + "' to a list of ConfigSource objects"); + } + + ConfigMapData configMapData = parse(configMap); + List result = new ArrayList<>(configMapData.fileData.size() + 1); + + if (!configMapData.literalData.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("Adding a ConfigSource for the literal data of ConfigMap '" + configMapName + "'"); + } + result.add(new ConfigMapLiteralDataPropertiesConfigSource(configMapName + "-literalData", configMapData.literalData, + ORDINAL)); + } + for (Map.Entry entry : configMapData.fileData) { + String fileName = entry.getKey(); + String rawFileData = entry.getValue(); + if (APPLICATION_PROPERTIES.equals(fileName)) { + if (log.isDebugEnabled()) { + log.debug("Adding a Properties ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName + + "'"); + } + result.add(new ConfigMapStringInputPropertiesConfigSource(configMapName, fileName, rawFileData, ORDINAL)); + } else if (APPLICATION_YAML.equals(fileName) || APPLICATION_YML.equals(fileName)) { + if (log.isDebugEnabled()) { + log.debug("Adding a YAML ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName + "'"); + } + result.add(new ConfigMapStringInputYamlConfigSource(configMapName, fileName, rawFileData, ORDINAL)); + } + } + + if (log.isDebugEnabled()) { + log.debug("ConfigMap's '" + configMapName + "' converted into " + result.size() + "ConfigSource objects"); + } + return result; + } + + private static ConfigMapData parse(ConfigMap configMap) { + Map data = configMap.getData(); + if ((data == null) || data.isEmpty()) { + return new ConfigMapData(Collections.emptyMap(), Collections.emptyList()); + } + + Map literalData = new HashMap<>(); + List> fileData = new ArrayList<>(); + for (Map.Entry entry : data.entrySet()) { + String key = entry.getKey(); + if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) { + fileData.add(entry); + } else { + literalData.put(key, entry.getValue()); + } + } + + return new ConfigMapData(literalData, fileData); + } + + private static class ConfigMapData { + final Map literalData; + final List> fileData; + + ConfigMapData(Map literalData, List> fileData) { + this.literalData = literalData; + this.fileData = fileData; + } + } + + private static final class ConfigMapLiteralDataPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_PREFIX = "ConfigMapLiteralDataPropertiesConfigSource[configMap="; + + public ConfigMapLiteralDataPropertiesConfigSource(String configMapName, Map propertyMap, int ordinal) { + super(NAME_PREFIX + configMapName + "]", propertyMap, ordinal); + } + } + + private static class ConfigMapStringInputPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_FORMAT = "ConfigMapStringInputPropertiesConfigSource[configMap=%s,file=%s]"; + + ConfigMapStringInputPropertiesConfigSource(String configMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, configMapName, fileName), readProperties(input), ordinal); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Map readProperties(String rawData) { + try (StringReader br = new StringReader(rawData)) { + final Properties properties = new Properties(); + properties.load(br); + return (Map) (Map) properties; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class ConfigMapStringInputYamlConfigSource extends YamlConfigSource { + + private static final String NAME_FORMAT = "ConfigMapStringInputYamlConfigSource[configMap=%s,file=%s]"; + + public ConfigMapStringInputYamlConfigSource(String configMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, configMapName, fileName), input, ordinal); + } + } + +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java new file mode 100644 index 0000000000000..576f44a26a00c --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java @@ -0,0 +1,41 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.Collections; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.jboss.logging.Logger; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class KubernetesConfigRecorder { + + private static final Logger log = Logger.getLogger(KubernetesConfigRecorder.class); + + public RuntimeValue configMaps(KubernetesConfigSourceConfig kubernetesConfigSourceConfig, + KubernetesClientBuildConfig clientConfig) { + if (!kubernetesConfigSourceConfig.enabled) { + log.debug( + "No attempt will be made to obtain configuration from the Kubernetes API server because the functionality has been disabled via configuration"); + return emptyRuntimeValue(); + } + + return new RuntimeValue<>(new ConfigMapConfigSourceProvider(kubernetesConfigSourceConfig, + KubernetesClientUtils.createClient(clientConfig))); + } + + private RuntimeValue emptyRuntimeValue() { + return new RuntimeValue<>(new EmptyConfigSourceProvider()); + } + + private static class EmptyConfigSourceProvider implements ConfigSourceProvider { + + @Override + public Iterable getConfigSources(ClassLoader forClassLoader) { + return Collections.emptyList(); + } + } + +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java new file mode 100644 index 0000000000000..2228efaab9f72 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java @@ -0,0 +1,31 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "kubernetes-config", phase = ConfigPhase.BOOTSTRAP) +public class KubernetesConfigSourceConfig { + + /** + * If set to true, the application will attempt to look up the configuration from the API server + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * If set to true, the application will not start if any of the configured config sources cannot be located + */ + @ConfigItem(defaultValue = "true") + public boolean failOnMissingConfig; + + /** + * ConfigMaps to look for in the namespace that the Kubernetes Client has been configured for + */ + @ConfigItem + public Optional> configMaps; + +} diff --git a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java new file mode 100644 index 0000000000000..c1b584443c6c7 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java @@ -0,0 +1,126 @@ +package io.quarkus.kubernetes.client.runtime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; + +class ConfigMapUtilTest { + + @Test + void testEmptyData() { + ConfigMap configMap = configMapBuilder("testEmptyData").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).isEmpty(); + } + + @Test + void testOnlyLiteralData() { + ConfigMap configMap = configMapBuilder("testOnlyLiteralData") + .addToData("some.key", "someValue").addToData("some.other", "someOtherValue").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("some.key", "someValue"), + entry("some.other", "someOtherValue")); + assertThat(c.getName()).contains("testOnlyLiteralData"); + }); + } + + @Test + void testOnlySingleMatchingPropertiesData() { + ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("application.properties", "key1=value1\nkey2=value2\nsome.key=someValue").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("key2", "value2"), + entry("some.key", "someValue")); + assertThat(c.getName()).contains("testOnlySingleMatchingPropertiesData"); + }); + } + + @Test + void testOnlySingleNonMatchingPropertiesData() { + ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("app.properties", "key1=value1\nkey2=value2\nsome.key=someValue").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).isEmpty(); + } + + @Test + void testOnlySingleMatchingYamlData() { + ConfigMap configMap = configMapBuilder("testOnlySingleMatchingYamlData") + .addToData("application.yaml", "key1: value1\nkey2: value2\nsome:\n key: someValue").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("key2", "value2"), + entry("some.key", "someValue")); + assertThat(c.getName()).contains("testOnlySingleMatchingYamlData"); + }); + } + + @Test + void testOnlySingleNonMatchingYamlData() { + ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("app.yaml", "key1: value1\nkey2: value2\nsome:\n key: someValue").build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).isEmpty(); + } + + @Test + void testWithAllKindsOfData() { + ConfigMap configMap = configMapBuilder("testWithAllKindsOfData") + .addToData("some.key", "someValue") + .addToData("application.properties", "key1=value1\napp.key=val") + .addToData("app.properties", "ignored1=ignoredValue1") + .addToData("application.yaml", "key2: value2\nsome:\n otherKey: someOtherValue") + .addToData("app.yaml", "ignored2: ignoredValue2") + .addToData("application.yml", "key3: value3") + .addToData("app.yml", "ignored3: ignoredValue3") + .build(); + + List configSources = ConfigMapUtil.toConfigSources(configMap); + + assertThat(configSources).hasSize(4); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("literal")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("some.key", "someValue")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.properties")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("app.key", "val")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yaml")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("key2", "value2"), + entry("some.otherKey", "someOtherValue")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yml")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsExactly(entry("key3", "value3")); + }); + } + + private ConfigMapBuilder configMapBuilder(String name) { + return new ConfigMapBuilder().withNewMetadata() + .withName(name).endMetadata(); + } + +} diff --git a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/ConfigMapProperties.java b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/ConfigMapProperties.java new file mode 100644 index 0000000000000..adde0e1952498 --- /dev/null +++ b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/ConfigMapProperties.java @@ -0,0 +1,55 @@ +package io.quarkus.it.kubernetes.client; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Path("/configMapProperties") +public class ConfigMapProperties { + + @ConfigProperty(name = "dummy") + String dummy; + + @ConfigProperty(name = "some.prop1") + String someProp1; + + @ConfigProperty(name = "some.prop2") + String someProp2; + + @ConfigProperty(name = "some.prop3") + String someProp3; + + @ConfigProperty(name = "some.prop4") + String someProp4; + + @GET + @Path("/dummy") + public String dummy() { + return dummy; + } + + @GET + @Path("/someProp1") + public String someProp1() { + return someProp1; + } + + @GET + @Path("/someProp2") + public String someProp2() { + return someProp2; + } + + @GET + @Path("/someProp3") + public String someProp3() { + return someProp3; + } + + @GET + @Path("/someProp4") + public String someProp4() { + return someProp4; + } +} diff --git a/integration-tests/kubernetes-client/src/main/resources/application.properties b/integration-tests/kubernetes-client/src/main/resources/application.properties new file mode 100644 index 0000000000000..01eb97377b7bf --- /dev/null +++ b/integration-tests/kubernetes-client/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.kubernetes-config.enabled=true +quarkus.kubernetes-config.config-maps=cmap1,cmap2 diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesIT.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesIT.java new file mode 100644 index 0000000000000..5037a08cdc64d --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.kubernetes.client; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class ConfigMapPropertiesIT extends ConfigMapPropertiesTest { +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java new file mode 100644 index 0000000000000..fea3f52735d5c --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java @@ -0,0 +1,31 @@ +package io.quarkus.it.kubernetes.client; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTest +public class ConfigMapPropertiesTest { + + @Test + public void testPropertiesReadFromConfigMap() { + assertProperty("dummy", "dummy"); + assertProperty("someProp1", "val1"); + assertProperty("someProp2", "val2"); + assertProperty("someProp3", "val3"); + assertProperty("someProp4", "val4"); + } + + private void assertProperty(String propertyName, String expectedValue) { + given() + .when().get("/configMapProperties/" + propertyName) + .then() + .statusCode(200) + .body(is(expectedValue)); + } +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java new file mode 100644 index 0000000000000..bf5ce2be6fd13 --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java @@ -0,0 +1,31 @@ +package io.quarkus.it.kubernetes.client; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource; + +public class CustomKubernetesMockServerTestResource extends KubernetesMockServerTestResource { + + // setup the ConfigMap objects that the application expects to lookup configuration from + @Override + public void configureMockServer(KubernetesMockServer mockServer) { + mockServer.expect().get().withPath("/api/v1/namespaces/test/configmaps/cmap1") + .andReturn(200, configMapBuilder("cmap1") + .addToData("dummy", "dummy") + .addToData("some.prop1", "val1") + .addToData("some.prop2", "val2") + .addToData("application.properties", "some.prop3=val3") + .addToData("application.yaml", "some:\n prop4: val4").build()) + .once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test/configmaps/cmap2") + .andReturn(200, configMapBuilder("cmap2") + .addToData("application.yaml", "some:\n prop4: val4").build()) + .once(); + } + + private ConfigMapBuilder configMapBuilder(String name) { + return new ConfigMapBuilder().withNewMetadata() + .withName(name).endMetadata(); + } +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java index 9ab440343b7be..cb1998866e8b3 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource; import io.quarkus.test.kubernetes.client.MockServer; import io.restassured.RestAssured; @@ -19,7 +18,7 @@ * KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server * It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server */ -@QuarkusTestResource(KubernetesMockServerTestResource.class) +@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) @QuarkusTest public class KubernetesClientTest { From b537140da114878ff13c59a8c182f3443da34828 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 3 Apr 2020 16:05:06 +0300 Subject: [PATCH 3/4] Add support for reading configuration from a Secret --- .../AbstractKubernetesConfigSourceUtil.java | 110 +++++++++++++ .../ConfigMapConfigSourceProvider.java | 63 -------- .../runtime/ConfigMapConfigSourceUtil.java | 74 +++++++++ .../client/runtime/ConfigMapUtil.java | 147 ------------------ .../runtime/KubernetesConfigRecorder.java | 2 +- .../runtime/KubernetesConfigSourceConfig.java | 6 + .../KubernetesConfigSourceProvider.java | 112 +++++++++++++ .../runtime/SecretConfigSourceUtil.java | 88 +++++++++++ ...ava => ConfigMapConfigSourceUtilTest.java} | 32 ++-- .../runtime/SecretConfigSourceUtilTest.java | 133 ++++++++++++++++ .../kubernetes/client/SecretProperties.java | 55 +++++++ .../src/main/resources/application.properties | 1 + ...ustomKubernetesMockServerTestResource.java | 21 +++ .../kubernetes/client/SecretPropertiesIT.java | 7 + .../client/SecretPropertiesTest.java | 31 ++++ 15 files changed, 656 insertions(+), 226 deletions(-) create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java delete mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java delete mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java create mode 100644 extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java rename extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/{ConfigMapUtilTest.java => ConfigMapConfigSourceUtilTest.java} (71%) create mode 100644 extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java create mode 100644 integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/SecretProperties.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesIT.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java new file mode 100644 index 0000000000000..c5d9fcf2524cc --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java @@ -0,0 +1,110 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.jboss.logging.Logger; + +abstract class AbstractKubernetesConfigSourceUtil { + + private static final Logger log = Logger.getLogger(AbstractKubernetesConfigSourceUtil.class); + + private static final String APPLICATION_YML = "application.yml"; + private static final String APPLICATION_YAML = "application.yaml"; + private static final String APPLICATION_PROPERTIES = "application.properties"; + + private static final int ORDINAL = 270; // this is higher than the file system or jar ordinals, but lower than env vars + + abstract String getType(); + + abstract ConfigSource createLiteralDataConfigSource(String kubernetesConfigSourceName, Map propertyMap, + int ordinal); + + abstract ConfigSource createPropertiesConfigSource(String kubernetesConfigSourceName, String fileName, String input, + int ordinal); + + abstract ConfigSource createYamlConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal); + + /** + * Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap + * and for the application.{properties|yaml|yml} files that might be contained in it as well + * + * All the {@code ConfigSource} objects use the same ordinal which is higher than the ordinal + * of normal configuration files, but lower than that of environment variables + */ + List toConfigSources(String kubernetesConfigSourceName, Map kubernetesConfigSourceDataMap) { + if (log.isDebugEnabled()) { + log.debug("Attempting to convert data in " + getType() + " '" + kubernetesConfigSourceName + + "' to a list of ConfigSource objects"); + } + + CategorizedConfigSourceData categorizedConfigSourceData = categorize(kubernetesConfigSourceDataMap); + List result = new ArrayList<>(categorizedConfigSourceData.fileData.size() + 1); + + if (!categorizedConfigSourceData.literalData.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug( + "Adding a ConfigSource for the literal data of " + getType() + " '" + kubernetesConfigSourceName + "'"); + } + result.add(createLiteralDataConfigSource(kubernetesConfigSourceName + "-literalData", + categorizedConfigSourceData.literalData, + ORDINAL)); + } + for (Map.Entry entry : categorizedConfigSourceData.fileData) { + String fileName = entry.getKey(); + String rawFileData = entry.getValue(); + if (APPLICATION_PROPERTIES.equals(fileName)) { + if (log.isDebugEnabled()) { + log.debug("Adding a Properties ConfigSource for file '" + fileName + "' of " + getType() + + " '" + kubernetesConfigSourceName + "'"); + } + result.add(createPropertiesConfigSource(kubernetesConfigSourceName, fileName, rawFileData, ORDINAL)); + } else if (APPLICATION_YAML.equals(fileName) || APPLICATION_YML.equals(fileName)) { + if (log.isDebugEnabled()) { + log.debug("Adding a YAML ConfigSource for file '" + fileName + "' of " + getType() + + " '" + kubernetesConfigSourceName + "'"); + } + result.add(createYamlConfigSource(kubernetesConfigSourceName, fileName, rawFileData, ORDINAL)); + } + } + + if (log.isDebugEnabled()) { + log.debug(getType() + " '" + kubernetesConfigSourceName + "' converted into " + result.size() + + "ConfigSource objects"); + } + return result; + } + + private static CategorizedConfigSourceData categorize(Map data) { + if ((data == null) || data.isEmpty()) { + return new CategorizedConfigSourceData(Collections.emptyMap(), Collections.emptyList()); + } + + Map literalData = new HashMap<>(); + List> fileData = new ArrayList<>(); + for (Map.Entry entry : data.entrySet()) { + String key = entry.getKey(); + if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) { + fileData.add(entry); + } else { + literalData.put(key, entry.getValue()); + } + } + + return new CategorizedConfigSourceData(literalData, fileData); + } + + private static class CategorizedConfigSourceData { + final Map literalData; + final List> fileData; + + CategorizedConfigSourceData(Map literalData, List> fileData) { + this.literalData = literalData; + this.fileData = fileData; + } + } +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java deleted file mode 100644 index 8bc6d3b748457..0000000000000 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceProvider.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.quarkus.kubernetes.client.runtime; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.ConfigSourceProvider; -import org.jboss.logging.Logger; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.KubernetesClient; - -class ConfigMapConfigSourceProvider implements ConfigSourceProvider { - - private static final Logger log = Logger.getLogger(ConfigMapConfigSourceProvider.class); - - private final KubernetesConfigSourceConfig config; - private final KubernetesClient client; - - public ConfigMapConfigSourceProvider(KubernetesConfigSourceConfig config, KubernetesClient client) { - this.config = config; - this.client = client; - } - - @Override - public Iterable getConfigSources(ClassLoader forClassLoader) { - if (!config.configMaps.isPresent()) { - log.debug("No ConfigMaps were configured for config source lookup"); - return Collections.emptyList(); - } - - List configMapNames = config.configMaps.orElse(Collections.emptyList()); - List result = new ArrayList<>(configMapNames.size()); - - try { - for (String configMapName : configMapNames) { - if (log.isDebugEnabled()) { - log.debug("Attempting to read ConfigMap " + configMapName); - } - ConfigMap configMap = client.configMaps().withName(configMapName).get(); - if (configMap == null) { - String message = "ConfigMap '" + configMap + "' not found in namespace '" - + client.getConfiguration().getNamespace() + "'"; - if (config.failOnMissingConfig) { - throw new RuntimeException(message); - } else { - log.info(message); - } - } else { - result.addAll(ConfigMapUtil.toConfigSources(configMap)); - if (log.isDebugEnabled()) { - log.debug("Done reading ConfigMap " + configMap); - } - } - } - return result; - } catch (Exception e) { - throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: " - + client.getConfiguration().getMasterUrl(), e); - } - } -} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java new file mode 100644 index 0000000000000..384db2197a15e --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java @@ -0,0 +1,74 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.common.MapBackedConfigSource; +import io.smallrye.config.source.yaml.YamlConfigSource; + +public class ConfigMapConfigSourceUtil extends AbstractKubernetesConfigSourceUtil { + + @Override + String getType() { + return "ConfigMap"; + } + + @Override + ConfigSource createLiteralDataConfigSource(String kubernetesConfigSourceName, Map propertyMap, + int ordinal) { + return new ConfigMapLiteralDataPropertiesConfigSource(kubernetesConfigSourceName, propertyMap, ordinal); + } + + @Override + ConfigSource createPropertiesConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal) { + return new ConfigMapStringInputPropertiesConfigSource(kubernetesConfigSourceName, fileName, input, ordinal); + } + + @Override + ConfigSource createYamlConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal) { + return new ConfigMapStringInputYamlConfigSource(kubernetesConfigSourceName, fileName, input, ordinal); + } + + private static final class ConfigMapLiteralDataPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_PREFIX = "ConfigMapLiteralDataPropertiesConfigSource[configMap="; + + public ConfigMapLiteralDataPropertiesConfigSource(String configMapName, Map propertyMap, int ordinal) { + super(NAME_PREFIX + configMapName + "]", propertyMap, ordinal); + } + } + + private static class ConfigMapStringInputPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_FORMAT = "ConfigMapStringInputPropertiesConfigSource[configMap=%s,file=%s]"; + + ConfigMapStringInputPropertiesConfigSource(String configMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, configMapName, fileName), readProperties(input), ordinal); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Map readProperties(String rawData) { + try (StringReader br = new StringReader(rawData)) { + final Properties properties = new Properties(); + properties.load(br); + return (Map) (Map) properties; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class ConfigMapStringInputYamlConfigSource extends YamlConfigSource { + + private static final String NAME_FORMAT = "ConfigMapStringInputYamlConfigSource[configMap=%s,file=%s]"; + + public ConfigMapStringInputYamlConfigSource(String configMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, configMapName, fileName), input, ordinal); + } + } +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java deleted file mode 100644 index a743f12213c14..0000000000000 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtil.java +++ /dev/null @@ -1,147 +0,0 @@ -package io.quarkus.kubernetes.client.runtime; - -import java.io.IOException; -import java.io.StringReader; -import java.io.UncheckedIOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.jboss.logging.Logger; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.smallrye.config.common.MapBackedConfigSource; -import io.smallrye.config.source.yaml.YamlConfigSource; - -final class ConfigMapUtil { - - private static final Logger log = Logger.getLogger(ConfigMapUtil.class); - - private static final String APPLICATION_YML = "application.yml"; - private static final String APPLICATION_YAML = "application.yaml"; - private static final String APPLICATION_PROPERTIES = "application.properties"; - - private static final int ORDINAL = 270; // this is higher than the file system or jar ordinals, but lower than env vars - - private ConfigMapUtil() { - } - - /** - * Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap - * and for the application.{properties|yaml|yml} files that might be contained in it as well - * - * All the {@code ConfigSource} objects use the same ordinal which is higher than the ordinal - * of normal configuration files, but lower than that of environment variables - */ - static List toConfigSources(ConfigMap configMap) { - String configMapName = configMap.getMetadata().getName(); - if (log.isDebugEnabled()) { - log.debug("Attempting to convert data in ConfigMap '" + configMapName + "' to a list of ConfigSource objects"); - } - - ConfigMapData configMapData = parse(configMap); - List result = new ArrayList<>(configMapData.fileData.size() + 1); - - if (!configMapData.literalData.isEmpty()) { - if (log.isDebugEnabled()) { - log.debug("Adding a ConfigSource for the literal data of ConfigMap '" + configMapName + "'"); - } - result.add(new ConfigMapLiteralDataPropertiesConfigSource(configMapName + "-literalData", configMapData.literalData, - ORDINAL)); - } - for (Map.Entry entry : configMapData.fileData) { - String fileName = entry.getKey(); - String rawFileData = entry.getValue(); - if (APPLICATION_PROPERTIES.equals(fileName)) { - if (log.isDebugEnabled()) { - log.debug("Adding a Properties ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName - + "'"); - } - result.add(new ConfigMapStringInputPropertiesConfigSource(configMapName, fileName, rawFileData, ORDINAL)); - } else if (APPLICATION_YAML.equals(fileName) || APPLICATION_YML.equals(fileName)) { - if (log.isDebugEnabled()) { - log.debug("Adding a YAML ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName + "'"); - } - result.add(new ConfigMapStringInputYamlConfigSource(configMapName, fileName, rawFileData, ORDINAL)); - } - } - - if (log.isDebugEnabled()) { - log.debug("ConfigMap's '" + configMapName + "' converted into " + result.size() + "ConfigSource objects"); - } - return result; - } - - private static ConfigMapData parse(ConfigMap configMap) { - Map data = configMap.getData(); - if ((data == null) || data.isEmpty()) { - return new ConfigMapData(Collections.emptyMap(), Collections.emptyList()); - } - - Map literalData = new HashMap<>(); - List> fileData = new ArrayList<>(); - for (Map.Entry entry : data.entrySet()) { - String key = entry.getKey(); - if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) { - fileData.add(entry); - } else { - literalData.put(key, entry.getValue()); - } - } - - return new ConfigMapData(literalData, fileData); - } - - private static class ConfigMapData { - final Map literalData; - final List> fileData; - - ConfigMapData(Map literalData, List> fileData) { - this.literalData = literalData; - this.fileData = fileData; - } - } - - private static final class ConfigMapLiteralDataPropertiesConfigSource extends MapBackedConfigSource { - - private static final String NAME_PREFIX = "ConfigMapLiteralDataPropertiesConfigSource[configMap="; - - public ConfigMapLiteralDataPropertiesConfigSource(String configMapName, Map propertyMap, int ordinal) { - super(NAME_PREFIX + configMapName + "]", propertyMap, ordinal); - } - } - - private static class ConfigMapStringInputPropertiesConfigSource extends MapBackedConfigSource { - - private static final String NAME_FORMAT = "ConfigMapStringInputPropertiesConfigSource[configMap=%s,file=%s]"; - - ConfigMapStringInputPropertiesConfigSource(String configMapName, String fileName, String input, int ordinal) { - super(String.format(NAME_FORMAT, configMapName, fileName), readProperties(input), ordinal); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private static Map readProperties(String rawData) { - try (StringReader br = new StringReader(rawData)) { - final Properties properties = new Properties(); - properties.load(br); - return (Map) (Map) properties; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - } - - private static class ConfigMapStringInputYamlConfigSource extends YamlConfigSource { - - private static final String NAME_FORMAT = "ConfigMapStringInputYamlConfigSource[configMap=%s,file=%s]"; - - public ConfigMapStringInputYamlConfigSource(String configMapName, String fileName, String input, int ordinal) { - super(String.format(NAME_FORMAT, configMapName, fileName), input, ordinal); - } - } - -} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java index 576f44a26a00c..aa35d9bb4365b 100644 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java @@ -22,7 +22,7 @@ public RuntimeValue configMaps(KubernetesConfigSourceConfi return emptyRuntimeValue(); } - return new RuntimeValue<>(new ConfigMapConfigSourceProvider(kubernetesConfigSourceConfig, + return new RuntimeValue<>(new KubernetesConfigSourceProvider(kubernetesConfigSourceConfig, KubernetesClientUtils.createClient(clientConfig))); } diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java index 2228efaab9f72..b0133159a19c2 100644 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java @@ -28,4 +28,10 @@ public class KubernetesConfigSourceConfig { @ConfigItem public Optional> configMaps; + /** + * Secrets to look for in the namespace that the Kubernetes Client has been configured for + */ + @ConfigItem + public Optional> secrets; + } diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java new file mode 100644 index 0000000000000..297a00853bfe8 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java @@ -0,0 +1,112 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.jboss.logging.Logger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; + +class KubernetesConfigSourceProvider implements ConfigSourceProvider { + + private static final Logger log = Logger.getLogger(KubernetesConfigSourceProvider.class); + + private final KubernetesConfigSourceConfig config; + private final KubernetesClient client; + + private final ConfigMapConfigSourceUtil configMapConfigSourceUtil; + private final SecretConfigSourceUtil secretConfigSourceUtil; + + public KubernetesConfigSourceProvider(KubernetesConfigSourceConfig config, KubernetesClient client) { + this.config = config; + this.client = client; + + this.configMapConfigSourceUtil = new ConfigMapConfigSourceUtil(); + this.secretConfigSourceUtil = new SecretConfigSourceUtil(); + } + + @Override + public Iterable getConfigSources(ClassLoader forClassLoader) { + if (!config.configMaps.isPresent() && !config.secrets.isPresent()) { + log.debug("No ConfigMaps or Secrets were configured for config source lookup"); + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + if (config.configMaps.isPresent()) { + result.addAll(getConfigMapConfigSources(config.configMaps.get())); + } + if (config.secrets.isPresent()) { + result.addAll(getSecretConfigSources(config.secrets.get())); + } + return result; + } + + private List getConfigMapConfigSources(List configMapNames) { + List result = new ArrayList<>(configMapNames.size()); + + try { + for (String configMapName : configMapNames) { + if (log.isDebugEnabled()) { + log.debug("Attempting to read ConfigMap " + configMapName); + } + ConfigMap configMap = client.configMaps().withName(configMapName).get(); + if (configMap == null) { + String message = "ConfigMap '" + configMap + "' not found in namespace '" + + client.getConfiguration().getNamespace() + "'"; + if (config.failOnMissingConfig) { + throw new RuntimeException(message); + } else { + log.info(message); + } + } else { + result.addAll( + configMapConfigSourceUtil.toConfigSources(configMap.getMetadata().getName(), configMap.getData())); + if (log.isDebugEnabled()) { + log.debug("Done reading ConfigMap " + configMap); + } + } + } + return result; + } catch (Exception e) { + throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: " + + client.getConfiguration().getMasterUrl(), e); + } + } + + private List getSecretConfigSources(List secretNames) { + List result = new ArrayList<>(secretNames.size()); + + try { + for (String secretName : secretNames) { + if (log.isDebugEnabled()) { + log.debug("Attempting to read Secret " + secretName); + } + Secret secret = client.secrets().withName(secretName).get(); + if (secret == null) { + String message = "Secret '" + secret + "' not found in namespace '" + + client.getConfiguration().getNamespace() + "'"; + if (config.failOnMissingConfig) { + throw new RuntimeException(message); + } else { + log.info(message); + } + } else { + result.addAll(secretConfigSourceUtil.toConfigSources(secret.getMetadata().getName(), secret.getData())); + if (log.isDebugEnabled()) { + log.debug("Done reading Secret " + secret); + } + } + } + return result; + } catch (Exception e) { + throw new RuntimeException("Unable to obtain configuration for Secret objects for Kubernetes API Server at: " + + client.getConfiguration().getMasterUrl(), e); + } + } +} diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java new file mode 100644 index 0000000000000..e832c13421987 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java @@ -0,0 +1,88 @@ +package io.quarkus.kubernetes.client.runtime; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.config.common.MapBackedConfigSource; +import io.smallrye.config.source.yaml.YamlConfigSource; + +public class SecretConfigSourceUtil extends AbstractKubernetesConfigSourceUtil { + + @Override + String getType() { + return "Secret"; + } + + @Override + ConfigSource createLiteralDataConfigSource(String kubernetesConfigSourceName, Map propertyMap, + int ordinal) { + return new SecretLiteralDataPropertiesConfigSource(kubernetesConfigSourceName, propertyMap, ordinal); + } + + @Override + ConfigSource createPropertiesConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal) { + return new SecretStringInputPropertiesConfigSource(kubernetesConfigSourceName, fileName, input, ordinal); + } + + @Override + ConfigSource createYamlConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal) { + return new SecretStringInputYamlConfigSource(kubernetesConfigSourceName, fileName, input, ordinal); + } + + static String decodeValue(String value) { + return new String(Base64.getDecoder().decode(value)); + } + + static Map decodeMapValues(Map input) { + Map decodedMap = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + decodedMap.put(entry.getKey(), decodeValue(entry.getValue())); + } + return decodedMap; + } + + private static final class SecretLiteralDataPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_PREFIX = "SecretLiteralDataPropertiesConfigSource[secret="; + + public SecretLiteralDataPropertiesConfigSource(String secretMapName, Map propertyMap, int ordinal) { + super(NAME_PREFIX + secretMapName + "]", decodeMapValues(propertyMap), ordinal); + } + } + + private static class SecretStringInputPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_FORMAT = "SecretStringInputPropertiesConfigSource[secret=%s,file=%s]"; + + SecretStringInputPropertiesConfigSource(String secretMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, secretMapName, fileName), readProperties(decodeValue(input)), ordinal); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Map readProperties(String rawData) { + try (StringReader br = new StringReader(rawData)) { + final Properties properties = new Properties(); + properties.load(br); + return (Map) (Map) properties; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class SecretStringInputYamlConfigSource extends YamlConfigSource { + + private static final String NAME_FORMAT = "SecretStringInputYamlConfigSource[secret=%s,file=%s]"; + + public SecretStringInputYamlConfigSource(String secretMapName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, secretMapName, fileName), decodeValue(input), ordinal); + } + } +} diff --git a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java similarity index 71% rename from extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java rename to extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java index c1b584443c6c7..43dcbb657c5f2 100644 --- a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapUtilTest.java +++ b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java @@ -11,13 +11,15 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -class ConfigMapUtilTest { +class ConfigMapConfigSourceUtilTest { + + ConfigMapConfigSourceUtil sut = new ConfigMapConfigSourceUtil(); @Test void testEmptyData() { ConfigMap configMap = configMapBuilder("testEmptyData").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).isEmpty(); } @@ -27,10 +29,10 @@ void testOnlyLiteralData() { ConfigMap configMap = configMapBuilder("testOnlyLiteralData") .addToData("some.key", "someValue").addToData("some.other", "someOtherValue").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("some.key", "someValue"), + assertThat(c.getProperties()).containsOnly(entry("some.key", "someValue"), entry("some.other", "someOtherValue")); assertThat(c.getName()).contains("testOnlyLiteralData"); }); @@ -41,10 +43,10 @@ void testOnlySingleMatchingPropertiesData() { ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") .addToData("application.properties", "key1=value1\nkey2=value2\nsome.key=someValue").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("key2", "value2"), + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("key2", "value2"), entry("some.key", "someValue")); assertThat(c.getName()).contains("testOnlySingleMatchingPropertiesData"); }); @@ -55,7 +57,7 @@ void testOnlySingleNonMatchingPropertiesData() { ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") .addToData("app.properties", "key1=value1\nkey2=value2\nsome.key=someValue").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).isEmpty(); } @@ -65,10 +67,10 @@ void testOnlySingleMatchingYamlData() { ConfigMap configMap = configMapBuilder("testOnlySingleMatchingYamlData") .addToData("application.yaml", "key1: value1\nkey2: value2\nsome:\n key: someValue").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("key2", "value2"), + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("key2", "value2"), entry("some.key", "someValue")); assertThat(c.getName()).contains("testOnlySingleMatchingYamlData"); }); @@ -79,7 +81,7 @@ void testOnlySingleNonMatchingYamlData() { ConfigMap configMap = configMapBuilder("testOnlySingleMatchingPropertiesData") .addToData("app.yaml", "key1: value1\nkey2: value2\nsome:\n key: someValue").build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).isEmpty(); } @@ -96,25 +98,25 @@ void testWithAllKindsOfData() { .addToData("app.yml", "ignored3: ignoredValue3") .build(); - List configSources = ConfigMapUtil.toConfigSources(configMap); + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); assertThat(configSources).hasSize(4); assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("literal")) .hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("some.key", "someValue")); + assertThat(c.getProperties()).containsOnly(entry("some.key", "someValue")); }); assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.properties")) .hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("key1", "value1"), entry("app.key", "val")); + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("app.key", "val")); }); assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yaml")) .hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("key2", "value2"), + assertThat(c.getProperties()).containsOnly(entry("key2", "value2"), entry("some.otherKey", "someOtherValue")); }); assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yml")) .hasOnlyOneElementSatisfying(c -> { - assertThat(c.getProperties()).containsExactly(entry("key3", "value3")); + assertThat(c.getProperties()).containsOnly(entry("key3", "value3")); }); } diff --git a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java new file mode 100644 index 0000000000000..6a22ab22cb7f6 --- /dev/null +++ b/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java @@ -0,0 +1,133 @@ +package io.quarkus.kubernetes.client.runtime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.util.Base64; +import java.util.List; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; + +class SecretConfigSourceUtilTest { + + SecretConfigSourceUtil sut = new SecretConfigSourceUtil(); + + @Test + void testEmptyData() { + Secret secret = secretMapBuilder("testEmptyData").build(); + + List configSources = sut.toConfigSources(secret.getMetadata().getName(), secret.getData()); + + assertThat(configSources).isEmpty(); + } + + @Test + void testOnlyLiteralData() { + Secret configMap = secretMapBuilder("testOnlyLiteralData") + .addToData("some.key", encodeValue("someValue")).addToData("some.other", encodeValue("someOtherValue")).build(); + + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("some.key", "someValue"), + entry("some.other", "someOtherValue")); + assertThat(c.getName()).contains("testOnlyLiteralData"); + }); + } + + @Test + void testOnlySingleMatchingPropertiesData() { + Secret secret = secretMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("application.properties", encodeValue("key1=value1\nkey2=value2\nsome.key=someValue")).build(); + + List configSources = sut.toConfigSources(secret.getMetadata().getName(), secret.getData()); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("key2", "value2"), + entry("some.key", "someValue")); + assertThat(c.getName()).contains("testOnlySingleMatchingPropertiesData"); + }); + } + + @Test + void testOnlySingleNonMatchingPropertiesData() { + Secret secret = secretMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("app.properties", encodeValue("key1=value1\nkey2=value2\nsome.key=someValue")).build(); + + List configSources = sut.toConfigSources(secret.getMetadata().getName(), secret.getData()); + + assertThat(configSources).isEmpty(); + } + + @Test + void testOnlySingleMatchingYamlData() { + Secret configMap = secretMapBuilder("testOnlySingleMatchingYamlData") + .addToData("application.yaml", encodeValue("key1: value1\nkey2: value2\nsome:\n key: someValue")).build(); + + List configSources = sut.toConfigSources(configMap.getMetadata().getName(), configMap.getData()); + + assertThat(configSources).hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("key2", "value2"), + entry("some.key", "someValue")); + assertThat(c.getName()).contains("testOnlySingleMatchingYamlData"); + }); + } + + @Test + void testOnlySingleNonMatchingYamlData() { + Secret secret = secretMapBuilder("testOnlySingleMatchingPropertiesData") + .addToData("app.yaml", encodeValue("key1: value1\nkey2: value2\nsome:\n key: someValue")).build(); + + List configSources = sut.toConfigSources(secret.getMetadata().getName(), secret.getData()); + + assertThat(configSources).isEmpty(); + } + + @Test + void testWithAllKindsOfData() { + Secret secret = secretMapBuilder("testWithAllKindsOfData") + .addToData("some.key", encodeValue("someValue")) + .addToData("application.properties", encodeValue("key1=value1\napp.key=val")) + .addToData("app.properties", encodeValue("ignored1=ignoredValue1")) + .addToData("application.yaml", encodeValue("key2: value2\nsome:\n otherKey: someOtherValue")) + .addToData("app.yaml", encodeValue("ignored2: ignoredValue2")) + .addToData("application.yml", encodeValue("key3: value3")) + .addToData("app.yml", encodeValue("ignored3: ignoredValue3")) + .build(); + + List configSources = sut.toConfigSources(secret.getMetadata().getName(), secret.getData()); + + assertThat(configSources).hasSize(4); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("literal")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("some.key", "someValue")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.properties")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("key1", "value1"), entry("app.key", "val")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yaml")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("key2", "value2"), + entry("some.otherKey", "someOtherValue")); + }); + assertThat(configSources).filteredOn(c -> c.getName().toLowerCase().contains("application.yml")) + .hasOnlyOneElementSatisfying(c -> { + assertThat(c.getProperties()).containsOnly(entry("key3", "value3")); + }); + } + + private SecretBuilder secretMapBuilder(String name) { + return new SecretBuilder().withNewMetadata() + .withName(name).endMetadata(); + } + + private String encodeValue(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } + +} diff --git a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/SecretProperties.java b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/SecretProperties.java new file mode 100644 index 0000000000000..24db0dbe770b5 --- /dev/null +++ b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/SecretProperties.java @@ -0,0 +1,55 @@ +package io.quarkus.it.kubernetes.client; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@Path("/secretProperties") +public class SecretProperties { + + @ConfigProperty(name = "dummysecret") + String dummysecret; + + @ConfigProperty(name = "secret.prop1") + String secretProp1; + + @ConfigProperty(name = "secret.prop2") + String secretProp2; + + @ConfigProperty(name = "secret.prop3") + String secretProp3; + + @ConfigProperty(name = "secret.prop4") + String secretProp4; + + @GET + @Path("/dummysecret") + public String dummysecret() { + return dummysecret; + } + + @GET + @Path("/secretProp1") + public String secretProp1() { + return secretProp1; + } + + @GET + @Path("/secretProp2") + public String secretProp2() { + return secretProp2; + } + + @GET + @Path("/secretProp3") + public String secretProp3() { + return secretProp3; + } + + @GET + @Path("/secretProp4") + public String secretProp4() { + return secretProp4; + } +} diff --git a/integration-tests/kubernetes-client/src/main/resources/application.properties b/integration-tests/kubernetes-client/src/main/resources/application.properties index 01eb97377b7bf..7718cf92c7718 100644 --- a/integration-tests/kubernetes-client/src/main/resources/application.properties +++ b/integration-tests/kubernetes-client/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.kubernetes-config.enabled=true quarkus.kubernetes-config.config-maps=cmap1,cmap2 +quarkus.kubernetes-config.secrets=s1 diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java index bf5ce2be6fd13..69b486a4fb11b 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesMockServerTestResource.java @@ -1,6 +1,9 @@ package io.quarkus.it.kubernetes.client; +import java.util.Base64; + import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource; @@ -22,10 +25,28 @@ public void configureMockServer(KubernetesMockServer mockServer) { .andReturn(200, configMapBuilder("cmap2") .addToData("application.yaml", "some:\n prop4: val4").build()) .once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test/secrets/s1") + .andReturn(200, secretBuilder("s1") + .addToData("dummysecret", encodeValue("dummysecret")) + .addToData("secret.prop1", encodeValue("val1")) + .addToData("secret.prop2", encodeValue("val2")) + .addToData("application.properties", encodeValue("secret.prop3=val3")) + .addToData("application.yaml", encodeValue("secret:\n prop4: val4")).build()) + .once(); } private ConfigMapBuilder configMapBuilder(String name) { return new ConfigMapBuilder().withNewMetadata() .withName(name).endMetadata(); } + + private SecretBuilder secretBuilder(String name) { + return new SecretBuilder().withNewMetadata() + .withName(name).endMetadata(); + } + + private String encodeValue(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } } diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesIT.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesIT.java new file mode 100644 index 0000000000000..5962e30562abf --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.kubernetes.client; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class SecretPropertiesIT extends SecretPropertiesTest { +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java new file mode 100644 index 0000000000000..1ec04e06800b3 --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java @@ -0,0 +1,31 @@ +package io.quarkus.it.kubernetes.client; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTest +public class SecretPropertiesTest { + + @Test + public void testPropertiesReadFromConfigMap() { + assertProperty("dummysecret", "dummysecret"); + assertProperty("secretProp1", "val1"); + assertProperty("secretProp2", "val2"); + assertProperty("secretProp3", "val3"); + assertProperty("secretProp4", "val4"); + } + + private void assertProperty(String propertyName, String expectedValue) { + given() + .when().get("/secretProperties/" + propertyName) + .then() + .statusCode(200) + .body(is(expectedValue)); + } +} From feaddb819d1dd5dd0826c9fd1dcb3d4df6a46ad1 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 3 Apr 2020 16:44:45 +0300 Subject: [PATCH 4/4] Move reading from ConfigMap and Secrets to a dedicated extension --- bom/deployment/pom.xml | 5 ++ bom/runtime/pom.xml | 5 ++ extensions/kubernetes-client/runtime/pom.xml | 11 ---- .../kubernetes-config/deployment/pom.xml | 43 ++++++++++++++ .../deployment/KubernetesConfigProcessor.java | 4 +- extensions/kubernetes-config/pom.xml | 20 +++++++ extensions/kubernetes-config/runtime/pom.xml | 56 +++++++++++++++++++ .../AbstractKubernetesConfigSourceUtil.java | 0 .../runtime/ConfigMapConfigSourceUtil.java | 0 .../runtime/KubernetesConfigRecorder.java | 2 +- .../runtime/KubernetesConfigSourceConfig.java | 0 .../KubernetesConfigSourceProvider.java | 0 .../runtime/SecretConfigSourceUtil.java | 0 .../resources/META-INF/quarkus-extension.yaml | 9 +++ .../ConfigMapConfigSourceUtilTest.java | 0 .../runtime/SecretConfigSourceUtilTest.java | 0 extensions/pom.xml | 1 + integration-tests/kubernetes-client/pom.xml | 2 +- 18 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 extensions/kubernetes-config/deployment/pom.xml rename extensions/{kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client => kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config}/deployment/KubernetesConfigProcessor.java (87%) create mode 100644 extensions/kubernetes-config/pom.xml create mode 100644 extensions/kubernetes-config/runtime/pom.xml rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java (100%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java (100%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java (92%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java (100%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java (100%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java (100%) create mode 100644 extensions/kubernetes-config/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java (100%) rename extensions/{kubernetes-client => kubernetes-config}/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java (100%) diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 119df2bdb8cc7..5bcc97b0bd12d 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -593,6 +593,11 @@ quarkus-kubernetes-client-deployment-internal ${project.version} + + io.quarkus + quarkus-kubernetes-config-deployment + ${project.version} + io.quarkus quarkus-flyway-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index d7fa5f9a82bf2..deb8da988eacb 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -810,6 +810,11 @@ quarkus-kubernetes-client-internal ${project.version} + + io.quarkus + quarkus-kubernetes-config + ${project.version} + io.quarkus quarkus-optaplanner diff --git a/extensions/kubernetes-client/runtime/pom.xml b/extensions/kubernetes-client/runtime/pom.xml index a5eb70f74a7bd..70a79f21f9d0c 100644 --- a/extensions/kubernetes-client/runtime/pom.xml +++ b/extensions/kubernetes-client/runtime/pom.xml @@ -64,17 +64,6 @@ io.smallrye.config smallrye-config-source-yaml - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - test - diff --git a/extensions/kubernetes-config/deployment/pom.xml b/extensions/kubernetes-config/deployment/pom.xml new file mode 100644 index 0000000000000..cf9f194469148 --- /dev/null +++ b/extensions/kubernetes-config/deployment/pom.xml @@ -0,0 +1,43 @@ + + + + quarkus-kubernetes-config-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-kubernetes-config-deployment + Quarkus - Kubernetes Config - Deployment + + + + io.quarkus + quarkus-kubernetes-client-deployment + + + + io.quarkus + quarkus-kubernetes-config + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java similarity index 87% rename from extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java rename to extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java index aafe6b19a321d..7a4c3991dc38e 100644 --- a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesConfigProcessor.java +++ b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.kubernetes.client.deployment; +package io.quarkus.kubernetes.config.deployment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -15,6 +15,6 @@ public class KubernetesConfigProcessor { public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecorder recorder, KubernetesConfigSourceConfig config, KubernetesClientBuildConfig clientConfig) { return new RunTimeConfigurationSourceValueBuildItem( - recorder.configMaps(config, clientConfig)); + recorder.configSources(config, clientConfig)); } } diff --git a/extensions/kubernetes-config/pom.xml b/extensions/kubernetes-config/pom.xml new file mode 100644 index 0000000000000..6941dbc222213 --- /dev/null +++ b/extensions/kubernetes-config/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-kubernetes-config-parent + Quarkus - Kubernetes Config + pom + + deployment + runtime + + diff --git a/extensions/kubernetes-config/runtime/pom.xml b/extensions/kubernetes-config/runtime/pom.xml new file mode 100644 index 0000000000000..58cf732b37278 --- /dev/null +++ b/extensions/kubernetes-config/runtime/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + io.quarkus + quarkus-kubernetes-config-parent + 999-SNAPSHOT + + + quarkus-kubernetes-config + Quarkus - Kubernetes Config - Runtime + Read runtime configuration from Kubernetes ConfigMaps and Secrets + + + io.quarkus + quarkus-kubernetes-client + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/AbstractKubernetesConfigSourceUtil.java diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtil.java diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java similarity index 92% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java index aa35d9bb4365b..648d5c841670f 100644 --- a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java +++ b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigRecorder.java @@ -14,7 +14,7 @@ public class KubernetesConfigRecorder { private static final Logger log = Logger.getLogger(KubernetesConfigRecorder.class); - public RuntimeValue configMaps(KubernetesConfigSourceConfig kubernetesConfigSourceConfig, + public RuntimeValue configSources(KubernetesConfigSourceConfig kubernetesConfigSourceConfig, KubernetesClientBuildConfig clientConfig) { if (!kubernetesConfigSourceConfig.enabled) { log.debug( diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceConfig.java diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProvider.java diff --git a/extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java rename to extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java diff --git a/extensions/kubernetes-config/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/kubernetes-config/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..82d9f52268c24 --- /dev/null +++ b/extensions/kubernetes-config/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +--- +name: "Kubernetes Config" +metadata: + keywords: + - "kubernetes-config" + guide: "https://quarkus.io/guides/kubernetes-config" + categories: + - "cloud" + status: "preview" diff --git a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java b/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java rename to extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/ConfigMapConfigSourceUtilTest.java diff --git a/extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java b/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java similarity index 100% rename from extensions/kubernetes-client/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java rename to extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtilTest.java diff --git a/extensions/pom.xml b/extensions/pom.xml index 9e4763a2500a7..f75dde9f61ffa 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -145,6 +145,7 @@ container-image kubernetes kubernetes-client + kubernetes-config flyway diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index a1e3d9f794c10..b837827e0d15b 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -21,7 +21,7 @@ io.quarkus - quarkus-kubernetes-client + quarkus-kubernetes-config io.rest-assured