Spring Cloud Kubernetes Reload

This commit is contained in:
Nicola Ferraro
2016-10-11 10:31:33 +02:00
committed by Ioannis Canellos
parent 7e521f5c23
commit 41bb31fd4c
16 changed files with 729 additions and 4 deletions

View File

@@ -22,6 +22,7 @@ import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
@Order(0)
@@ -35,7 +36,7 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator {
}
@Override
public PropertySource<?> locate(Environment environment) {
public MapPropertySource locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = env.getProperty(Constants.SPRING_APPLICATION_NAME, Constants.FALLBACK_APPLICATION_NAME);

View File

@@ -22,6 +22,7 @@ import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
@Order(1)
@@ -35,7 +36,7 @@ public class SecretsPropertySourceLocator implements PropertySourceLocator {
}
@Override
public PropertySource<?> locate(Environment environment) {
public MapPropertySource locate(Environment environment) {
return environment instanceof ConfigurableEnvironment
? new SecretsPropertySource(client, environment, properties)
: null;

View File

@@ -0,0 +1,137 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.annotation.PreDestroy;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.spring.cloud.kubernetes.config.ConfigMapPropertySourceLocator;
import io.fabric8.spring.cloud.kubernetes.config.SecretsPropertySourceLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;
/**
* Definition of beans needed for the automatic reload of configuration.
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.kubernetes.enabled", matchIfMissing = true)
@EnableConfigurationProperties(ConfigReloadProperties.class)
public class ConfigReloadAutoConfiguration {
/**
* Configuration reload must be enabled explicitly.
*/
@ConditionalOnProperty(value = "spring.cloud.kubernetes.reload.enabled")
@ConditionalOnClass({RestartEndpoint.class, ContextRefresher.class})
@EnableScheduling
@EnableAsync
protected static class ConfigReloadAutoConfigurationBeans {
@Autowired
private AbstractEnvironment environment;
@Autowired
private KubernetesClient kubernetesClient;
@Autowired
private ConfigMapPropertySourceLocator configMapPropertySourceLocator;
@Autowired
private SecretsPropertySourceLocator secretsPropertySourceLocator;
/**
* Provides a bean that listen to configuration changes and fire a reload.
*/
@Bean
@ConditionalOnMissingBean
public ConfigurationChangeDetector propertyChangeWatcher(ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy, EventWatcher eventWatcher) {
switch (properties.getMode()) {
case POLLING:
return new PollingConfigurationChangeDetector(environment, properties, kubernetesClient, strategy, configMapPropertySourceLocator, secretsPropertySourceLocator);
case EVENT:
return new EventBasedConfigurationChangeDetector(environment, properties, kubernetesClient, strategy, configMapPropertySourceLocator, secretsPropertySourceLocator, eventWatcher);
}
throw new IllegalStateException("Unsupported configuration reload mode: " + properties.getMode());
}
/**
* Provides the action to execute when the configuration changes.
*/
@Bean
@ConditionalOnMissingBean
public ConfigurationUpdateStrategy configurationUpdateStrategy(ConfigReloadProperties properties, ConfigurableApplicationContext ctx, RestartEndpoint restarter, ContextRefresher refresher) {
switch (properties.getStrategy()) {
case RESTART_CONTEXT:
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), restarter::restart);
case REFRESH:
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), refresher::refresh);
case SHUTDOWN:
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), ctx::close);
}
throw new IllegalStateException("Unsupported configuration update strategy: " + properties.getStrategy());
}
/**
* Manages watches asynchronously and clean them up on context close.
*/
@Component
public static class DefaultEventWatcher implements EventWatcher {
private Logger log = LoggerFactory.getLogger(getClass());
private KubernetesClient kubernetesClient;
private Map<String, Watch> watches;
@Autowired
public DefaultEventWatcher(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
this.watches = new ConcurrentHashMap<>();
}
@Async
public void addWatch(String name, Function<KubernetesClient, Watch> watch) {
if (watches.containsKey(name)) {
throw new IllegalArgumentException("Watch already present: " + name);
}
watches.put(name, watch.apply(kubernetesClient));
log.info("Added new Kubernetes watch: {}", name);
}
@PreDestroy
public void unwatch() {
if (this.watches != null) {
for (Watch watch : this.watches.values()) {
try {
watch.close();
} catch (Exception e) {
log.error("Error while closing the watch connection", e);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* General configuration for the configuration reload.
*/
@ConfigurationProperties(prefix = "spring.cloud.kubernetes.reload")
public class ConfigReloadProperties {
/**
* Enables the Kubernetes configuration reload on change.
*/
private boolean enabled = false;
/**
* Enables monitoring on config maps to detect changes.
*/
private boolean monitoringConfigMaps = true;
/**
* Enables monitoring on secrets to detect changes.
*/
private boolean monitoringSecrets = false;
/**
* Sets the reload strategy for Kubernetes configuration reload on change.
*/
private ReloadStrategy strategy = ReloadStrategy.REFRESH;
/**
* Sets the detection mode for Kubernetes configuration reload.
*/
private ReloadDetectionMode mode = ReloadDetectionMode.EVENT;
/**
* Sets the polling period in milliseconds to use when the detection mode is POLLING.
*/
private Long period = 15000L;
public ConfigReloadProperties() {
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isMonitoringConfigMaps() {
return monitoringConfigMaps;
}
public void setMonitoringConfigMaps(boolean monitoringConfigMaps) {
this.monitoringConfigMaps = monitoringConfigMaps;
}
public boolean isMonitoringSecrets() {
return monitoringSecrets;
}
public void setMonitoringSecrets(boolean monitoringSecrets) {
this.monitoringSecrets = monitoringSecrets;
}
public ReloadStrategy getStrategy() {
return strategy;
}
public void setStrategy(ReloadStrategy strategy) {
this.strategy = strategy;
}
public ReloadDetectionMode getMode() {
return mode;
}
public Long getPeriod() {
return period;
}
public void setPeriod(Long period) {
this.period = period;
}
public void setMode(ReloadDetectionMode mode) {
this.mode = mode;
}
public enum ReloadStrategy {
/**
* Fire a refresh of beans annotated with @ConfigurationProperties or @RefreshScope.
*/
REFRESH,
/**
* Restarts the Spring ApplicationContext to apply the new configuration.
*/
RESTART_CONTEXT,
/**
* Shuts down the Spring ApplicationContext to activate a restart of the container.
* Make sure that the lifecycle of all non-daemon threads is bound to the ApplicationContext and that
* a replication controller or replica set is configured to restart the pod.
*/
SHUTDOWN
}
public enum ReloadDetectionMode {
/**
* Enables a polling task that retrieves periodically all external properties and
* fire a reload when they change.
*/
POLLING,
/**
* Listens to Kubernetes events and checks if a reload is needed when configmaps or secrets change.
*/
EVENT
}
}

View File

@@ -0,0 +1,109 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.annotation.PreDestroy;
import io.fabric8.kubernetes.client.Client;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
/**
* This is the superclass of all beans that can listen to changes in the configuration and fire a reload.
*/
public abstract class ConfigurationChangeDetector {
protected Logger log = LoggerFactory.getLogger(getClass());
protected ConfigurableEnvironment environment;
protected ConfigReloadProperties properties;
protected KubernetesClient kubernetesClient;
protected ConfigurationUpdateStrategy strategy;
public ConfigurationChangeDetector(ConfigurableEnvironment environment, ConfigReloadProperties properties, KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy) {
this.environment = environment;
this.properties = properties;
this.kubernetesClient = kubernetesClient;
this.strategy = strategy;
}
@PreDestroy
public void shutdown() {
// Ensure the kubernetes client is cleaned up from spare threads when shutting down
if (kubernetesClient instanceof Client) {
((Client) kubernetesClient).close();
}
}
public void reloadProperties() {
log.info("Reloading using strategy: " + strategy.getName());
strategy.reload();
}
/**
* Determines if two property sources are different.
*/
protected boolean changed(MapPropertySource mp1, MapPropertySource mp2) {
if (mp1 == mp2) return false;
if (mp1 == null && mp2 != null || mp1 != null && mp2 == null) return true;
Map<String, Object> s1 = mp1.getSource();
Map<String, Object> s2 = mp2.getSource();
return s1 == null ? s2 != null : !s1.equals(s2);
}
/**
* Finds one registered property source of the given type, logging a warning if
* multiple property sources of that type are available.
*/
protected <S extends PropertySource<?>> S findPropertySource(Class<S> sourceClass) {
List<S> sources = findPropertySources(sourceClass);
if (sources.size() == 0) {
return null;
}
if (sources.size() > 1) {
log.warn("Found more than one property source of type " + sourceClass);
}
return sources.get(0);
}
/**
* Finds all registered property sources of the given type.
*/
protected <S extends PropertySource<?>> List<S> findPropertySources(Class<S> sourceClass) {
List<S> managedSources = new LinkedList<>();
LinkedList<PropertySource<?>> sources = toLinkedList(environment.getPropertySources());
while (!sources.isEmpty()) {
PropertySource<?> source = sources.pop();
if (source instanceof CompositePropertySource) {
CompositePropertySource comp = (CompositePropertySource) source;
sources.addAll(comp.getPropertySources());
} else if (sourceClass.isInstance(source)) {
managedSources.add(sourceClass.cast(source));
}
}
return managedSources;
}
private <E> LinkedList<E> toLinkedList(Iterable<E> it) {
LinkedList<E> list = new LinkedList<E>();
for (E e : it) {
list.add(e);
}
return list;
}
}

View File

@@ -0,0 +1,36 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import java.util.Objects;
/**
* This is the superclass of all named strategies that can be fired when the configuration changes.
*/
public class ConfigurationUpdateStrategy {
private String name;
private Runnable reloadProcedure;
public ConfigurationUpdateStrategy(String name, Runnable reloadProcedure) {
Objects.requireNonNull(name, "name cannot be null");
Objects.requireNonNull(reloadProcedure, "reloadProcedure cannot be null");
this.name = name;
this.reloadProcedure = reloadProcedure;
}
public String getName() {
return name;
}
public void reload() {
this.reloadProcedure.run();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ConfigurationUpdateStrategy{");
sb.append("name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,100 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import javax.annotation.PostConstruct;
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.Watcher;
import io.fabric8.spring.cloud.kubernetes.config.ConfigMapPropertySource;
import io.fabric8.spring.cloud.kubernetes.config.ConfigMapPropertySourceLocator;
import io.fabric8.spring.cloud.kubernetes.config.SecretsPropertySource;
import io.fabric8.spring.cloud.kubernetes.config.SecretsPropertySourceLocator;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.MapPropertySource;
/**
* A change detector that subscribes to changes in secrets and configmaps and fire a reload when something changes.
*/
public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDetector {
private ConfigMapPropertySourceLocator configMapPropertySourceLocator;
private SecretsPropertySourceLocator secretsPropertySourceLocator;
private EventWatcher eventWatcher;
public EventBasedConfigurationChangeDetector(AbstractEnvironment environment,
ConfigReloadProperties properties,
KubernetesClient kubernetesClient,
ConfigurationUpdateStrategy strategy,
ConfigMapPropertySourceLocator configMapPropertySourceLocator,
SecretsPropertySourceLocator secretsPropertySourceLocator,
EventWatcher eventWatcher) {
super(environment, properties, kubernetesClient, strategy);
this.eventWatcher = eventWatcher;
this.configMapPropertySourceLocator = configMapPropertySourceLocator;
this.secretsPropertySourceLocator = secretsPropertySourceLocator;
}
@PostConstruct
public void watch() {
if (properties.isMonitoringConfigMaps()) {
eventWatcher.addWatch("config-maps-watch", k -> k.configMaps()
.watch(new Watcher<ConfigMap>() {
@Override
public void eventReceived(Action action, ConfigMap configMap) {
onEvent(configMap);
}
@Override
public void onClose(KubernetesClientException e) {
}
}));
}
if (properties.isMonitoringSecrets()) {
eventWatcher.addWatch("secrets-watch", k -> k.secrets()
.watch(new Watcher<Secret>() {
@Override
public void eventReceived(Action action, Secret secret) {
onEvent(secret);
}
@Override
public void onClose(KubernetesClientException e) {
}
}));
}
log.info("Kubernetes polling configuration change detector activated");
}
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();
}
}
}
private void onEvent(Secret secret) {
MapPropertySource currentSecretSource = findPropertySource(SecretsPropertySource.class);
if (currentSecretSource != null) {
MapPropertySource newSecretSource = secretsPropertySourceLocator.locate(environment);
if (changed(currentSecretSource, newSecretSource)) {
log.info("Detected change in secrets");
reloadProperties();
}
}
}
}

View File

@@ -0,0 +1,15 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import java.util.function.Function;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Watch;
/**
* Provides a way to start Kubernetes watches and bind their lifecycle to the application context.
*/
public interface EventWatcher {
void addWatch(String name, Function<KubernetesClient, Watch> watch);
}

View File

@@ -0,0 +1,67 @@
package io.fabric8.spring.cloud.kubernetes.reload;
import javax.annotation.PostConstruct;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.spring.cloud.kubernetes.config.ConfigMapPropertySource;
import io.fabric8.spring.cloud.kubernetes.config.ConfigMapPropertySourceLocator;
import io.fabric8.spring.cloud.kubernetes.config.SecretsPropertySource;
import io.fabric8.spring.cloud.kubernetes.config.SecretsPropertySourceLocator;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.scheduling.annotation.Scheduled;
/**
* A change detector that periodically retrieves secrets and configmaps and fire a reload when something changes.
*/
public class PollingConfigurationChangeDetector extends ConfigurationChangeDetector {
private ConfigMapPropertySourceLocator configMapPropertySourceLocator;
private SecretsPropertySourceLocator secretsPropertySourceLocator;
public PollingConfigurationChangeDetector(AbstractEnvironment environment,
ConfigReloadProperties properties,
KubernetesClient kubernetesClient,
ConfigurationUpdateStrategy strategy,
ConfigMapPropertySourceLocator configMapPropertySourceLocator,
SecretsPropertySourceLocator secretsPropertySourceLocator) {
super(environment, properties, kubernetesClient, strategy);
this.configMapPropertySourceLocator = configMapPropertySourceLocator;
this.secretsPropertySourceLocator = secretsPropertySourceLocator;
}
@PostConstruct
public void init() {
log.info("Kubernetes polling configuration change detector activated");
}
@Scheduled(initialDelayString = "${spring.cloud.kubernetes.reload.period:15000}", fixedDelayString = "${spring.cloud.kubernetes.reload.period:15000}")
public void executeCycle() {
boolean changedConfigMap = false;
if (properties.isMonitoringConfigMaps()) {
MapPropertySource currentConfigMapSource = findPropertySource(ConfigMapPropertySource.class);
if (currentConfigMapSource != null) {
MapPropertySource newConfigMapSource = configMapPropertySourceLocator.locate(environment);
changedConfigMap = changed(currentConfigMapSource, newConfigMapSource);
}
}
boolean changedSecrets = false;
if (properties.isMonitoringSecrets()) {
MapPropertySource currentSecretSource = findPropertySource(SecretsPropertySource.class);
if (currentSecretSource != null) {
MapPropertySource newSecretSource = secretsPropertySourceLocator.locate(environment);
changedSecrets = changed(currentSecretSource, newSecretSource);
}
}
if (changedConfigMap || changedSecrets) {
reloadProperties();
}
}
}

View File

@@ -1,5 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.fabric8.spring.cloud.kubernetes.KubernetesAutoConfiguration
io.fabric8.spring.cloud.kubernetes.KubernetesAutoConfiguration,\
io.fabric8.spring.cloud.kubernetes.reload.ConfigReloadAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
io.fabric8.spring.cloud.kubernetes.config.BootstrapConfiguration