Skip to content

Commit

Permalink
feature: reading namespaced ConfigMaps
Browse files Browse the repository at this point in the history
  • Loading branch information
aureamunoz committed Jul 7, 2020
1 parent 754eced commit 1c89c51
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 5 deletions.
4 changes: 2 additions & 2 deletions docs/src/main/asciidoc/kubernetes-config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ The extension understands the following types of ConfigMaps 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).

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).
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).
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).

=== Priority of obtained properties

Expand Down
5 changes: 5 additions & 0 deletions extensions/kubernetes-config/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ public class KubernetesConfigSourceConfig {
@ConfigItem
public Optional<List<String>> configMaps;

/**
* Namespace to look for config maps. 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<String> namespace;

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,17 @@ private List<ConfigSource> getConfigMapConfigSources(List<String> configMapNames
if (log.isDebugEnabled()) {
log.debug("Attempting to read ConfigMap " + configMapName);
}
ConfigMap configMap = client.configMaps().withName(configMapName).get();
ConfigMap configMap;
String namespace;
if (config.namespace.isPresent()) {
namespace = config.namespace.get();
configMap = client.configMaps().inNamespace(namespace).withName(configMapName).get();
} else {
namespace = client.getNamespace();
configMap = client.configMaps().withName(configMapName).get();
}
if (configMap == null) {
logMissingOrFail(configMapName, client.getNamespace(), "ConfigMap", config.failOnMissingConfig);
logMissingOrFail(configMapName, namespace, "ConfigMap", config.failOnMissingConfig);
} else {
result.addAll(
configMapConfigSourceUtil.toConfigSources(configMap.getMetadata().getName(), configMap.getData()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package io.quarkus.kubernetes.client.runtime;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.Optional;

import org.assertj.core.util.Lists;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ConfigMapList;
import io.fabric8.kubernetes.api.model.DoneableConfigMap;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
import io.fabric8.kubernetes.client.dsl.Resource;

public class KubernetesConfigSourceProviderTest {

@Test
public void testEmptyConfigSources() {
KubernetesConfigSourceConfig config = defaultConfig();
KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
assertThat(configSources).isEmpty();
}

@Test
public void testRetrieveNamespacedConfigSources() {
KubernetesConfigSourceConfig config = defaultConfig();
config.namespace = Optional.of("demo");
List<String> configMaps = Lists.list("cm1");
config.configMaps = Optional.of(configMaps);

ConfigMap configMap = configMapBuilder("cm1")
.addToData("some.key", "someValue").addToData("some.other", "someOtherValue").build();

KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
stubNamespacedConfigMap(kubernetesClient, configMap, "cm1");

KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
assertThat(configSources).isNotEmpty();
ConfigSource next = configSources.iterator().next();
assertThat(next.getProperties()).containsKeys("some.key", "some.other");
}

@Test
public void testNamespacedConfigSourcesAbsents() {
KubernetesConfigSourceConfig config = defaultConfig();
config.namespace = Optional.of("demo");
List<String> configMaps = Lists.list("cm2");
config.configMaps = Optional.of(configMaps);

KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
stubNamespacedConfigMap(kubernetesClient, null, "cm2");

KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
assertThat(configSources).isEmpty();
}

@Test
public void testRetrieveConfigSources() {
KubernetesConfigSourceConfig config = defaultConfig();
List<String> configMaps = Lists.list("cm1");
config.configMaps = Optional.of(configMaps);

ConfigMap configMap = configMapBuilder("cm1")
.addToData("some.key", "someValue").addToData("some.other", "someOtherValue").build();

KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
stubConfigMap(kubernetesClient, configMap, "cm1");

KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
assertThat(configSources).isNotEmpty();
ConfigSource next = configSources.iterator().next();
assertThat(next.getProperties()).containsKeys("some.key", "some.other");
}

@Test
public void testConfigSourcesAbsent() {
KubernetesConfigSourceConfig config = defaultConfig();
List<String> configMaps = Lists.list("cm2");
config.configMaps = Optional.of(configMaps);

KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
stubConfigMap(kubernetesClient, null, "cm2");

KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
assertThat(configSources).isEmpty();
}

@Test
public void testConfigSourcesAbsentFailOnMissing() {
try {
KubernetesConfigSourceConfig config = defaultConfig();
config.namespace = Optional.of("demo");
List<String> configMaps = Lists.list("cm2");
config.configMaps = Optional.of(configMaps);
config.failOnMissingConfig = true;

KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class);
stubNamespacedConfigMap(kubernetesClient, null, "cm2");

KubernetesConfigSourceProvider kcsp = new KubernetesConfigSourceProvider(config, kubernetesClient);
Iterable<ConfigSource> configSources = kcsp.getConfigSources(null);
fail("an exception should be raised");
} catch (RuntimeException expected) {
}
}

private void stubNamespacedConfigMap(KubernetesClient kubernetesClient, ConfigMap configMap, String configMapName) {
MixedOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>> mixedOperation = (MixedOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>>) mock(
MixedOperation.class);
when(kubernetesClient.configMaps()).thenReturn(mixedOperation);
Resource<ConfigMap, DoneableConfigMap> resource = (Resource<ConfigMap, DoneableConfigMap>) mock(Resource.class);
NonNamespaceOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>> nsClient = (NonNamespaceOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>>) mock(
NonNamespaceOperation.class);
when(mixedOperation.inNamespace("demo")).thenReturn(nsClient);
when(nsClient.withName(configMapName)).thenReturn(resource);

when(resource.get()).thenReturn(configMap);
Config kubernetesConfig = mock(Config.class);
when(kubernetesClient.getConfiguration()).thenReturn(kubernetesConfig);
when(kubernetesConfig.getMasterUrl()).thenReturn("url");

}

private void stubConfigMap(KubernetesClient kubernetesClient, ConfigMap configMap, String configMapName) {
MixedOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>> mixedOperation = (MixedOperation<ConfigMap, ConfigMapList, DoneableConfigMap, Resource<ConfigMap, DoneableConfigMap>>) mock(
MixedOperation.class);
when(kubernetesClient.configMaps()).thenReturn(mixedOperation);
Resource<ConfigMap, DoneableConfigMap> resource = (Resource<ConfigMap, DoneableConfigMap>) mock(Resource.class);
when(mixedOperation.withName(configMapName)).thenReturn(resource);
when(resource.get()).thenReturn(configMap);
Config kubernetesConfig = mock(Config.class);
when(kubernetesClient.getConfiguration()).thenReturn(kubernetesConfig);
when(kubernetesConfig.getMasterUrl()).thenReturn("url");

}

private KubernetesConfigSourceConfig defaultConfig() {
KubernetesConfigSourceConfig config = new KubernetesConfigSourceConfig();
config.namespace = Optional.empty();
config.configMaps = Optional.empty();
return config;
}

private ConfigMapBuilder configMapBuilder(String name) {
return new ConfigMapBuilder().withNewMetadata()
.withName(name).endMetadata();
}

}
10 changes: 10 additions & 0 deletions integration-tests/kubernetes-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,21 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-kubernetes-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class ConfigMapProperties {
@ConfigProperty(name = "some.prop4")
String someProp4;

@ConfigProperty(name = "some.prop5")
String someProp5;

@GET
@Path("/dummy")
public String dummy() {
Expand Down Expand Up @@ -52,4 +55,10 @@ public String someProp3() {
public String someProp4() {
return someProp4;
}

@GET
@Path("/someProp5")
public String someProp5() {
return someProp5;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.it.kubernetes.client;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;

import org.assertj.core.api.Assertions;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusProdModeTest;
import io.quarkus.test.common.QuarkusTestResource;

@QuarkusTestResource(CustomKubernetesMockServerTestResource.class)
public class AbsentConfigMapPropertiesTest {

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(ConfigMapProperties.class))
.setApplicationName("k8s-configMaps")
.withConfigurationResource("application-demo.properties")
.setRun(true)
.setExpectExit(true)
.setApplicationVersion("0.1-SNAPSHOT");

@Test
public void startUpShouldFail() throws IOException {
Assertions.assertThat(config.getStartupConsoleOutput())
.contains("ConfigMap 'cmap4' not found in namespace 'demo'").contains("RuntimeException");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ public void testPropertiesReadFromConfigMap() {
assertProperty("someProp2", "val2");
assertProperty("someProp3", "val3");
assertProperty("someProp4", "val4");
assertProperty("someProp5", "val5");
}

private void assertProperty(String propertyName, String expectedValue) {
public static void assertProperty(String propertyName, String expectedValue) {
given()
.when().get("/configMapProperties/" + propertyName)
.then()
.statusCode(200)
.body(is(expectedValue));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public void configureMockServer(KubernetesMockServer mockServer) {
.addToData("dummy", "dummy")
.addToData("some.prop1", "val1")
.addToData("some.prop2", "val2")
.addToData("some.prop5", "val5")
.addToData("application.properties", "some.prop3=val3")
.addToData("application.yaml", "some:\n prop4: val4").build())
.once();
Expand All @@ -22,6 +23,16 @@ 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/demo/configmaps/cmap3")
.andReturn(200, configMapBuilder("cmap3")
.addToData("dummy", "dummyFromDemo")
.addToData("some.prop1", "val1FromDemo")
.addToData("some.prop2", "val2FromDemo")
.addToData("some.prop5", "val5FromDemo")
.addToData("application.properties", "some.prop3=val3FromDemo")
.addToData("application.yaml", "some:\n prop4: val4FromDemo").build())
.once();
}

private ConfigMapBuilder configMapBuilder(String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.it.kubernetes.client;

import static io.quarkus.it.kubernetes.client.ConfigMapPropertiesTest.assertProperty;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;

@QuarkusTestResource(CustomKubernetesMockServerTestResource.class)
@TestProfile(NamespacedConfigMapPropertiesTest.MyProfile.class)
@QuarkusTest
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");
}

public static class MyProfile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
Map<String, String> conf = new HashMap<>();
conf.put("quarkus.kubernetes-config.enabled", "true");
conf.put("quarkus.kubernetes-config.config-maps", "cmap3");
conf.put("quarkus.kubernetes-config.namespace", "demo");
conf.put("quarkus.kubernetes-config.secrets", "s1");
return conf;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
quarkus.kubernetes-config.enabled=true
quarkus.kubernetes-config.config-maps=cmap4
quarkus.kubernetes-config.namespace=demo
quarkus.kubernetes-config.secrets=s1

0 comments on commit 1c89c51

Please sign in to comment.