diff --git a/example/pom.xml b/example/pom.xml new file mode 100644 index 00000000..b782b635 --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,58 @@ + + + + spring-cloud-kubernetes-project + io.fabric8 + 0.1-SNAPSHOT + + 4.0.0 + + example + + + + + io.fabric8 + spring-cloud-kubernetes-core + ${project.version} + + + + + + + + + org.springframework.boot + spring-boot-starter + 1.4.1.RELEASE + + + org.hibernate + hibernate-validator + 5.2.4.Final + + + + + + + + io.fabric8 + fabric8-maven-plugin + 3.1.69 + + + + resource + build + + + + + + + + \ No newline at end of file diff --git a/example/src/main/java/io/app/App.java b/example/src/main/java/io/app/App.java new file mode 100644 index 00000000..306d88aa --- /dev/null +++ b/example/src/main/java/io/app/App.java @@ -0,0 +1,16 @@ +package io.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * + */ +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } + +} diff --git a/example/src/main/java/io/app/MyBean.java b/example/src/main/java/io/app/MyBean.java new file mode 100644 index 00000000..76569e3d --- /dev/null +++ b/example/src/main/java/io/app/MyBean.java @@ -0,0 +1,29 @@ +package io.app; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * + */ +@Component +@ConfigurationProperties(prefix = "bean") +public class MyBean { + + private String message; + + @Scheduled(fixedDelay = 5000) + public void hello() { + System.out.println("The message is: " + message); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/example/src/main/resources/application.properties b/example/src/main/resources/application.properties new file mode 100644 index 00000000..358fdac4 --- /dev/null +++ b/example/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.application.name=example + + +#spring.cloud.kubernetes.reload.strategy=refresh +#spring.cloud.kubernetes.reload.mode=polling +#spring.cloud.kubernetes.reload.period=2000 +#spring.cloud.kubernetes.reload.monitoring-config-maps=false +#spring.cloud.kubernetes.reload.monitoring-secrets=true +#spring.cloud.kubernetes.reload.enabled=true diff --git a/pom.xml b/pom.xml index a560c839..14e1fdf3 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,7 @@ spring-cloud-starter-kubernetes-netflix spring-cloud-starter-kubernetes-zipkin spring-cloud-starter-kubernetes-all + example diff --git a/readme.md b/readme.md index d26eee16..58f20cda 100644 --- a/readme.md +++ b/readme.md @@ -64,7 +64,7 @@ The `ConfigMap` `PropertySource` when enabled will lookup Kubernetes for a `Conf - apply individual configuration properties. - apply as yaml the content of any property named `application.yaml` -- apple as properties file the content of any property named `application.properties` +- apply as properties file the content of any property named `application.properties` Example: @@ -196,6 +196,27 @@ You can select the Secrets to consume in a number of ways: - The property spring.cloud.kubernetes.secrets.paths behave as defined by [Collection-based binding](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#collection-based-binding). - Access to secrets via API may be restricted for security reasons, the preferred way is to mount secret to the POD. +#### PropertySource Reload + +Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. +The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when its related ConfigMap or Secret change. + +This feature is disabled by default and can be enabled using the configuration property `spring.cloud.kubernetes.reload.enabled=true` + (eg. in the *application.properties* file). + +The following levels of reload are supported (property `spring.cloud.kubernetes.reload.strategy`): +- **refresh (default)**: only spring beans annotated with `@ConfigurationProperties` or `@RefreshScope` are reloaded. +This reload level leverages the refresh feature of Spring Cloud Context. +- **restart_context**: the whole Spring _ApplicationContext_ is gracefully restarted. Beans are recreated with the new configuration. +- **shutdown**: the Spring _ApplicationContext_ is shut down to activate a restart of the container. + When using this level, 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. + +Example: + +Assuming that the reload feature is enabled with default settings, the following bean will be refreshed + + ### Pod Health Indicator Spring Boot uses [HealthIndicator](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicator.java) to expose info about the health of an application. diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java index 98e6579e..7322a3df 100644 --- a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java @@ -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); diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java index 6da9d449..1b36e1bd 100644 --- a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java @@ -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; diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadAutoConfiguration.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadAutoConfiguration.java new file mode 100644 index 00000000..bdec7c62 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadAutoConfiguration.java @@ -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 watches; + + @Autowired + public DefaultEventWatcher(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + this.watches = new ConcurrentHashMap<>(); + } + + @Async + public void addWatch(String name, Function 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); + } + } + } + } + + } + } + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadProperties.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadProperties.java new file mode 100644 index 00000000..c5c4c551 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigReloadProperties.java @@ -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 + } + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationChangeDetector.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationChangeDetector.java new file mode 100644 index 00000000..b0b2169e --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationChangeDetector.java @@ -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 s1 = mp1.getSource(); + Map 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 findPropertySource(Class sourceClass) { + List 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 > List findPropertySources(Class sourceClass) { + List managedSources = new LinkedList<>(); + + LinkedList> 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 LinkedList toLinkedList(Iterable it) { + LinkedList list = new LinkedList(); + for (E e : it) { + list.add(e); + } + return list; + } + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationUpdateStrategy.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationUpdateStrategy.java new file mode 100644 index 00000000..008ed792 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/ConfigurationUpdateStrategy.java @@ -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(); + } +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventBasedConfigurationChangeDetector.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventBasedConfigurationChangeDetector.java new file mode 100644 index 00000000..1832d7f6 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventBasedConfigurationChangeDetector.java @@ -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() { + @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() { + @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(); + } + } + } + + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventWatcher.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventWatcher.java new file mode 100644 index 00000000..24f5061e --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/EventWatcher.java @@ -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 watch); + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/PollingConfigurationChangeDetector.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/PollingConfigurationChangeDetector.java new file mode 100644 index 00000000..9e2f5bdc --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/reload/PollingConfigurationChangeDetector.java @@ -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(); + } + } + +} diff --git a/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories index 9db489c5..b1ba6a70 100644 --- a/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories @@ -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