Allow the use of multiple configmaps as sources

This commit is contained in:
Georgios Andrianakis
2018-05-21 18:44:01 +03:00
committed by Ioannis Canellos
parent 5166090b77
commit a30b2d85dc
18 changed files with 585 additions and 45 deletions

View File

@@ -88,11 +88,37 @@ The [Spring Cloud Kubernetes Config](./spring-cloud-kubernetes-config) project m
during application bootstrapping and triggers hot reloading of beans or Spring context when changes are detected on
observed `ConfigMap`s.
`ConfigMapPropertySource` will search for a Kubernetes `ConfigMap` which `metadata.name` is either the name of
The default behavior is to create a `ConfigMapPropertySource` based on a Kubernetes `ConfigMap` which has `metadata.name` of either the name of
your Spring application (as defined by its `spring.application.name` property) or a custom name defined within the
`bootstrap.properties` file under the following key `spring.cloud.kubernetes.config.name`.
If such a `ConfigMap` is found, it will be processed as follows:
However, more advanced configuration are possible where multiple ConfigMaps can be used
This is made possible by the `spring.cloud.kubernetes.config.sources` list.
For example one could define the following ConfigMaps
```yaml
spring:
application:
name: cloud-k8s-app
cloud:
kubernetes:
config:
name: default-name
namespace: default-namespace
sources:
# Spring Cloud Kubernetes will lookup a ConfigMap named c1 in namespace default-namespace
- name: c1
# Spring Cloud Kubernetes will lookup a ConfigMap named default-name in whatever namespace n2
- namespace: n2
# Spring Cloud Kubernetes will lookup a ConfigMap named c3 in namespace n3
- namespace: n3
name: c3
```
In the example above, it `spring.cloud.kubernetes.config.namespace` had not been set,
then the ConfigMap named `c1` would be looked up in the namespace that the application runs
Any matching `ConfigMap` that is found, will be processed as follows:
- apply individual configuration properties.
- apply as `yaml` the content of any property named `application.yaml`

View File

@@ -17,10 +17,12 @@
package org.springframework.cloud.kubernetes.config;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
@ConfigurationProperties("spring.cloud.kubernetes.config")
public class ConfigMapConfigProperties extends AbstractConfigProperties {
@@ -29,6 +31,7 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
private boolean enableApi = true;
private List<String> paths = new LinkedList<>();
private List<Source> sources = new LinkedList<>();
public boolean isEnableApi() {
return enableApi;
@@ -46,8 +49,101 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
return paths;
}
public List<Source> getSources() {
return sources;
}
public void setSources(List<Source> sources) {
this.sources = sources;
}
/**
* @return A list of Source to use
* If the user has not specified any Source properties, then a single Source
* is constructed based on the supplied name and namespace
*
* These are the actual name/namespace pairs that are used to create a ConfigMapPropertySource
*/
public List<NormalizedSource> determineSources() {
if (sources.isEmpty()) {
return new ArrayList<NormalizedSource>() {{
add(new NormalizedSource(name, namespace));
}};
}
return sources.stream().map(s -> s.normalize(name, namespace)).collect(Collectors.toList());
}
@Override
public String getConfigurationTarget() {
return TARGET;
}
public static class Source {
/**
* The name of the ConfigMap
*/
private String name;
/**
* The namespace where the ConfigMap is found
*/
private String namespace;
public Source() {
}
public Source(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public boolean isEmpty() {
return StringUtils.isEmpty(name) && StringUtils.isEmpty(namespace);
}
public NormalizedSource normalize(String defaultName, String defaultNamespace) {
final String normalizedName =
StringUtils.isEmpty(this.name) ? defaultName : this.name;
final String normalizedNamespace =
StringUtils.isEmpty(this.namespace) ? defaultNamespace : this.namespace;
return new NormalizedSource(normalizedName, normalizedNamespace);
}
}
static class NormalizedSource {
private final String name;
private final String namespace;
public NormalizedSource(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public String getName() {
return name;
}
public String getNamespace() {
return namespace;
}
}
}

View File

@@ -17,13 +17,19 @@
package org.springframework.cloud.kubernetes.config;
import static org.springframework.cloud.kubernetes.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.config.ConfigUtils.getApplicationNamespace;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.List;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.kubernetes.config.ConfigMapConfigProperties.NormalizedSource;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.config.ConfigUtils.*;
import org.springframework.core.env.PropertySource;
@Order(0)
public class ConfigMapPropertySourceLocator implements PropertySourceLocator {
@@ -36,13 +42,36 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator {
}
@Override
public MapPropertySource locate(Environment environment) {
public PropertySource locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String name = getApplicationName(environment, properties);
String namespace = getApplicationNamespace(client, env, properties);
return new ConfigMapPropertySource(client, name, namespace, env.getActiveProfiles(), properties);
}
List<ConfigMapConfigProperties.NormalizedSource> sources =
properties.determineSources();
if (sources.size() == 1) {
return getMapPropertySourceForSingleConfigMap(env, sources.get(0));
}
CompositePropertySource composite = new CompositePropertySource("composite-configmap");
sources.forEach(s ->
composite.addFirstPropertySource(getMapPropertySourceForSingleConfigMap(env, s))
);
return composite;
}
return null;
}
private MapPropertySource getMapPropertySourceForSingleConfigMap(
ConfigurableEnvironment environment, NormalizedSource normalizedSource) {
String configurationTarget = properties.getConfigurationTarget();
return new ConfigMapPropertySource(
client,
getApplicationName(environment, normalizedSource.getName(), configurationTarget),
getApplicationNamespace(client, normalizedSource.getNamespace(), configurationTarget),
environment.getActiveProfiles(),
properties
);
}
}

