Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: reading namespaced ConfigMaps #9728

Merged
merged 1 commit into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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