From 4eab152a07579dea6c53cd0de8e668febb327019 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 20 Jul 2020 11:26:30 +0200 Subject: [PATCH 1/3] restore the ConfigSource based on Kubernetes Secrets This reverts commit 0c71c47152681cb23b1928a12b202c283e5e27fb and adds a couple of cleanups. Most importantly, the issue which caused this functionality to be removed remains: the application by default runs under a role that doesn't have access to secrets. This is marked with one TODO in the code and will be addressed in a subsequent commit. --- .../AbstractKubernetesConfigSourceUtil.java | 5 +- .../runtime/KubernetesConfigSourceConfig.java | 8 +- .../KubernetesConfigSourceProvider.java | 48 ++++++- .../runtime/SecretConfigSourceUtil.java | 88 ++++++++++++ .../KubernetesConfigSourceProviderTest.java | 1 + .../runtime/SecretConfigSourceUtilTest.java | 133 ++++++++++++++++++ .../kubernetes/client/SecretProperties.java | 55 ++++++++ ...ustomKubernetesMockServerTestResource.java | 20 +++ .../kubernetes/client/SecretPropertiesIT.java | 7 + .../client/SecretPropertiesTest.java | 31 ++++ 10 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/SecretConfigSourceUtil.java create mode 100644 extensions/kubernetes-config/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-config/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 index c5d9fcf2524cc..6d58dc71b7929 100644 --- a/extensions/kubernetes-config/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 @@ -30,7 +30,7 @@ abstract ConfigSource createPropertiesConfigSource(String kubernetesConfigSource 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 + * Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap/Secret * 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 @@ -69,6 +69,9 @@ List toConfigSources(String kubernetesConfigSourceName, Map> configMaps; /** - * Namespace to look for config maps. If this is not specified, then the namespace configured in the kubectl config context + * Secrets to look for in the namespace that the Kubernetes Client has been configured for + */ + @ConfigItem + public Optional> secrets; + + /** + * Namespace to look for config maps and secrets. If this is not specified, then the namespace configured in the kubectl config context * is used. If the value is specified and the namespace doesn't exist, the application will fail to start. */ @ConfigItem diff --git a/extensions/kubernetes-config/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 index 53a28645fc357..a384c2bcb8533 100644 --- a/extensions/kubernetes-config/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 @@ -9,6 +9,7 @@ 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 { @@ -19,18 +20,20 @@ class KubernetesConfigSourceProvider implements ConfigSourceProvider { 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()) { - log.debug("No ConfigMaps were configured for config source lookup"); + if (!config.configMaps.isPresent() && !config.secrets.isPresent()) { + log.debug("No ConfigMaps or Secrets were configured for config source lookup"); return Collections.emptyList(); } @@ -38,6 +41,10 @@ public Iterable getConfigSources(ClassLoader forClassLoader) { if (config.configMaps.isPresent()) { result.addAll(getConfigMapConfigSources(config.configMaps.get())); } + if (config.secrets.isPresent()) { + // TODO generate a role with permissions to read secrets, and a role binding for that role + result.addAll(getSecretConfigSources(config.secrets.get())); + } return result; } @@ -70,7 +77,40 @@ private List getConfigMapConfigSources(List configMapNames } return result; } catch (Exception e) { - throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: " + throw new RuntimeException("Unable to obtain configuration for ConfigMap objects from 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; + String namespace; + if (config.namespace.isPresent()) { + namespace = config.namespace.get(); + secret = client.secrets().inNamespace(namespace).withName(secretName).get(); + } else { + namespace = client.getNamespace(); + secret = client.secrets().withName(secretName).get(); + } + if (secret == null) { + logMissingOrFail(secretName, namespace, "Secret", config.failOnMissingConfig); + } 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 from Kubernetes API Server at: " + client.getConfiguration().getMasterUrl(), e); } } @@ -79,7 +119,7 @@ private void logMissingOrFail(String name, String namespace, String type, boolea String message = type + " '" + name + "' not found"; if (namespace == null) { message = message - + ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the ConfigMap"; + + ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the " + type; } else { message = message + " in namespace '" + namespace + "'"; } diff --git a/extensions/kubernetes-config/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 new file mode 100644 index 0000000000000..959076b77a9d6 --- /dev/null +++ b/extensions/kubernetes-config/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 secretName, Map propertyMap, int ordinal) { + super(NAME_PREFIX + secretName + "]", decodeMapValues(propertyMap), ordinal); + } + } + + private static class SecretStringInputPropertiesConfigSource extends MapBackedConfigSource { + + private static final String NAME_FORMAT = "SecretStringInputPropertiesConfigSource[secret=%s,file=%s]"; + + SecretStringInputPropertiesConfigSource(String secretName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, secretName, 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 secretName, String fileName, String input, int ordinal) { + super(String.format(NAME_FORMAT, secretName, fileName), decodeValue(input), ordinal); + } + } +} diff --git a/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProviderTest.java b/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProviderTest.java index f0c075ab490ad..a255c646c2d94 100644 --- a/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProviderTest.java +++ b/extensions/kubernetes-config/runtime/src/test/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigSourceProviderTest.java @@ -155,6 +155,7 @@ private KubernetesConfigSourceConfig defaultConfig() { KubernetesConfigSourceConfig config = new KubernetesConfigSourceConfig(); config.namespace = Optional.empty(); config.configMaps = Optional.empty(); + config.secrets = Optional.empty(); return config; } diff --git a/extensions/kubernetes-config/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 new file mode 100644 index 0000000000000..6a22ab22cb7f6 --- /dev/null +++ b/extensions/kubernetes-config/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/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 a5c9636793ba4..10731a4f2e49d 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; @@ -33,6 +36,15 @@ public void configureMockServer(KubernetesMockServer mockServer) { .addToData("application.properties", "some.prop3=val3FromDemo") .addToData("application.yaml", "some:\n prop4: val4FromDemo").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) { @@ -40,4 +52,12 @@ private ConfigMapBuilder configMapBuilder(String name) { .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 e91c1d8668d0b2c1a815dbd5c171ad524cb89ad6 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Thu, 23 Jul 2020 17:15:56 +0200 Subject: [PATCH 2/3] support reading configuration from Kubernetes Secrets The `kubernetes` extension automatically generates a RoleBinding that refers to the `view` ClusterRole. This ClusterRole doesn't allow access to secrets. This commit therefore adds a configuration property which, when enabled, makes the `kubernetes` extension generate a special Role `view-secrets` and a second RoleBinding referring to that role. This configuration property is build-time only and has no other effect. With this configuration in place, there's nothing preventing the application from reading Secrets directly from the API server. For convenience, a warning is printed at runtime if configuration is read from Secrets yet the property is disabled. --- bom/application/pom.xml | 2 +- docs/src/main/asciidoc/kubernetes-config.adoc | 25 +++-- .../deployment/KubernetesClientProcessor.java | 6 +- .../deployment/KubernetesConfigProcessor.java | 25 +++++ .../KubernetesConfigBuildTimeConfig.java | 15 +++ .../runtime/KubernetesConfigRecorder.java | 11 ++ .../runtime/KubernetesConfigSourceConfig.java | 7 +- .../KubernetesConfigSourceProvider.java | 4 +- .../spi/KubernetesRoleBindingBuildItem.java | 49 +++++++++ .../spi/KubernetesRoleBuildItem.java | 73 ++++++++++++- .../deployment/AddRoleResourceDecorator.java | 43 ++++++++ .../deployment/KubernetesProcessor.java | 13 ++- ...ustomKubernetesMockServerTestResource.java | 9 ++ .../NamespacedConfigMapPropertiesTest.java | 20 ++-- .../client/SecretPropertiesTest.java | 2 +- .../KubernetesConfigWithSecretsTest.java | 100 ++++++++++++++++++ .../kubernetes-config-with-secrets.properties | 3 + 17 files changed, 374 insertions(+), 33 deletions(-) create mode 100644 extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigBuildTimeConfig.java create mode 100644 extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java create mode 100644 extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets.properties diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 00d5a87686a34..fb3f3a245a5a8 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -146,7 +146,7 @@ 2.31.0 1.3.0 1.3.72 - 0.12.6 + 0.12.7 0.10.0 2.14.6 3.0.1 diff --git a/docs/src/main/asciidoc/kubernetes-config.adoc b/docs/src/main/asciidoc/kubernetes-config.adoc index 4eeb8949a102e..f2001704e620e 100644 --- a/docs/src/main/asciidoc/kubernetes-config.adoc +++ b/docs/src/main/asciidoc/kubernetes-config.adoc @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc include::./attributes.adoc[] -Quarkus includes the `kubernetes-config` extension which allows developers to use Kubernetes https://cloud.google.com/kubernetes-engine/docs/concepts/configmap[ConfigMaps] as a configuration source, without having to mount the ConfigMaps into the https://kubernetes.io/docs/concepts/workloads/pods/pod/[Pod] running the Quarkus application. +Quarkus includes the `kubernetes-config` extension which allows developers to use Kubernetes https://cloud.google.com/kubernetes-engine/docs/concepts/configmap[ConfigMaps] and https://cloud.google.com/kubernetes-engine/docs/concepts/secret[Secrets] as a configuration source, without having to mount them into the https://kubernetes.io/docs/concepts/workloads/pods/pod/[Pod] running the Quarkus application. == Configuration @@ -33,19 +33,23 @@ This will add the following to your `pom.xml`: == Usage -The extension works by reading ConfigMaps directly from the Kubernetes API server using the link:kubernetes-client[Kubernetes Client]. +The extension works by reading ConfigMaps and Secrets directly from the Kubernetes API server using the link:kubernetes-client[Kubernetes Client]. -The extension understands the following types of ConfigMaps as input sources: +The extension understands the following types of ConfigMaps and Secrets as input sources: -* ConfigMaps that contain literal data (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-literal-values[this] for an example on how to create one) -* ConfigMaps created from files named, `application.properties`, `application.yaml` or `application.yml` (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-files[this] for an example on how to create one). +* ConfigMaps and Secrets that contain literal data (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-literal-values[this] for an example on how to create one) +* ConfigMaps and Secrets created from files named `application.properties`, `application.yaml` or `application.yml` (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-files[this] for an example on how to create one). -To configure which ConfigMaps (from the namespace that the application runs in, or the explicitly configured namespace via `quarkus.kubernetes-client.namespace`), you can set the `quarkus.kubernetes-config.config-maps` property (in any of the usual Quarkus ways). If access to ConfigMaps from a specific namespace you can set the -`quarkus.kubernetes-config.namespace` property. Keep in mind however that you will also have to explicitly enable the retrieval of ConfigMaps by setting `quarkus.kubernetes-config.enabled=true` (the default is `false` in order to make it easy to test the application locally). +You have to explicitly enable the retrieval of ConfigMaps and Secrets by setting `quarkus.kubernetes-config.enabled=true`. +The default is `false` in order to make it easy to test the application locally. + +Afterwards, set the `quarkus.kubernetes-config.configmaps` property to configure which ConfigMaps should be used. +Set the `quarkus.kubernetes-config.secrets` property to configure which Secrets should be used. +To access ConfigMaps and Secrets from a specific namespace, you can set the `quarkus.kubernetes-config.namespace` property. === Priority of obtained properties -The properties obtained from the ConfigMaps have a higher priority (i.e. they override) any properties of the same name that are found in `application.properties` (or the YAML equivalents), but they have lower priority than properties set via Environment Variables or Java System Properties. +The properties obtained from the ConfigMaps and Secrets have a higher priority than (i.e. they override) any properties of the same name that are found in `application.properties` (or the YAML equivalents), but they have lower priority than properties set via Environment Variables or Java System Properties. === Kubernetes Permissions @@ -54,6 +58,11 @@ that is used to run the application needs to have the proper permissions for suc Thankfully, when using the `kubernetes-config` extension along with the link:deploying-to-kubernetes[Kubernetes] extension, all the necessary Kubernetes resources to make that happen are automatically generated. +==== Secrets + +By default, the link:deploying-to-kubernetes[Kubernetes] extension doesn't generate the necessary resources to allow accessing secrets. +Set `quarkus.kubernetes-config.secrets.enabled=true` to generate the necessary role and corresponding role binding. + == Configuration Reference include::{generated-dir}/config/quarkus-kubernetes-config-kubernetes-config-source-config.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java index b004defffbf94..fa3bc2e8b3657 100644 --- a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java +++ b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientProcessor.java @@ -26,7 +26,7 @@ import io.quarkus.deployment.util.JandexUtil; import io.quarkus.jackson.deployment.IgnoreJsonDeserializeClassBuildItem; import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer; -import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; +import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; public class KubernetesClientProcessor { @@ -53,7 +53,7 @@ public class KubernetesClientProcessor { BuildProducer ignoredJsonDeserializationClasses; @Inject - BuildProducer roleProducer; + BuildProducer roleBindingProducer; @BuildStep public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBuildItem combinedIndexBuildItem, @@ -61,7 +61,7 @@ public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBui BuildProducer additionalBeanBuildItemBuildItem) { featureProducer.produce(new FeatureBuildItem(Feature.KUBERNETES_CLIENT)); - roleProducer.produce(new KubernetesRoleBuildItem("view")); + roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view", true)); // make sure the watchers fully (and not weakly) register Kubernetes classes for reflection final Set watchedClasses = new HashSet<>(); diff --git a/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java index 648179f3f878b..34a53b365112d 100644 --- a/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java +++ b/extensions/kubernetes-config/deployment/src/main/java/io/quarkus/kubernetes/config/deployment/KubernetesConfigProcessor.java @@ -1,5 +1,8 @@ package io.quarkus.kubernetes.config.deployment; +import java.util.Arrays; +import java.util.Collections; + import org.jboss.logmanager.Level; import io.quarkus.deployment.annotations.BuildProducer; @@ -9,8 +12,11 @@ import io.quarkus.deployment.builditem.LogCategoryBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationSourceValueBuildItem; import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig; +import io.quarkus.kubernetes.client.runtime.KubernetesConfigBuildTimeConfig; import io.quarkus.kubernetes.client.runtime.KubernetesConfigRecorder; import io.quarkus.kubernetes.client.runtime.KubernetesConfigSourceConfig; +import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; +import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; public class KubernetesConfigProcessor { @@ -22,6 +28,25 @@ public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecord recorder.configSources(config, clientConfig)); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void handleAccessToSecrets(KubernetesConfigSourceConfig config, + KubernetesConfigBuildTimeConfig buildTimeConfig, + BuildProducer roleProducer, + BuildProducer roleBindingProducer, + KubernetesConfigRecorder recorder) { + if (buildTimeConfig.secretsEnabled) { + roleProducer.produce(new KubernetesRoleBuildItem("view-secrets", Collections.singletonList( + new KubernetesRoleBuildItem.PolicyRule( + Collections.singletonList(""), + Collections.singletonList("secrets"), + Arrays.asList("get", "list", "watch"))))); + roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view-secrets", false)); + } + + recorder.warnAboutSecrets(config, buildTimeConfig); + } + // done in order to ensure that http logs aren't shown by default which happens because of the interplay between // not yet setup logging (as the bootstrap config runs before logging is setup) and the configuration // of the okhttp3.logging.HttpLoggingInterceptor by io.fabric8.kubernetes.client.utils.HttpClientUtils diff --git a/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigBuildTimeConfig.java b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigBuildTimeConfig.java new file mode 100644 index 0000000000000..c854100bb3247 --- /dev/null +++ b/extensions/kubernetes-config/runtime/src/main/java/io/quarkus/kubernetes/client/runtime/KubernetesConfigBuildTimeConfig.java @@ -0,0 +1,15 @@ +package io.quarkus.kubernetes.client.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "kubernetes-config", phase = ConfigPhase.BUILD_TIME) +public class KubernetesConfigBuildTimeConfig { + /** + * Whether or not configuration can be read from secrets. + * If set to {@code true}, Kubernetes resources allowing access to secrets (role and role binding) will be generated. + */ + @ConfigItem(name = "secrets.enabled", defaultValue = "false") + public boolean secretsEnabled; +} diff --git a/extensions/kubernetes-config/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 index 648d5c841670f..f311d6a90a02d 100644 --- a/extensions/kubernetes-config/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 @@ -26,6 +26,17 @@ public RuntimeValue configSources(KubernetesConfigSourceCo KubernetesClientUtils.createClient(clientConfig))); } + public void warnAboutSecrets(KubernetesConfigSourceConfig config, KubernetesConfigBuildTimeConfig buildTimeConfig) { + if (config.enabled + && config.secrets.isPresent() + && !config.secrets.get().isEmpty() + && !buildTimeConfig.secretsEnabled) { + log.warn("Configuration is read from Secrets " + config.secrets.get() + + ", but quarkus.kubernetes-config.secrets.enabled is false." + + " Check if your application's service account has enough permissions to read secrets."); + } + } + private RuntimeValue emptyRuntimeValue() { return new RuntimeValue<>(new EmptyConfigSourceProvider()); } diff --git a/extensions/kubernetes-config/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 index c56d0ae5d79c8..b5f906a39f920 100644 --- a/extensions/kubernetes-config/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 @@ -29,14 +29,15 @@ public class KubernetesConfigSourceConfig { public Optional> configMaps; /** - * Secrets to look for in the namespace that the Kubernetes Client has been configured for + * Secrets to look for in the namespace that the Kubernetes Client has been configured for. + * If you use this, you probably want to enable {@code quarkus.kubernetes-config.secrets.enabled}. */ @ConfigItem public Optional> secrets; /** - * Namespace to look for config maps and secrets. If this is not specified, then the namespace configured in the kubectl config context - * is used. If the value is specified and the namespace doesn't exist, the application will fail to start. + * Namespace to look for config maps and secrets. If this is not specified, then the namespace configured in the kubectl + * config context is used. If the value is specified and the namespace doesn't exist, the application will fail to start. */ @ConfigItem public Optional namespace; diff --git a/extensions/kubernetes-config/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 index a384c2bcb8533..c780e2976544a 100644 --- a/extensions/kubernetes-config/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 @@ -42,7 +42,6 @@ public Iterable getConfigSources(ClassLoader forClassLoader) { result.addAll(getConfigMapConfigSources(config.configMaps.get())); } if (config.secrets.isPresent()) { - // TODO generate a role with permissions to read secrets, and a role binding for that role result.addAll(getSecretConfigSources(config.secrets.get())); } return result; @@ -119,7 +118,8 @@ private void logMissingOrFail(String name, String namespace, String type, boolea String message = type + " '" + name + "' not found"; if (namespace == null) { message = message - + ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the " + type; + + ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the " + + type; } else { message = message + " in namespace '" + namespace + "'"; } diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java new file mode 100644 index 0000000000000..6075a1204d7f2 --- /dev/null +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBindingBuildItem.java @@ -0,0 +1,49 @@ +package io.quarkus.kubernetes.spi; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Produce this build item to request the Kubernetes extension to generate + * a Kubernetes {@code RoleBinding} resource. The configuration here is limited; + * in particular, you can't specify subjects of the role binding. The role will always + * be bound to the application's service account. + *

+ * Note that this can't be used to generate a {@code ClusterRoleBinding}. + */ +public final class KubernetesRoleBindingBuildItem extends MultiBuildItem { + /** + * Name of the generated {@code RoleBinding} resource. + * Can be {@code null}, in which case the resource name is autogenerated. + */ + private final String name; + /** + * Name of the bound role. + */ + private final String role; + /** + * If {@code true}, the binding refers to a {@code ClusterRole}, otherwise to a namespaced {@code Role}. + */ + private final boolean clusterWide; + + public KubernetesRoleBindingBuildItem(String role, boolean clusterWide) { + this(null, role, clusterWide); + } + + public KubernetesRoleBindingBuildItem(String name, String role, boolean clusterWide) { + this.name = name; + this.role = role; + this.clusterWide = clusterWide; + } + + public String getName() { + return this.name; + } + + public String getRole() { + return this.role; + } + + public boolean isClusterWide() { + return clusterWide; + } +} diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java index 95d0f48532c87..52a7501837a36 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesRoleBuildItem.java @@ -1,16 +1,79 @@ package io.quarkus.kubernetes.spi; +import java.util.List; + import io.quarkus.builder.item.MultiBuildItem; +/** + * Produce this build item to request the Kubernetes extension to generate + * a Kubernetes {@code Role} resource. + *

+ * Note that this can't be used to generate a {@code ClusterRole}. + */ public final class KubernetesRoleBuildItem extends MultiBuildItem { + /** + * Name of the generated {@code Role} resource. + */ + private final String name; + /** + * The {@code PolicyRule} resources for this {@code Role}. + */ + private final List rules; + + public KubernetesRoleBuildItem(String name, List rules) { + this.name = name; + this.rules = rules; + } - private final String role; + public String getName() { + return name; + } - public KubernetesRoleBuildItem(String role) { - this.role = role; + public List getRules() { + return rules; } - public String getRole() { - return this.role; + /** + * Corresponds directly to the Kubernetes {@code PolicyRule} resource. + */ + public static final class PolicyRule { + private final List apiGroups; + private final List nonResourceURLs; + private final List resourceNames; + private final List resources; + private final List verbs; + + public PolicyRule(List apiGroups, List resources, List verbs) { + this(apiGroups, null, null, resources, verbs); + } + + public PolicyRule(List apiGroups, List nonResourceURLs, List resourceNames, + List resources, List verbs) { + this.apiGroups = apiGroups; + this.nonResourceURLs = nonResourceURLs; + this.resourceNames = resourceNames; + this.resources = resources; + this.verbs = verbs; + } + + public List getApiGroups() { + return apiGroups; + } + + public List getNonResourceURLs() { + return nonResourceURLs; + } + + public List getResourceNames() { + return resourceNames; + } + + public List getResources() { + return resources; + } + + public List getVerbs() { + return verbs; + } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java new file mode 100644 index 0000000000000..f173102360c78 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java @@ -0,0 +1,43 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.stream.Collectors; + +import io.dekorate.deps.kubernetes.api.model.KubernetesListBuilder; +import io.dekorate.deps.kubernetes.api.model.ObjectMeta; +import io.dekorate.deps.kubernetes.api.model.rbac.PolicyRuleBuilder; +import io.dekorate.deps.kubernetes.api.model.rbac.RoleBuilder; +import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; + +class AddRoleResourceDecorator extends ResourceProvidingDecorator { + private final KubernetesRoleBuildItem spec; + + public AddRoleResourceDecorator(KubernetesRoleBuildItem buildItem) { + this.spec = buildItem; + } + + public void visit(KubernetesListBuilder list) { + ObjectMeta meta = getMandatoryDeploymentMetadata(list); + + if (contains(list, "rbac.authorization.k8s.io/v1", "Role", spec.getName())) { + return; + } + + list.addToItems(new RoleBuilder() + .withNewMetadata() + .withName(spec.getName()) + .withLabels(meta.getLabels()) + .endMetadata() + .withRules( + spec.getRules() + .stream() + .map(it -> new PolicyRuleBuilder() + .withApiGroups(it.getApiGroups()) + .withNonResourceURLs(it.getNonResourceURLs()) + .withResourceNames(it.getResourceNames()) + .withResources(it.getResources()) + .withVerbs(it.getVerbs()) + .build()) + .collect(Collectors.toList()))); + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java index 93078292dee2c..78b76491abdba 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java @@ -133,6 +133,7 @@ import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesLabelBuildItem; import io.quarkus.kubernetes.spi.KubernetesPortBuildItem; +import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem; import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem; import io.quarkus.runtime.LaunchMode; @@ -271,6 +272,7 @@ public void build(ApplicationInfoBuildItem applicationInfo, List kubernetesLabels, List kubernetesEnvs, List kubernetesRoles, + List kubernetesRoleBindings, List kubernetesPorts, EnabledKubernetesDeploymentTargetsBuildItem kubernetesDeploymentTargets, Optional baseImage, @@ -381,6 +383,7 @@ public void visit(S2iBuildConfigFluent s2i) { kubernetesLabels, kubernetesEnvs, kubernetesRoles, + kubernetesRoleBindings, kubernetesPorts, baseImage, containerImage, @@ -669,6 +672,7 @@ private void applyBuildItems(Session session, List kubernetesLabels, List kubernetesEnvs, List kubernetesRoles, + List kubernetesRoleBindings, List kubernetesPorts, Optional baseImage, Optional containerImage, @@ -742,11 +746,16 @@ private void applyBuildItems(Session session, .forEach(p -> session.configurators().add(new AddPort(p))); //Handle RBAC + // TODO why this condition? if (!kubernetesPorts.isEmpty()) { session.resources().decorate(new ApplyServiceAccountNamedDecorator()); session.resources().decorate(new AddServiceAccountResourceDecorator()); - kubernetesRoles - .forEach(r -> session.resources().decorate(new AddRoleBindingResourceDecorator(r.getRole()))); + kubernetesRoles.forEach(r -> session.resources().decorate(new AddRoleResourceDecorator(r))); + kubernetesRoleBindings.forEach(rb -> session.resources().decorate( + new AddRoleBindingResourceDecorator(rb.getName(), null, rb.getRole(), + rb.isClusterWide() + ? AddRoleBindingResourceDecorator.RoleKind.ClusterRole + : AddRoleBindingResourceDecorator.RoleKind.Role))); } handleServices(session, kubernetesConfig, openshiftConfig, knativeConfig, kubernetesName, openshiftName, knativeName); 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 10731a4f2e49d..f193d356137d5 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 @@ -45,6 +45,15 @@ public void configureMockServer(KubernetesMockServer mockServer) { .addToData("application.properties", encodeValue("secret.prop3=val3")) .addToData("application.yaml", encodeValue("secret:\n prop4: val4")).build()) .once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/demo/secrets/s1") + .andReturn(200, secretBuilder("s1") + .addToData("dummysecret", encodeValue("dummysecretFromDemo")) + .addToData("secret.prop1", encodeValue("val1FromDemo")) + .addToData("secret.prop2", encodeValue("val2FromDemo")) + .addToData("application.properties", encodeValue("secret.prop3=val3FromDemo")) + .addToData("application.yaml", encodeValue("secret:\n prop4: val4FromDemo")).build()) + .once(); } private ConfigMapBuilder configMapBuilder(String name) { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java index 7eaec8fd683de..dc814334c40b0 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java @@ -1,7 +1,5 @@ package io.quarkus.it.kubernetes.client; -import static io.quarkus.it.kubernetes.client.ConfigMapPropertiesTest.assertProperty; - import java.util.HashMap; import java.util.Map; @@ -19,12 +17,18 @@ public class NamespacedConfigMapPropertiesTest { @Test public void testPropertiesReadFromConfigMap() { - assertProperty("dummy", "dummyFromDemo"); - assertProperty("someProp1", "val1FromDemo"); - assertProperty("someProp2", "val2FromDemo"); - assertProperty("someProp3", "val3FromDemo"); - assertProperty("someProp4", "val4FromDemo"); - assertProperty("someProp5", "val5FromDemo"); + ConfigMapPropertiesTest.assertProperty("dummy", "dummyFromDemo"); + ConfigMapPropertiesTest.assertProperty("someProp1", "val1FromDemo"); + ConfigMapPropertiesTest.assertProperty("someProp2", "val2FromDemo"); + ConfigMapPropertiesTest.assertProperty("someProp3", "val3FromDemo"); + ConfigMapPropertiesTest.assertProperty("someProp4", "val4FromDemo"); + ConfigMapPropertiesTest.assertProperty("someProp5", "val5FromDemo"); + + SecretPropertiesTest.assertProperty("dummysecret", "dummysecretFromDemo"); + SecretPropertiesTest.assertProperty("secretProp1", "val1FromDemo"); + SecretPropertiesTest.assertProperty("secretProp2", "val2FromDemo"); + SecretPropertiesTest.assertProperty("secretProp3", "val3FromDemo"); + SecretPropertiesTest.assertProperty("secretProp4", "val4FromDemo"); } public static class MyProfile implements QuarkusTestProfile { 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 index 1ec04e06800b3..3b573dc4c10cc 100644 --- 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 @@ -21,7 +21,7 @@ public void testPropertiesReadFromConfigMap() { assertProperty("secretProp4", "val4"); } - private void assertProperty(String propertyName, String expectedValue) { + public static void assertProperty(String propertyName, String expectedValue) { given() .when().get("/secretProperties/" + propertyName) .then() diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java new file mode 100644 index 0000000000000..ecfd7e6a5b6f5 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesConfigWithSecretsTest.java @@ -0,0 +1,100 @@ +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.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.rbac.PolicyRule; +import io.fabric8.kubernetes.api.model.rbac.Role; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; +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 KubernetesConfigWithSecretsTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("kubernetes-config-with-secrets") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("kubernetes-config-with-secrets.properties") + .setForcedDependencies(Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-kubernetes-config", 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("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil.deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + + assertThat(kubernetesList).filteredOn(h -> "Role".equals(h.getKind())).hasSize(1); + + assertThat(kubernetesList).anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(Role.class, role -> { + assertThat(role.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("view-secrets"); + }); + + assertThat(role.getRules()).hasOnlyOneElementSatisfying(r -> { + assertThat(r).isInstanceOfSatisfying(PolicyRule.class, rule -> { + assertThat(rule.getApiGroups()).containsExactly(""); + assertThat(rule.getResources()).containsExactly("secrets"); + assertThat(rule.getVerbs()).containsExactly("get", "list", "watch"); + }); + }); + }); + }); + + assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).hasSize(2); + + assertThat(kubernetesList).anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { + assertThat(roleBinding.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("kubernetes-config-with-secrets:view-secrets"); + }); + + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("Role"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("view-secrets"); + + assertThat(roleBinding.getSubjects()).hasOnlyOneElementSatisfying(subject -> { + assertThat(subject.getKind()).isEqualTo("ServiceAccount"); + assertThat(subject.getName()).isEqualTo("kubernetes-config-with-secrets"); + }); + }); + }); + + assertThat(kubernetesList).anySatisfy(res -> { + assertThat(res).isInstanceOfSatisfying(RoleBinding.class, roleBinding -> { + assertThat(roleBinding.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("kubernetes-config-with-secrets:view"); + }); + + assertThat(roleBinding.getRoleRef().getKind()).isEqualTo("ClusterRole"); + assertThat(roleBinding.getRoleRef().getName()).isEqualTo("view"); + + assertThat(roleBinding.getSubjects()).hasOnlyOneElementSatisfying(subject -> { + assertThat(subject.getKind()).isEqualTo("ServiceAccount"); + assertThat(subject.getName()).isEqualTo("kubernetes-config-with-secrets"); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets.properties new file mode 100644 index 0000000000000..03d54b3c43582 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-config-with-secrets.properties @@ -0,0 +1,3 @@ +quarkus.kubernetes-config.enabled=true +quarkus.kubernetes-config.secrets.enabled=true +quarkus.kubernetes-config.secrets=my-secret From 571aaef97c885f436bb3ad7ca76ef21eba5e3d31 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 29 Jul 2020 17:52:47 +0200 Subject: [PATCH 3/3] make sure ConfigMap/Secret content isn't accidentally exposed --- .../client/runtime/KubernetesConfigSourceProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/kubernetes-config/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 index c780e2976544a..0633f0d68aa31 100644 --- a/extensions/kubernetes-config/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 @@ -70,7 +70,7 @@ private List getConfigMapConfigSources(List configMapNames result.addAll( configMapConfigSourceUtil.toConfigSources(configMap.getMetadata().getName(), configMap.getData())); if (log.isDebugEnabled()) { - log.debug("Done reading ConfigMap " + configMap); + log.debug("Done reading ConfigMap " + configMap.getMetadata().getName()); } } } @@ -103,7 +103,7 @@ private List getSecretConfigSources(List secretNames) { } else { result.addAll(secretConfigSourceUtil.toConfigSources(secret.getMetadata().getName(), secret.getData())); if (log.isDebugEnabled()) { - log.debug("Done reading Secret " + secret); + log.debug("Done reading Secret " + secret.getMetadata().getName()); } } }