View File

@@ -14,12 +14,13 @@ public class ConfigUtils {
private static final Log LOG = LogFactory.getLog(ConfigUtils.class);
public static <C extends AbstractConfigProperties> String getApplicationName(Environment env, C config) {
String name = config.getName();
public static <C extends AbstractConfigProperties> String getApplicationName(Environment env,
String configName, String configurationTarget) {
String name = configName;
if (StringUtils.isEmpty(name)) {
//TODO: use relaxed binding
if (LOG.isDebugEnabled()) {
LOG.debug(config.getConfigurationTarget() +
LOG.debug(configurationTarget +
" name has not been set, taking it from property/env " +
SPRING_APPLICATION_NAME + " (default=" + FALLBACK_APPLICATION_NAME + ")");
}
@@ -30,11 +31,13 @@ public class ConfigUtils {
return name;
}
public static <C extends AbstractConfigProperties> String getApplicationNamespace(KubernetesClient client, Environment env, C config) {
String namespace = config.getNamespace();
public static <C extends AbstractConfigProperties> String getApplicationNamespace(
KubernetesClient client, String configNamespace, String configurationTarget) {
String namespace = configNamespace;
if (StringUtils.isEmpty(namespace)) {
if (LOG.isDebugEnabled()) {
LOG.debug(config.getConfigurationTarget() + " namespace has not been set, taking it from client (ns="+client.getNamespace()+")");
LOG.debug(
configurationTarget + " namespace has not been set, taking it from client (ns="+client.getNamespace()+")");
}
namespace = client.getNamespace();

View File

@@ -46,15 +46,17 @@ public class SecretsPropertySource extends KubernetesPropertySource {
return new StringBuilder()
.append(PREFIX)
.append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR)
.append(getApplicationName(env,config))
.append(getApplicationName(env, config.getName(), config.getConfigurationTarget()))
.append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR)
.append(getApplicationNamespace(client, env, config))
.append(getApplicationNamespace(client, config.getNamespace(),
config.getConfigurationTarget()))
.toString();
}
private static Map<String, Object> getSourceData(KubernetesClient client, Environment env, SecretsConfigProperties config) {
String name = getApplicationName(env, config);
String namespace = getApplicationNamespace(client, env, config);
String name = getApplicationName(env, config.getName(), config.getConfigurationTarget());
String namespace = getApplicationNamespace(client, config.getNamespace(),
config.getConfigurationTarget());
Map<String, Object> result = new HashMap<>();
if (config.isEnableApi()) {

View File

@@ -16,17 +16,19 @@
*/
package org.springframework.cloud.kubernetes.config.reload;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
@@ -76,6 +78,23 @@ public abstract class ConfigurationChangeDetector {
return s1 == null ? s2 != null : !s1.equals(s2);
}
protected boolean changed(List<? extends MapPropertySource> l1,
List<? extends MapPropertySource> l2) {
if(l1.size() != l2.size()) {
log.debug("The current number of Confimap PropertySources does not match "
+ "the ones loaded from the Kubernetes - No reload will take place");
return false;
}
for(int i=0; i<l1.size(); i++) {
if (changed(l1.get(i), l2.get(i))) {
return true;
}
}
return false;
}
/**
* Finds one registered property source of the given type, logging a warning if
* multiple property sources of that type are available.
@@ -119,4 +138,30 @@ public abstract class ConfigurationChangeDetector {
return list;
}
/**
* Returns a list of MapPropertySource that correspond to the current state of the system
* This only handles the PropertySource objects that are returned
*/
protected List<MapPropertySource> locateMapPropertySources(
PropertySourceLocator propertySourceLocator, Environment environment) {
List<MapPropertySource> result = new ArrayList<>();
PropertySource propertySource= propertySourceLocator.locate(environment);
if(propertySource instanceof MapPropertySource) {
result.add((MapPropertySource) propertySource);
} else if(propertySource instanceof CompositePropertySource) {
result.addAll(((CompositePropertySource) propertySource)
.getPropertySources()
.stream()
.filter(p -> p instanceof MapPropertySource)
.map(p -> (MapPropertySource) p)
.collect(Collectors.toList()));
} else {
log.debug("Found property source that cannot be handled: "
+ propertySource.getClass());
}
return result;
}
}

View File

@@ -16,22 +16,20 @@
*/
package org.springframework.cloud.kubernetes.config.reload;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.cloud.kubernetes.config.ConfigMapPropertySource;
import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.MapPropertySource;
@@ -127,14 +125,14 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe
}
private void onEvent(ConfigMap configMap) {
MapPropertySource currentConfigMapSource = findPropertySource(ConfigMapPropertySource.class);
if (currentConfigMapSource != null) {
MapPropertySource newConfigMapSource = configMapPropertySourceLocator.locate(environment);
if (changed(currentConfigMapSource, newConfigMapSource)) {
log.info("Detected change in config maps");
reloadProperties();
}
}
boolean changed = changed(
locateMapPropertySources(configMapPropertySourceLocator, environment),
findPropertySources(ConfigMapPropertySource.class)
);
if(changed) {
log.info("Detected change in config maps");
reloadProperties();
}
}
private void onEvent(Secret secret) {

View File

@@ -16,14 +16,15 @@
*/
package org.springframework.cloud.kubernetes.config.reload;
import javax.annotation.PostConstruct;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.config.ConfigMapPropertySource;
import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.scheduling.annotation.Scheduled;
@@ -33,6 +34,8 @@ import org.springframework.scheduling.annotation.Scheduled;
*/
public class PollingConfigurationChangeDetector extends ConfigurationChangeDetector {
protected Log log = LogFactory.getLog(getClass());
private ConfigMapPropertySourceLocator configMapPropertySourceLocator;
private SecretsPropertySourceLocator secretsPropertySourceLocator;
@@ -59,10 +62,14 @@ public class PollingConfigurationChangeDetector extends ConfigurationChangeDetec
boolean changedConfigMap = false;
if (properties.isMonitoringConfigMaps()) {
MapPropertySource currentConfigMapSource = findPropertySource(ConfigMapPropertySource.class);
if (currentConfigMapSource != null) {
MapPropertySource newConfigMapSource = configMapPropertySourceLocator.locate(environment);
changedConfigMap = changed(currentConfigMapSource, newConfigMapSource);
List<? extends MapPropertySource> currentConfigMapSources
= findPropertySources(ConfigMapPropertySource.class);
if (!currentConfigMapSources.isEmpty()) {
changedConfigMap = changed(
locateMapPropertySources(configMapPropertySourceLocator, environment),
currentConfigMapSources
);
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2016 to the original authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.springframework.cloud.kubernetes.config;
import static io.restassured.RestAssured.when;
import static org.hamcrest.core.Is.is;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.restassured.RestAssured;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.config.example2.ExampleApp;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author <a href="mailto:cmoullia@redhat.com">Charles Moulliard</a>
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApp.class,
properties = {"spring.cloud.bootstrap.name=multiplecms"})
public class MultipleConfigMapsSpringBootTest {
@ClassRule
public static KubernetesServer server = new KubernetesServer();
private static KubernetesClient mockClient;
@Value("${local.server.port}")
private int port;
@BeforeClass
public static void setUpBeforeClass() {
mockClient = server.getClient();
//Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
createConfigmap(
server,
"s1",
"defnamespace",
new HashMap<String, String>() {{
put("bean.common-message","c1");
put("bean.message1", "m1");
}});
createConfigmap(
server,
"defname",
"s2",
new HashMap<String, String>() {{
put("bean.common-message","c2");
put("bean.message2", "m2");
}});
createConfigmap(
server,
"othername",
"othernamespace",
new HashMap<String, String>() {{
put("bean.common-message","c3");
put("bean.message3", "m3");
}});
}
private static void createConfigmap(KubernetesServer server, String configMapName,
String namespace, Map<String, String> data) {
server
.expect()
.withPath(String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName))
.andReturn(
200,
new ConfigMapBuilder()
.withNewMetadata().withName(configMapName).endMetadata()
.addToData(data)
.build()
)
.always();
}
@Before
public void setUp() {
RestAssured.baseURI = String.format("http://localhost:%d/", port);
}
//the last confimap defined in 'multiplecms.yml' has the highest priority, so
//the common property defined in all configmaps is taken from the last one defined
@Test
public void testCommonMessage() {
assertResponse("/common", "c3");
}
@Test
public void testMessage1() {
assertResponse("/m1", "m1");
}
@Test
public void testMessage2() {
assertResponse("/m2", "m2");
}
@Test
public void testMessage3() {
assertResponse("/m3", "m3");
}
private void assertResponse(String path, String expectedMessage) {
when().get(path)
.then()
.statusCode(200)
.body("message", is(expectedMessage));
}
}

View File

@@ -0,0 +1,60 @@
package org.springframework.cloud.kubernetes.config.example2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableConfigurationProperties(ExampleAppProps.class)
public class ExampleApp {
public static void main(String[] args) {
SpringApplication.run(org.springframework.cloud.kubernetes.config.example.App.class, args);
}
@RestController
public static class Controller {
private final ExampleAppProps exampleAppProps;
public Controller(ExampleAppProps exampleAppProps) {
this.exampleAppProps = exampleAppProps;
}
@GetMapping("/common")
public Response commonMessage() {
return new Response(exampleAppProps.getCommonMessage());
}
@GetMapping("/m1")
public Response message1() {
return new Response(exampleAppProps.getMessage1());
}
@GetMapping("/m2")
public Response message2() {
return new Response(exampleAppProps.getMessage2());
}
@GetMapping("/m3")
public Response message3() {
return new Response(exampleAppProps.getMessage3());
}
}
public static class Response {
private final String message;
public Response(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
}

View File

@@ -0,0 +1,44 @@
package org.springframework.cloud.kubernetes.config.example2;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("bean")
public class ExampleAppProps {
private String commonMessage;
private String message1;
private String message2;
private String message3;
public String getCommonMessage() {
return commonMessage;
}
public void setCommonMessage(String commonMessage) {
this.commonMessage = commonMessage;
}
public String getMessage1() {
return message1;
}
public void setMessage1(String message1) {
this.message1 = message1;
}
public String getMessage2() {
return message2;
}
public void setMessage2(String message2) {
this.message2 = message2;
}
public String getMessage3() {
return message3;
}
public void setMessage3(String message3) {
this.message3 = message3;
}
}

View File

@@ -0,0 +1,16 @@
---
spring:
application:
name: name-in-file
cloud:
kubernetes:
reload:
enabled: false
config:
name: defname
namespace: defnamespace
sources:
- name: s1
- namespace: s2
- name: othername
namespace: othernamespace

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2016 to the original authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.springframework.cloud.kubernetes.examples;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "dummy")
public class DummyConfig {
private String message = "this is a dummy message";
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -24,11 +24,15 @@ import org.springframework.stereotype.Component;
public class MyBean {
@Autowired
private MyConfig config;
private MyConfig myConfig;
@Autowired
private DummyConfig dummyConfig;
@Scheduled(fixedDelay = 5000)
public void hello() {
System.out.println("The message is: " + config.getMessage());
System.out.println("The first message is: " + myConfig.getMessage());
System.out.println("The other message is: " + dummyConfig.getMessage());
}

View File

@@ -0,0 +1,8 @@
management:
endpoint:
restart:
enabled: true
health:
enabled: true
info:
enabled: true

View File

@@ -0,0 +1,13 @@
spring:
application:
name: reload-example
cloud:
kubernetes:
reload:
enabled: true
mode: polling
period: 5000
config:
sources:
- name: other
- name: ${spring.application.name}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml" />
<logger name="org.hibernate.validator" level="info" /> <!-- Validator prints a lot of debug messages during integration tests -->
<logger name="org.springframework.cloud.kubernetes" level="debug" />
</configuration>