Reload functionality clean-up part 4 (#1036)
This commit is contained in:
@@ -23,7 +23,6 @@ import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfi
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
|
||||
import org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration;
|
||||
@@ -42,6 +41,7 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationC
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.ConditionalOnKubernetesReloadEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.EventReloadDetectionMode;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.PollingReloadDetectionMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -49,106 +49,98 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnKubernetesAndConfigEnabled
|
||||
@ConditionalOnClass(EndpointAutoConfiguration.class)
|
||||
@ConditionalOnKubernetesReloadEnabled
|
||||
@ConditionalOnClass({ EndpointAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
|
||||
@AutoConfigureAfter({ InfoEndpointAutoConfiguration.class, RefreshEndpointAutoConfiguration.class,
|
||||
RefreshAutoConfiguration.class })
|
||||
@Import(ConfigReloadAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(ConfigReloadProperties.class)
|
||||
@Import(ConfigReloadAutoConfiguration.class)
|
||||
public class KubernetesClientConfigReloadAutoConfiguration {
|
||||
|
||||
/**
|
||||
* Configuration reload must be enabled explicitly.
|
||||
* Polling configMap ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param configMapPropertySourceLocator configMap property source locator
|
||||
* @return a bean that listen to configuration changes and fire a reload.
|
||||
*/
|
||||
@ConditionalOnProperty("spring.cloud.kubernetes.reload.enabled")
|
||||
@ConditionalOnClass({ RestartEndpoint.class, ContextRefresher.class })
|
||||
protected static class ConfigReloadAutoConfigurationBeans {
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientConfigMapPropertySourceLocator.class)
|
||||
@Conditional(PollingReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector configMapPropertyChangePollingWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator configMapPropertySourceLocator,
|
||||
AbstractEnvironment environment, TaskSchedulerWrapper<TaskScheduler> taskScheduler) {
|
||||
|
||||
/**
|
||||
* Polling configMap ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param configMapPropertySourceLocator configMap property source locator
|
||||
* @return a bean that listen to configuration changes and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientConfigMapPropertySourceLocator.class)
|
||||
@Conditional(PollingReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector configMapPropertyChangePollingWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator configMapPropertySourceLocator,
|
||||
AbstractEnvironment environment, TaskSchedulerWrapper taskScheduler) {
|
||||
return new PollingConfigMapChangeDetector(environment, properties, strategy,
|
||||
KubernetesClientConfigMapPropertySource.class, configMapPropertySourceLocator,
|
||||
taskScheduler.getTaskScheduler());
|
||||
}
|
||||
|
||||
return new PollingConfigMapChangeDetector(environment, properties, strategy,
|
||||
KubernetesClientConfigMapPropertySource.class, configMapPropertySourceLocator,
|
||||
taskScheduler.getTaskScheduler());
|
||||
}
|
||||
/**
|
||||
* Polling secrets ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param secretsPropertySourceLocator secrets property source locator
|
||||
* @return a bean that listen to configuration changes and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientSecretsPropertySourceLocator.class)
|
||||
@Conditional(PollingReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector secretsPropertyChangePollingWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientSecretsPropertySourceLocator secretsPropertySourceLocator, AbstractEnvironment environment,
|
||||
TaskSchedulerWrapper<TaskScheduler> taskScheduler) {
|
||||
|
||||
/**
|
||||
* Polling secrets ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param secretsPropertySourceLocator secrets property source locator
|
||||
* @return a bean that listen to configuration changes and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientSecretsPropertySourceLocator.class)
|
||||
@Conditional(PollingReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector secretsPropertyChangePollingWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientSecretsPropertySourceLocator secretsPropertySourceLocator,
|
||||
AbstractEnvironment environment, TaskSchedulerWrapper taskScheduler) {
|
||||
return new PollingSecretsChangeDetector(environment, properties, strategy,
|
||||
KubernetesClientSecretsPropertySource.class, secretsPropertySourceLocator,
|
||||
taskScheduler.getTaskScheduler());
|
||||
}
|
||||
|
||||
return new PollingSecretsChangeDetector(environment, properties, strategy,
|
||||
KubernetesClientSecretsPropertySource.class, secretsPropertySourceLocator,
|
||||
taskScheduler.getTaskScheduler());
|
||||
}
|
||||
/**
|
||||
* Event Based configMap ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param configMapPropertySourceLocator configMap property source locator
|
||||
* @return a bean that listen to configMap change events and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientConfigMapPropertySourceLocator.class)
|
||||
@Conditional(EventReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector configMapPropertyChangeEventWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator configMapPropertySourceLocator,
|
||||
AbstractEnvironment environment, CoreV1Api coreV1Api,
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
|
||||
/**
|
||||
* Event Based configMap ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param configMapPropertySourceLocator configMap property source locator
|
||||
* @return a bean that listen to configMap change events and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientConfigMapPropertySourceLocator.class)
|
||||
@Conditional(EventReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector configMapPropertyChangeEventWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator configMapPropertySourceLocator,
|
||||
AbstractEnvironment environment, CoreV1Api coreV1Api,
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
return new KubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment, properties, strategy,
|
||||
configMapPropertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
|
||||
return new KubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment, properties, strategy,
|
||||
configMapPropertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Based secrets ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param secretsPropertySourceLocator secrets property source locator
|
||||
* @return a bean that listen to secrets change events and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientSecretsPropertySourceLocator.class)
|
||||
@Conditional(EventReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector secretsPropertyChangeEventWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientSecretsPropertySourceLocator secretsPropertySourceLocator,
|
||||
AbstractEnvironment environment, CoreV1Api coreV1Api,
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
|
||||
return new KubernetesClientEventBasedSecretsChangeDetector(coreV1Api, environment, properties, strategy,
|
||||
secretsPropertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
/**
|
||||
* Event Based secrets ConfigurationChangeDetector.
|
||||
* @param properties config reload properties
|
||||
* @param strategy configuration update strategy
|
||||
* @param secretsPropertySourceLocator secrets property source locator
|
||||
* @return a bean that listen to secrets change events and fire a reload.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(KubernetesClientSecretsPropertySourceLocator.class)
|
||||
@Conditional(EventReloadDetectionMode.class)
|
||||
public ConfigurationChangeDetector secretsPropertyChangeEventWatcher(ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy,
|
||||
KubernetesClientSecretsPropertySourceLocator secretsPropertySourceLocator, AbstractEnvironment environment,
|
||||
CoreV1Api coreV1Api, KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
|
||||
return new KubernetesClientEventBasedSecretsChangeDetector(coreV1Api, environment, properties, strategy,
|
||||
secretsPropertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,12 +25,9 @@ import io.kubernetes.client.openapi.models.V1ConfigMapList;
|
||||
import io.kubernetes.client.util.CallGeneratorParams;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationChangeDetector;
|
||||
@@ -44,17 +41,15 @@ import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.
|
||||
*/
|
||||
public class KubernetesClientEventBasedConfigMapChangeDetector extends ConfigurationChangeDetector {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(KubernetesClientEventBasedConfigMapChangeDetector.class);
|
||||
|
||||
private CoreV1Api coreV1Api = null;
|
||||
private final CoreV1Api coreV1Api;
|
||||
|
||||
private final KubernetesClientConfigMapPropertySourceLocator propertySourceLocator;
|
||||
|
||||
private final SharedInformerFactory factory;
|
||||
|
||||
private KubernetesClientProperties kubernetesClientProperties;
|
||||
private final String namespace;
|
||||
|
||||
private KubernetesNamespaceProvider kubernetesNamespaceProvider;
|
||||
private final boolean monitorConfigMaps;
|
||||
|
||||
public KubernetesClientEventBasedConfigMapChangeDetector(CoreV1Api coreV1Api, ConfigurableEnvironment environment,
|
||||
ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy,
|
||||
@@ -71,38 +66,33 @@ public class KubernetesClientEventBasedConfigMapChangeDetector extends Configura
|
||||
// certificate authorities for the cluster. This results in SSL errors.
|
||||
// See https://github.com/spring-cloud/spring-cloud-kubernetes/issues/885
|
||||
this.factory = new SharedInformerFactory(createApiClientForInformerClient());
|
||||
this.kubernetesNamespaceProvider = kubernetesNamespaceProvider;
|
||||
}
|
||||
|
||||
private String getNamespace() {
|
||||
return kubernetesNamespaceProvider != null ? kubernetesNamespaceProvider.getNamespace()
|
||||
: kubernetesClientProperties.getNamespace();
|
||||
this.namespace = kubernetesNamespaceProvider.getNamespace();
|
||||
this.monitorConfigMaps = properties.isMonitoringConfigMaps();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void watch() {
|
||||
if (coreV1Api != null && this.properties.isMonitoringConfigMaps()) {
|
||||
if (coreV1Api != null && monitorConfigMaps) {
|
||||
SharedIndexInformer<V1ConfigMap> configMapInformer = factory.sharedIndexInformerFor(
|
||||
(CallGeneratorParams params) -> coreV1Api.listNamespacedConfigMapCall(getNamespace(), null, null,
|
||||
null, null, null, null, params.resourceVersion, null, params.timeoutSeconds, params.watch,
|
||||
null),
|
||||
(CallGeneratorParams params) -> coreV1Api.listNamespacedConfigMapCall(namespace, null, null, null,
|
||||
null, null, null, params.resourceVersion, null, params.timeoutSeconds, params.watch, null),
|
||||
V1ConfigMap.class, V1ConfigMapList.class);
|
||||
configMapInformer.addEventHandler(new ResourceEventHandler<V1ConfigMap>() {
|
||||
configMapInformer.addEventHandler(new ResourceEventHandler<>() {
|
||||
@Override
|
||||
public void onAdd(V1ConfigMap obj) {
|
||||
LOG.info("CongifMap " + obj.getMetadata().getName() + " was added.");
|
||||
log.info("ConfigMap " + obj.getMetadata().getName() + " was added.");
|
||||
onEvent(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(V1ConfigMap oldObj, V1ConfigMap newObj) {
|
||||
LOG.info("ConfigMap " + newObj.getMetadata().getName() + " was added.");
|
||||
log.info("ConfigMap " + newObj.getMetadata().getName() + " was added.");
|
||||
onEvent(newObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete(V1ConfigMap obj, boolean deletedFinalStateUnknown) {
|
||||
LOG.info("ConfigMap " + obj.getMetadata() + " was deleted.");
|
||||
log.info("ConfigMap " + obj.getMetadata() + " was deleted.");
|
||||
onEvent(obj);
|
||||
}
|
||||
});
|
||||
@@ -116,15 +106,15 @@ public class KubernetesClientEventBasedConfigMapChangeDetector extends Configura
|
||||
}
|
||||
|
||||
private void onEvent(V1ConfigMap configMap) {
|
||||
this.log.debug(String.format("onEvent configMap: %s", configMap.toString()));
|
||||
log.debug("onEvent configMap: " + configMap.toString());
|
||||
boolean changed = changed(locateMapPropertySources(this.propertySourceLocator, this.environment),
|
||||
findPropertySources(KubernetesClientConfigMapPropertySource.class));
|
||||
if (changed) {
|
||||
LOG.info("Configuration change detected, reloading properties.");
|
||||
log.info("Configuration change detected, reloading properties.");
|
||||
reloadProperties();
|
||||
}
|
||||
else {
|
||||
LOG.warn("Configuration change was not detected.");
|
||||
log.warn("Configuration change was not detected.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,12 +24,9 @@ import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||
import io.kubernetes.client.util.CallGeneratorParams;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationChangeDetector;
|
||||
@@ -43,17 +40,15 @@ import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.
|
||||
*/
|
||||
public class KubernetesClientEventBasedSecretsChangeDetector extends ConfigurationChangeDetector {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(KubernetesClientEventBasedSecretsChangeDetector.class);
|
||||
|
||||
private CoreV1Api coreV1Api;
|
||||
private final CoreV1Api coreV1Api;
|
||||
|
||||
private final KubernetesClientSecretsPropertySourceLocator propertySourceLocator;
|
||||
|
||||
private final SharedInformerFactory factory;
|
||||
|
||||
private KubernetesClientProperties kubernetesClientProperties;
|
||||
private final String namespace;
|
||||
|
||||
private KubernetesNamespaceProvider kubernetesNamespaceProvider;
|
||||
private final boolean monitorSecrets;
|
||||
|
||||
public KubernetesClientEventBasedSecretsChangeDetector(CoreV1Api coreV1Api, ConfigurableEnvironment environment,
|
||||
ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy,
|
||||
@@ -70,37 +65,33 @@ public class KubernetesClientEventBasedSecretsChangeDetector extends Configurati
|
||||
// See https://github.com/spring-cloud/spring-cloud-kubernetes/issues/885
|
||||
this.factory = new SharedInformerFactory(createApiClientForInformerClient());
|
||||
this.coreV1Api = coreV1Api;
|
||||
this.kubernetesNamespaceProvider = kubernetesNamespaceProvider;
|
||||
}
|
||||
|
||||
private String getNamespace() {
|
||||
return kubernetesNamespaceProvider != null ? kubernetesNamespaceProvider.getNamespace()
|
||||
: kubernetesClientProperties.getNamespace();
|
||||
this.namespace = kubernetesNamespaceProvider.getNamespace();
|
||||
this.monitorSecrets = properties.isMonitoringSecrets();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void watch() {
|
||||
if (coreV1Api != null && this.properties.isMonitoringSecrets()) {
|
||||
if (coreV1Api != null && monitorSecrets) {
|
||||
SharedIndexInformer<V1Secret> configMapInformer = factory.sharedIndexInformerFor(
|
||||
(CallGeneratorParams params) -> coreV1Api.listNamespacedSecretCall(getNamespace(), null, null, null,
|
||||
(CallGeneratorParams params) -> coreV1Api.listNamespacedSecretCall(namespace, null, null, null,
|
||||
null, null, null, params.resourceVersion, null, params.timeoutSeconds, params.watch, null),
|
||||
V1Secret.class, V1SecretList.class);
|
||||
configMapInformer.addEventHandler(new ResourceEventHandler<V1Secret>() {
|
||||
configMapInformer.addEventHandler(new ResourceEventHandler<>() {
|
||||
@Override
|
||||
public void onAdd(V1Secret obj) {
|
||||
LOG.info("Secret " + obj.getMetadata().getName() + " was added.");
|
||||
log.info("Secret " + obj.getMetadata().getName() + " was added.");
|
||||
onEvent(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(V1Secret oldObj, V1Secret newObj) {
|
||||
LOG.info("Secret " + newObj.getMetadata().getName() + " was added.");
|
||||
log.info("Secret " + newObj.getMetadata().getName() + " was added.");
|
||||
onEvent(newObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete(V1Secret obj, boolean deletedFinalStateUnknown) {
|
||||
LOG.info("Secret " + obj.getMetadata() + " was deleted.");
|
||||
log.info("Secret " + obj.getMetadata() + " was deleted.");
|
||||
onEvent(obj);
|
||||
}
|
||||
});
|
||||
@@ -109,11 +100,11 @@ public class KubernetesClientEventBasedSecretsChangeDetector extends Configurati
|
||||
}
|
||||
|
||||
private void onEvent(V1Secret secret) {
|
||||
this.log.debug(String.format("onEvent configMap: %s", secret.toString()));
|
||||
log.debug("onEvent configMap: " + secret.toString());
|
||||
boolean changed = changed(locateMapPropertySources(this.propertySourceLocator, this.environment),
|
||||
findPropertySources(KubernetesClientSecretsPropertySource.class));
|
||||
if (changed) {
|
||||
this.log.info("Detected change in secrets");
|
||||
log.info("Detected change in secrets");
|
||||
reloadProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,22 +16,21 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.commons.config.reload;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
|
||||
import org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration;
|
||||
import org.springframework.cloud.commons.util.TaskSchedulerWrapper;
|
||||
import org.springframework.cloud.context.refresh.ContextRefresher;
|
||||
import org.springframework.cloud.context.restart.RestartEndpoint;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConditionalOnKubernetesAndConfigEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.ConditionalOnKubernetesReloadEnabled;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -43,10 +42,10 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnKubernetesAndConfigEnabled
|
||||
@ConditionalOnClass(EndpointAutoConfiguration.class)
|
||||
@ConditionalOnKubernetesReloadEnabled
|
||||
@ConditionalOnClass({ EndpointAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
|
||||
@AutoConfigureAfter({ InfoEndpointAutoConfiguration.class, RefreshEndpointAutoConfiguration.class,
|
||||
RefreshAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
|
||||
@ConditionalOnProperty("spring.cloud.kubernetes.reload.enabled")
|
||||
RefreshAutoConfiguration.class })
|
||||
public class ConfigReloadAutoConfiguration {
|
||||
|
||||
@Bean("springCloudKubernetesTaskScheduler")
|
||||
@@ -63,32 +62,33 @@ public class ConfigReloadAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ConfigurationUpdateStrategy configurationUpdateStrategy(ConfigReloadProperties properties,
|
||||
ConfigurableApplicationContext ctx, @Autowired(required = false) RestartEndpoint restarter,
|
||||
ContextRefresher refresher) {
|
||||
ConfigurableApplicationContext ctx, Optional<RestartEndpoint> restarter, ContextRefresher refresher) {
|
||||
String strategyName = properties.getStrategy().name();
|
||||
switch (properties.getStrategy()) {
|
||||
case RESTART_CONTEXT:
|
||||
Objects.requireNonNull(restarter, "Restart endpoint is not enabled");
|
||||
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), () -> {
|
||||
restarter.orElseThrow(() -> new AssertionError("Restart endpoint is not enabled"));
|
||||
return new ConfigurationUpdateStrategy(strategyName, () -> {
|
||||
wait(properties);
|
||||
restarter.restart();
|
||||
restarter.get().restart();
|
||||
});
|
||||
case REFRESH:
|
||||
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), refresher::refresh);
|
||||
return new ConfigurationUpdateStrategy(strategyName, refresher::refresh);
|
||||
case SHUTDOWN:
|
||||
return new ConfigurationUpdateStrategy(properties.getStrategy().name(), () -> {
|
||||
return new ConfigurationUpdateStrategy(strategyName, () -> {
|
||||
wait(properties);
|
||||
ctx.close();
|
||||
});
|
||||
}
|
||||
throw new IllegalStateException("Unsupported configuration update strategy: " + properties.getStrategy());
|
||||
throw new IllegalStateException("Unsupported configuration update strategy: " + strategyName);
|
||||
}
|
||||
|
||||
private static void wait(ConfigReloadProperties properties) {
|
||||
final long waitMillis = ThreadLocalRandom.current().nextLong(properties.getMaxWaitForRestart().toMillis());
|
||||
long waitMillis = ThreadLocalRandom.current().nextLong(properties.getMaxWaitForRestart().toMillis());
|
||||
try {
|
||||
Thread.sleep(waitMillis);
|
||||
}
|
||||
catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.commons.config.reload;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -47,7 +46,9 @@ public class PollingConfigMapChangeDetector extends ConfigurationChangeDetector
|
||||
|
||||
private final TaskScheduler taskExecutor;
|
||||
|
||||
private final Duration period;
|
||||
private final long period;
|
||||
|
||||
private final boolean monitorConfigMaps;
|
||||
|
||||
public PollingConfigMapChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy, Class<? extends MapPropertySource> propertySourceClass,
|
||||
@@ -56,21 +57,22 @@ public class PollingConfigMapChangeDetector extends ConfigurationChangeDetector
|
||||
this.propertySourceLocator = propertySourceLocator;
|
||||
this.propertySourceClass = propertySourceClass;
|
||||
this.taskExecutor = taskExecutor;
|
||||
this.period = properties.getPeriod();
|
||||
this.period = properties.getPeriod().toMillis();
|
||||
this.monitorConfigMaps = properties.isMonitoringConfigMaps();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
private void init() {
|
||||
log.info("Kubernetes polling configMap change detector activated");
|
||||
PeriodicTrigger trigger = new PeriodicTrigger(period.toMillis());
|
||||
trigger.setInitialDelay(period.toMillis());
|
||||
PeriodicTrigger trigger = new PeriodicTrigger(period);
|
||||
trigger.setInitialDelay(period);
|
||||
taskExecutor.schedule(this::executeCycle, trigger);
|
||||
}
|
||||
|
||||
private void executeCycle() {
|
||||
|
||||
boolean changedConfigMap = false;
|
||||
if (properties.isMonitoringConfigMaps()) {
|
||||
if (monitorConfigMaps) {
|
||||
log.debug("Polling for changes in config maps");
|
||||
List<? extends MapPropertySource> currentConfigMapSources = findPropertySources(propertySourceClass);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.commons.config.reload;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -47,7 +46,9 @@ public class PollingSecretsChangeDetector extends ConfigurationChangeDetector {
|
||||
|
||||
private final TaskScheduler taskExecutor;
|
||||
|
||||
private final Duration period;
|
||||
private final long period;
|
||||
|
||||
private final boolean monitorSecrets;
|
||||
|
||||
public PollingSecretsChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy, Class<? extends MapPropertySource> propertySourceClass,
|
||||
@@ -56,21 +57,22 @@ public class PollingSecretsChangeDetector extends ConfigurationChangeDetector {
|
||||
this.propertySourceLocator = propertySourceLocator;
|
||||
this.propertySourceClass = propertySourceClass;
|
||||
this.taskExecutor = taskExecutor;
|
||||
this.period = properties.getPeriod();
|
||||
this.period = properties.getPeriod().toMillis();
|
||||
this.monitorSecrets = properties.isMonitoringSecrets();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
private void init() {
|
||||
log.info("Kubernetes polling secrets change detector activated");
|
||||
PeriodicTrigger trigger = new PeriodicTrigger(period.toMillis());
|
||||
trigger.setInitialDelay(period.toMillis());
|
||||
PeriodicTrigger trigger = new PeriodicTrigger(period);
|
||||
trigger.setInitialDelay(period);
|
||||
taskExecutor.schedule(this::executeCycle, trigger);
|
||||
}
|
||||
|
||||
public void executeCycle() {
|
||||
private void executeCycle() {
|
||||
|
||||
boolean changedSecrets = false;
|
||||
if (this.properties.isMonitoringSecrets()) {
|
||||
if (monitorSecrets) {
|
||||
log.debug("Polling for changes in secrets");
|
||||
List<MapPropertySource> currentSecretSources = locateMapPropertySources(this.propertySourceLocator,
|
||||
this.environment);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.commons.config.reload.condition;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
/**
|
||||
* Provides a more succinct conditional for:
|
||||
* <code>spring.cloud.kubernetes.reload.enabled</code>.
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@ConditionalOnProperty(name = "spring.cloud.kubernetes.reload.enabled", havingValue = "true")
|
||||
public @interface ConditionalOnKubernetesReloadEnabled {
|
||||
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public abstract class SecretsWatcherChangeDetector extends Fabric8EventBasedSecr
|
||||
|
||||
protected Log log = LogFactory.getLog(getClass());
|
||||
|
||||
private ScheduledExecutorService executorService;
|
||||
private final ScheduledExecutorService executorService;
|
||||
|
||||
protected ConfigurationWatcherConfigurationProperties k8SConfigurationProperties;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,6 @@ import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfi
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
|
||||
import org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration;
|
||||
@@ -37,6 +36,7 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationC
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.ConditionalOnKubernetesReloadEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.EventReloadDetectionMode;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.condition.PollingReloadDetectionMode;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource;
|
||||
@@ -58,11 +58,11 @@ import org.springframework.scheduling.TaskScheduler;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnKubernetesAndConfigEnabled
|
||||
@ConditionalOnClass(EndpointAutoConfiguration.class)
|
||||
@ConditionalOnKubernetesReloadEnabled
|
||||
@ConditionalOnClass({ EndpointAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
|
||||
@AutoConfigureAfter({ InfoEndpointAutoConfiguration.class, RefreshEndpointAutoConfiguration.class,
|
||||
RefreshAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
|
||||
RefreshAutoConfiguration.class })
|
||||
@EnableConfigurationProperties(ConfigReloadProperties.class)
|
||||
@ConditionalOnProperty("spring.cloud.kubernetes.reload.enabled")
|
||||
@Import(ConfigReloadAutoConfiguration.class)
|
||||
public class Fabric8ConfigReloadAutoConfiguration {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,18 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.fabric8.config.reload;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.Status;
|
||||
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 io.fabric8.kubernetes.client.WatcherException;
|
||||
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
|
||||
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
@@ -50,9 +42,11 @@ public class Fabric8EventBasedConfigMapChangeDetector extends ConfigurationChang
|
||||
|
||||
private final Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator;
|
||||
|
||||
private final Map<String, Watch> watches;
|
||||
private final KubernetesClient kubernetesClient;
|
||||
|
||||
private KubernetesClient kubernetesClient;
|
||||
private final boolean monitoringConfigMaps;
|
||||
|
||||
private SharedIndexInformer<ConfigMap> informer;
|
||||
|
||||
public Fabric8EventBasedConfigMapChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties,
|
||||
KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy,
|
||||
@@ -60,80 +54,60 @@ public class Fabric8EventBasedConfigMapChangeDetector extends ConfigurationChang
|
||||
super(environment, properties, strategy);
|
||||
this.kubernetesClient = kubernetesClient;
|
||||
this.fabric8ConfigMapPropertySourceLocator = fabric8ConfigMapPropertySourceLocator;
|
||||
this.watches = new HashMap<>();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
// Ensure the kubernetes client is cleaned up from spare threads when shutting
|
||||
// down
|
||||
this.kubernetesClient.close();
|
||||
monitoringConfigMaps = properties.isMonitoringConfigMaps();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void watch() {
|
||||
boolean activated = false;
|
||||
private void inform() {
|
||||
if (monitoringConfigMaps) {
|
||||
log.info("Kubernetes event-based configMap change detector activated");
|
||||
informer = kubernetesClient.configMaps().inform();
|
||||
informer.addEventHandler(new ResourceEventHandler<>() {
|
||||
@Override
|
||||
public void onAdd(ConfigMap configMap) {
|
||||
onEvent(configMap);
|
||||
}
|
||||
|
||||
if (this.properties.isMonitoringConfigMaps()) {
|
||||
try {
|
||||
String name = "config-maps-watch-event";
|
||||
this.watches.put(name, this.kubernetesClient.configMaps().watch(new Watcher<ConfigMap>() {
|
||||
@Override
|
||||
public void eventReceived(Watcher.Action action, ConfigMap configMap) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(name + " received event for ConfigMap " + configMap.getMetadata().getName());
|
||||
}
|
||||
onEvent(configMap);
|
||||
}
|
||||
@Override
|
||||
public void onUpdate(ConfigMap oldConfigMap, ConfigMap newConfigMap) {
|
||||
onEvent(newConfigMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WatcherException exception) {
|
||||
log.warn("ConfigMaps watch closed", exception);
|
||||
Optional.ofNullable(exception).map(e -> {
|
||||
log.debug("Exception received during watch", e);
|
||||
return exception.asClientException();
|
||||
}).map(KubernetesClientException::getStatus).map(Status::getCode)
|
||||
.filter(c -> c.equals(HttpURLConnection.HTTP_GONE)).ifPresent(c -> watch());
|
||||
}
|
||||
}));
|
||||
activated = true;
|
||||
this.log.info("Added new Kubernetes watch: " + name);
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.log.error(
|
||||
"Error while establishing a connection to watch config maps: configuration may remain stale",
|
||||
e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onDelete(ConfigMap configMap, boolean deletedFinalStateUnknown) {
|
||||
onEvent(configMap);
|
||||
}
|
||||
|
||||
if (activated) {
|
||||
this.log.info("Kubernetes event-based configMap change detector activated");
|
||||
// leave as comment on purpose, may be this will be useful in the future
|
||||
// @Override
|
||||
// public void onNothing() {
|
||||
// boolean isStoreEmpty = informer.getStore().list().isEmpty();
|
||||
// if(!isStoreEmpty) {
|
||||
// // HTTP_GONE, thus re-inform
|
||||
// inform();
|
||||
// }
|
||||
// }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void unwatch() {
|
||||
if (this.watches != null) {
|
||||
for (Map.Entry<String, Watch> entry : this.watches.entrySet()) {
|
||||
try {
|
||||
this.log.debug("Closing the watch " + entry.getKey());
|
||||
entry.getValue().close();
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.log.error("Error while closing the watch connection", e);
|
||||
}
|
||||
}
|
||||
private void shutdown() {
|
||||
if (informer != null) {
|
||||
log.debug("closing configmap informer");
|
||||
informer.close();
|
||||
}
|
||||
// Ensure the kubernetes client is cleaned up from spare threads when shutting
|
||||
// down
|
||||
kubernetesClient.close();
|
||||
}
|
||||
|
||||
protected void onEvent(ConfigMap configMap) {
|
||||
this.log.debug(String.format("onEvent configMap: %s", configMap.toString()));
|
||||
boolean changed = changed(
|
||||
locateMapPropertySources(this.fabric8ConfigMapPropertySourceLocator, this.environment),
|
||||
log.debug("onEvent configMap: " + configMap.toString());
|
||||
boolean changed = changed(locateMapPropertySources(fabric8ConfigMapPropertySourceLocator, environment),
|
||||
findPropertySources(Fabric8ConfigMapPropertySource.class));
|
||||
if (changed) {
|
||||
this.log.info("Detected change in config maps");
|
||||
log.info("Detected change in config maps");
|
||||
reloadProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
* Copyright 2013-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,18 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.fabric8.config.reload;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.Status;
|
||||
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 io.fabric8.kubernetes.client.WatcherException;
|
||||
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
|
||||
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
@@ -48,11 +40,13 @@ import org.springframework.core.env.AbstractEnvironment;
|
||||
*/
|
||||
public class Fabric8EventBasedSecretsChangeDetector extends ConfigurationChangeDetector {
|
||||
|
||||
private Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator;
|
||||
private final Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator;
|
||||
|
||||
private Map<String, Watch> watches;
|
||||
private final KubernetesClient kubernetesClient;
|
||||
|
||||
private KubernetesClient kubernetesClient;
|
||||
private final boolean monitorSecrets;
|
||||
|
||||
private SharedIndexInformer<Secret> informer;
|
||||
|
||||
public Fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties,
|
||||
KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy,
|
||||
@@ -60,79 +54,60 @@ public class Fabric8EventBasedSecretsChangeDetector extends ConfigurationChangeD
|
||||
super(environment, properties, strategy);
|
||||
this.kubernetesClient = kubernetesClient;
|
||||
this.fabric8SecretsPropertySourceLocator = fabric8SecretsPropertySourceLocator;
|
||||
this.watches = new HashMap<>();
|
||||
this.monitorSecrets = properties.isMonitoringSecrets();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
private void shutdown() {
|
||||
if (informer != null) {
|
||||
log.debug("closing secrets informer");
|
||||
informer.close();
|
||||
}
|
||||
// Ensure the kubernetes client is cleaned up from spare threads when shutting
|
||||
// down
|
||||
this.kubernetesClient.close();
|
||||
kubernetesClient.close();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void watch() {
|
||||
boolean activated = false;
|
||||
|
||||
if (this.properties.isMonitoringSecrets()) {
|
||||
try {
|
||||
activated = false;
|
||||
String name = "secrets-watch-event";
|
||||
this.watches.put(name, this.kubernetesClient.secrets().watch(new Watcher<Secret>() {
|
||||
@Override
|
||||
public void eventReceived(Action action, Secret secret) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(name + " received event for Secret " + secret.getMetadata().getName());
|
||||
}
|
||||
onEvent(secret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WatcherException exception) {
|
||||
log.warn("Secrects watch closed", exception);
|
||||
Optional.ofNullable(exception).map(e -> {
|
||||
log.debug("Exception received during watch", e);
|
||||
return exception.asClientException();
|
||||
}).map(KubernetesClientException::getStatus).map(Status::getCode)
|
||||
.filter(c -> c.equals(HttpURLConnection.HTTP_GONE)).ifPresent(c -> watch());
|
||||
}
|
||||
}));
|
||||
activated = true;
|
||||
this.log.info("Added new Kubernetes watch: " + name);
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.log.error("Error while establishing a connection to watch secrets: configuration may remain stale",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
if (activated) {
|
||||
this.log.info("Kubernetes event-based secrets change detector activated");
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void unwatch() {
|
||||
if (this.watches != null) {
|
||||
for (Map.Entry<String, Watch> entry : this.watches.entrySet()) {
|
||||
try {
|
||||
this.log.debug("Closing the watch " + entry.getKey());
|
||||
entry.getValue().close();
|
||||
|
||||
private void inform() {
|
||||
if (monitorSecrets) {
|
||||
log.info("Kubernetes event-based secrets change detector activated");
|
||||
informer = kubernetesClient.secrets().inform();
|
||||
informer.addEventHandler(new ResourceEventHandler<>() {
|
||||
@Override
|
||||
public void onAdd(Secret secret) {
|
||||
onEvent(secret);
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.log.error("Error while closing the watch connection", e);
|
||||
|
||||
@Override
|
||||
public void onUpdate(Secret oldSecret, Secret newSecret) {
|
||||
onEvent(newSecret);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete(Secret secret, boolean deletedFinalStateUnknown) {
|
||||
onEvent(secret);
|
||||
}
|
||||
|
||||
// leave as comment on purpose, may be this will be useful in the future
|
||||
// @Override
|
||||
// public void onNothing() {
|
||||
// boolean isStoreEmpty = informer.getStore().list().isEmpty();
|
||||
// if(!isStoreEmpty) {
|
||||
// // HTTP_GONE, thus re-inform
|
||||
// inform();
|
||||
// }
|
||||
// }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void onEvent(Secret secret) {
|
||||
this.log.debug(String.format("onEvent configMap: %s", secret.toString()));
|
||||
boolean changed = changed(locateMapPropertySources(this.fabric8SecretsPropertySourceLocator, this.environment),
|
||||
log.debug("onEvent secrets: " + secret.toString());
|
||||
boolean changed = changed(locateMapPropertySources(fabric8SecretsPropertySourceLocator, environment),
|
||||
findPropertySources(Fabric8SecretsPropertySource.class));
|
||||
if (changed) {
|
||||
this.log.info("Detected change in secrets");
|
||||
log.info("Detected change in secrets");
|
||||
reloadProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ public class KubernetesConfigTestBase {
|
||||
}
|
||||
|
||||
protected void setup(Class<?> mockClientConfiguration, String... env) {
|
||||
String[] properties = Stream.concat(Arrays.stream(commonProperties), Arrays.stream(env))
|
||||
.toArray(size -> new String[size]);
|
||||
String[] properties = Stream.concat(Arrays.stream(commonProperties), Arrays.stream(env)).toArray(String[]::new);
|
||||
context = new SpringApplicationBuilder(PropertyPlaceholderAutoConfiguration.class, mockClientConfiguration,
|
||||
BootstrapConfiguration.class, Fabric8ConfigReloadAutoConfiguration.class,
|
||||
RefreshAutoConfiguration.class).web(org.springframework.boot.WebApplicationType.NONE)
|
||||
|
||||
@@ -114,5 +114,8 @@
|
||||
<module>spring-cloud-kubernetes-client-reactive-discoveryclient-it</module>
|
||||
<module>spring-cloud-kubernetes-configuration-watcher-it</module>
|
||||
<module>spring-cloud-kubernetes-core-k8s-client-it</module>
|
||||
</modules>
|
||||
<module>spring-cloud-kubernetes-fabric8-client-configmap-polling-reload</module>
|
||||
<module>spring-cloud-kubernetes-fabric8-client-secrets-event-reload</module>
|
||||
<module>spring-cloud-kubernetes-client-secrets-event-reload</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-kubernetes-client-secrets-event-reload</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.kubernetes</groupId>
|
||||
<artifactId>client-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.kubernetes</groupId>
|
||||
<artifactId>client-java-extended</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-transport-httpclient5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>k3s</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<!-- build image in the 'package' phase, and ignore plain tests -->
|
||||
<!-- via maven-surefire-plugin::skipTests -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
|
||||
<imageBuilder>paketobuildpacks/builder</imageBuilder>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- run tests in the 'integration-tests' phase, one that is after 'package' (where we build the image) -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>${testsToRun}</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.client.secrets.event.reload;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(SecretsProperties.class)
|
||||
public class SecretsApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SecretsApp.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.client.secrets.event.reload;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@RestController
|
||||
public class SecretsController {
|
||||
|
||||
private final SecretsProperties properties;
|
||||
|
||||
public SecretsController(SecretsProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@GetMapping("/key")
|
||||
public String key() {
|
||||
return properties.getKey();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.client.secrets.event.reload;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@ConfigurationProperties("from.properties")
|
||||
public class SecretsProperties {
|
||||
|
||||
private String key;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key1) {
|
||||
this.key = key1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
logging:
|
||||
level:
|
||||
root: DEBUG
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: event-reload
|
||||
config:
|
||||
import: "kubernetes:"
|
||||
cloud:
|
||||
kubernetes:
|
||||
reload:
|
||||
enabled: true
|
||||
strategy: shutdown
|
||||
mode: event
|
||||
monitoring-secrets: true
|
||||
secrets:
|
||||
enabled: true
|
||||
enable-api: true
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or 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
|
||||
*
|
||||
* https://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.client.secrets.event.reload;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.kubernetes.client.openapi.apis.AppsV1Api;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1Deployment;
|
||||
import io.kubernetes.client.openapi.models.V1Ingress;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1Service;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import org.testcontainers.shaded.org.awaitility.Awaitility;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
|
||||
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.getPomVersion;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class SecretsEventReloadIT {
|
||||
|
||||
private static final String PROPERTY_URL = "localhost:80/key";
|
||||
|
||||
private static final String SPRING_CLOUD_CLIENT_CONFIG_IT_DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-secrets-deployment-event-reload";
|
||||
|
||||
private static final String K8S_CONFIG_CLIENT_IT_SERVICE_NAME = "spring-cloud-kubernetes-client-secrets-event-reload";
|
||||
|
||||
private static final String NAMESPACE = "default";
|
||||
|
||||
private static CoreV1Api api;
|
||||
|
||||
private static AppsV1Api appsApi;
|
||||
|
||||
private static NetworkingV1Api networkingApi;
|
||||
|
||||
private static K8SUtils k8SUtils;
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
K3S.start();
|
||||
Commons.validateImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
|
||||
createApiClient(K3S.getKubeConfigYaml());
|
||||
api = new CoreV1Api();
|
||||
appsApi = new AppsV1Api();
|
||||
networkingApi = new NetworkingV1Api();
|
||||
k8SUtils = new K8SUtils(api, appsApi);
|
||||
k8SUtils.setUp(NAMESPACE);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() throws Exception {
|
||||
Commons.cleanUp(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, K3S);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void after() throws Exception {
|
||||
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
|
||||
"metadata.name=" + SPRING_CLOUD_CLIENT_CONFIG_IT_DEPLOYMENT_NAME, null, null, null, null, null, null,
|
||||
null, null, null);
|
||||
api.deleteNamespacedService(K8S_CONFIG_CLIENT_IT_SERVICE_NAME, NAMESPACE, null, null, null, null, null, null);
|
||||
networkingApi.deleteNamespacedIngress("spring-cloud-kubernetes-client-secrets-ingress-event-reload", NAMESPACE,
|
||||
null, null, null, null, null, null);
|
||||
api.deleteNamespacedSecret("event-reload", NAMESPACE, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSecretReload() throws Exception {
|
||||
deployConfigK8sClientIt();
|
||||
|
||||
// Check to make sure the controller deployment is ready
|
||||
k8SUtils.waitForDeployment(SPRING_CLOUD_CLIENT_CONFIG_IT_DEPLOYMENT_NAME, NAMESPACE);
|
||||
testSecretEventReload();
|
||||
}
|
||||
|
||||
void testSecretEventReload() throws Exception {
|
||||
|
||||
WebClient.Builder builder = builder();
|
||||
WebClient secretClient = builder.baseUrl(PROPERTY_URL).build();
|
||||
String secret = secretClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec())
|
||||
.block();
|
||||
assertThat(secret).isEqualTo("initial");
|
||||
|
||||
V1Secret v1Secret = getConfigK8sClientItCSecret();
|
||||
Map<String, byte[]> secretData = v1Secret.getData();
|
||||
secretData.replace("application.properties", "from.properties.key: after-change".getBytes());
|
||||
v1Secret.setData(secretData);
|
||||
api.replaceNamespacedSecret("event-reload", NAMESPACE, v1Secret, null, null, null);
|
||||
|
||||
Awaitility.await().timeout(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(2))
|
||||
.until(() -> secretClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class)
|
||||
.retryWhen(retrySpec()).block().equals("after-change"));
|
||||
}
|
||||
|
||||
private static void deployConfigK8sClientIt() throws Exception {
|
||||
k8SUtils.waitForDeploymentToBeDeleted(SPRING_CLOUD_CLIENT_CONFIG_IT_DEPLOYMENT_NAME, NAMESPACE);
|
||||
api.createNamespacedSecret(NAMESPACE, getConfigK8sClientItCSecret(), null, null, null);
|
||||
appsApi.createNamespacedDeployment(NAMESPACE, getConfigK8sClientItDeployment(), null, null, null);
|
||||
api.createNamespacedService(NAMESPACE, getConfigK8sClientItService(), null, null, null);
|
||||
|
||||
V1Ingress ingress = getConfigK8sClientItIngress();
|
||||
networkingApi.createNamespacedIngress(NAMESPACE, ingress, null, null, null);
|
||||
k8SUtils.waitForIngress(ingress.getMetadata().getName(), NAMESPACE);
|
||||
}
|
||||
|
||||
private static V1Deployment getConfigK8sClientItDeployment() throws Exception {
|
||||
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath("deployment.yaml");
|
||||
String image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + ":"
|
||||
+ getPomVersion();
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
|
||||
return deployment;
|
||||
}
|
||||
|
||||
private static V1Service getConfigK8sClientItService() throws Exception {
|
||||
return (V1Service) K8SUtils.readYamlFromClasspath("service.yaml");
|
||||
}
|
||||
|
||||
private static V1Ingress getConfigK8sClientItIngress() throws Exception {
|
||||
return (V1Ingress) K8SUtils.readYamlFromClasspath("ingress.yaml");
|
||||
}
|
||||
|
||||
private static V1Secret getConfigK8sClientItCSecret() throws Exception {
|
||||
return (V1Secret) K8SUtils.readYamlFromClasspath("secret.yaml");
|
||||
}
|
||||
|
||||
private WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
private RetryBackoffSpec retrySpec() {
|
||||
return Retry.fixedDelay(60, Duration.ofSeconds(2)).filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-client-secrets-deployment-event-reload
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
spec:
|
||||
serviceAccountName: spring-cloud-kubernetes-serviceaccount
|
||||
containers:
|
||||
- name: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
image: docker.io/springcloud/spring-cloud-kubernetes-client-secrets-event-reload
|
||||
imagePullPolicy: IfNotPresent
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/readiness
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/liveness
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-client-secrets-ingress-event-reload
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
port:
|
||||
number: 8080
|
||||
@@ -0,0 +1,14 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
||||
<logger name="org.testcontainers" level="INFO"/>
|
||||
<logger name="com.github.dockerjava" level="WARN"/>
|
||||
</configuration>
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: event-reload
|
||||
namespace: default
|
||||
data:
|
||||
# from.properties.key=initial
|
||||
application.properties: |
|
||||
ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg==
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
name: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: spring-cloud-kubernetes-client-secrets-event-reload
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-kubernetes-fabric8-client-configmap-polling-reload</artifactId>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-fabric8-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-transport-httpclient5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<!-- build image in the 'package' phase, and ignore plain tests -->
|
||||
<!-- via maven-surefire-plugin::skipTests -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
|
||||
<imageBuilder>paketobuildpacks/builder</imageBuilder>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- run tests in the 'integration-tests' phase, one that is after 'package' (where we build the image) -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>${testsToRun}</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.configmap.polling.reload;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(ConfigMapProperties.class)
|
||||
public class ConfigMapApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ConfigMapApp.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.configmap.polling.reload;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@RestController
|
||||
public class ConfigMapController {
|
||||
|
||||
private final ConfigMapProperties properties;
|
||||
|
||||
public ConfigMapController(ConfigMapProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@GetMapping("/key")
|
||||
public String key() {
|
||||
return properties.getKey();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.configmap.polling.reload;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@ConfigurationProperties("from.properties")
|
||||
public class ConfigMapProperties {
|
||||
|
||||
private String key;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key1) {
|
||||
this.key = key1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
logging:
|
||||
level:
|
||||
root: DEBUG
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: poll-reload
|
||||
config:
|
||||
import: "kubernetes:"
|
||||
cloud:
|
||||
kubernetes:
|
||||
reload:
|
||||
enabled: true
|
||||
monitoring-config-maps: true
|
||||
strategy: shutdown
|
||||
mode: polling
|
||||
period: 5000
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.configmap.polling.reload;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.dsl.base.HasMetadataOperation;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Fabric8Utils;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class ConfigMapPollingReloadIT {
|
||||
|
||||
private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-configmap-polling-reload";
|
||||
|
||||
private static final String NAMESPACE = "default";
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
private static String deploymentName;
|
||||
|
||||
private static String serviceName;
|
||||
|
||||
private static String ingressName;
|
||||
|
||||
private static String configMapName;
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
K3S.start();
|
||||
Commons.validateImage(IMAGE_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
|
||||
Config config = Config.fromKubeconfig(K3S.getKubeConfigYaml());
|
||||
client = new DefaultKubernetesClient(config);
|
||||
Fabric8Utils.setUp(client, NAMESPACE);
|
||||
deployManifests();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() throws Exception {
|
||||
deleteManifests();
|
||||
Commons.cleanUp(IMAGE_NAME, K3S);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "raw", "unchecked" })
|
||||
@Test
|
||||
void test() {
|
||||
WebClient webClient = builder().baseUrl("localhost/key").build();
|
||||
String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec())
|
||||
.block();
|
||||
|
||||
// we first read the initial value from the configmap
|
||||
Assertions.assertEquals("initial", result);
|
||||
|
||||
// then deploy a new version of configmap
|
||||
// since we poll and have reload in place, the new property must be visible
|
||||
ConfigMap map = new ConfigMapBuilder()
|
||||
.withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("poll-reload").build())
|
||||
.withData(Map.of("application.properties", "from.properties.key=after-change")).build();
|
||||
|
||||
// the weird cast comes from :
|
||||
// https://github.com/fabric8io/kubernetes-client/issues/2445
|
||||
((HasMetadataOperation) client.configMaps().inNamespace("default").withName("poll-reload"))
|
||||
.createOrReplace(map);
|
||||
|
||||
await().timeout(Duration.ofSeconds(30)).until(() -> webClient.method(HttpMethod.GET).retrieve()
|
||||
.bodyToMono(String.class).retryWhen(retrySpec()).block().equals("after-change"));
|
||||
|
||||
}
|
||||
|
||||
private static void deleteManifests() {
|
||||
|
||||
try {
|
||||
|
||||
client.configMaps().inNamespace(NAMESPACE).withName(configMapName).delete();
|
||||
client.apps().deployments().inNamespace(NAMESPACE).withName(deploymentName).delete();
|
||||
client.services().inNamespace(NAMESPACE).withName(serviceName).delete();
|
||||
client.network().v1().ingresses().inNamespace(NAMESPACE).withName(ingressName).delete();
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void deployManifests() {
|
||||
|
||||
try {
|
||||
|
||||
ConfigMap configMap = client.configMaps().load(getConfigMap()).get();
|
||||
configMapName = configMap.getMetadata().getName();
|
||||
client.configMaps().create(configMap);
|
||||
|
||||
Deployment deployment = client.apps().deployments().load(getDeployment()).get();
|
||||
|
||||
String version = K8SUtils.getPomVersion();
|
||||
String currentImage = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(currentImage + ":" + version);
|
||||
|
||||
client.apps().deployments().inNamespace(NAMESPACE).create(deployment);
|
||||
deploymentName = deployment.getMetadata().getName();
|
||||
|
||||
Service service = client.services().load(getService()).get();
|
||||
serviceName = service.getMetadata().getName();
|
||||
client.services().inNamespace(NAMESPACE).create(service);
|
||||
|
||||
Ingress ingress = client.network().v1().ingresses().load(getIngress()).get();
|
||||
ingressName = ingress.getMetadata().getName();
|
||||
client.network().v1().ingresses().inNamespace(NAMESPACE).create(ingress);
|
||||
|
||||
Fabric8Utils.waitForDeployment(client,
|
||||
"spring-cloud-kubernetes-fabric8-client-configmap-deployment-polling-reload", NAMESPACE, 2, 600);
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static InputStream getService() {
|
||||
return Fabric8Utils.inputStream("service.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getDeployment() {
|
||||
return Fabric8Utils.inputStream("deployment.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getIngress() {
|
||||
return Fabric8Utils.inputStream("ingress.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getConfigMap() {
|
||||
return Fabric8Utils.inputStream("configmap.yaml");
|
||||
}
|
||||
|
||||
private WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
private RetryBackoffSpec retrySpec() {
|
||||
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: poll-reload
|
||||
namespace: default
|
||||
data:
|
||||
application.properties: |
|
||||
from.properties.key=initial
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-fabric8-client-configmap-deployment-polling-reload
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
spec:
|
||||
serviceAccountName: spring-cloud-kubernetes-serviceaccount
|
||||
containers:
|
||||
- name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
imagePullPolicy: IfNotPresent
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/readiness
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/liveness
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-fabric8-client-configmap-ingress-polling-reload
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
port:
|
||||
number: 8080
|
||||
@@ -0,0 +1,14 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
||||
<logger name="org.testcontainers" level="INFO"/>
|
||||
<logger name="com.github.dockerjava" level="WARN"/>
|
||||
</configuration>
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
name: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: spring-cloud-kubernetes-fabric8-client-configmap-polling-reload
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-kubernetes-fabric8-client-secrets-event-reload</artifactId>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-fabric8-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.docker-java</groupId>
|
||||
<artifactId>docker-java-transport-httpclient5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<!-- build image in the 'package' phase, and ignore plain tests -->
|
||||
<!-- via maven-surefire-plugin::skipTests -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
|
||||
<imageBuilder>paketobuildpacks/builder</imageBuilder>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- run tests in the 'integration-tests' phase, one that is after 'package' (where we build the image) -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>${testsToRun}</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.secrets.event.reload;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(SecretsProperties.class)
|
||||
public class SecretsApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SecretsApp.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.secrets.event.reload;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@RestController
|
||||
public class SecretsController {
|
||||
|
||||
private final SecretsProperties properties;
|
||||
|
||||
public SecretsController(SecretsProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@GetMapping("/key")
|
||||
public String key() {
|
||||
return properties.getKey();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.secrets.event.reload;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@ConfigurationProperties("from.properties")
|
||||
public class SecretsProperties {
|
||||
|
||||
private String key;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key1) {
|
||||
this.key = key1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
logging:
|
||||
level:
|
||||
root: DEBUG
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: event-reload
|
||||
config:
|
||||
import: "kubernetes:"
|
||||
cloud:
|
||||
kubernetes:
|
||||
reload:
|
||||
enabled: true
|
||||
monitoring-secrets: true
|
||||
strategy: shutdown
|
||||
mode: event
|
||||
secrets:
|
||||
enabled: true
|
||||
enable-api: true
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2013-2022 the original author or 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
|
||||
*
|
||||
* https://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.fabric8.secrets.event.reload;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.dsl.base.HasMetadataOperation;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Fabric8Utils;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class SecretsEventsReloadIT {
|
||||
|
||||
private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-secrets-event-reload";
|
||||
|
||||
private static final String NAMESPACE = "default";
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
private static String deploymentName;
|
||||
|
||||
private static String serviceName;
|
||||
|
||||
private static String ingressName;
|
||||
|
||||
private static String secretName;
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
K3S.start();
|
||||
Commons.validateImage(IMAGE_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
|
||||
Config config = Config.fromKubeconfig(K3S.getKubeConfigYaml());
|
||||
client = new DefaultKubernetesClient(config);
|
||||
Fabric8Utils.setUp(client, NAMESPACE);
|
||||
deployManifests();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() throws Exception {
|
||||
deleteManifests();
|
||||
Commons.cleanUp(IMAGE_NAME, K3S);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "raw", "unchecked" })
|
||||
@Test
|
||||
void test() {
|
||||
WebClient webClient = builder().baseUrl("localhost/key").build();
|
||||
String result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec())
|
||||
.block();
|
||||
|
||||
// we first read the initial value from the secret
|
||||
Assertions.assertEquals("initial", result);
|
||||
|
||||
// then deploy a new version of the secret
|
||||
// since we poll and have reload in place, the new property must be visible
|
||||
Secret secret = new SecretBuilder()
|
||||
.withMetadata(new ObjectMetaBuilder().withNamespace("default").withName("event-reload").build())
|
||||
.withData(Map.of("application.properties",
|
||||
Base64.getEncoder().encodeToString("from.properties.key=after-change".getBytes())))
|
||||
.build();
|
||||
|
||||
// the weird cast comes from :
|
||||
// https://github.com/fabric8io/kubernetes-client/issues/2445
|
||||
((HasMetadataOperation) client.secrets().inNamespace("default").withName("event-reload"))
|
||||
.createOrReplace(secret);
|
||||
|
||||
await().timeout(Duration.ofSeconds(120)).until(() -> webClient.method(HttpMethod.GET).retrieve()
|
||||
.bodyToMono(String.class).retryWhen(retrySpec()).block().equals("after-change"));
|
||||
|
||||
}
|
||||
|
||||
private static void deleteManifests() {
|
||||
|
||||
try {
|
||||
|
||||
client.secrets().inNamespace(NAMESPACE).withName(secretName).delete();
|
||||
client.apps().deployments().inNamespace(NAMESPACE).withName(deploymentName).delete();
|
||||
client.services().inNamespace(NAMESPACE).withName(serviceName).delete();
|
||||
client.network().v1().ingresses().inNamespace(NAMESPACE).withName(ingressName).delete();
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void deployManifests() {
|
||||
|
||||
try {
|
||||
|
||||
Secret configMap = client.secrets().load(getSecret()).get();
|
||||
secretName = configMap.getMetadata().getName();
|
||||
client.secrets().create(configMap);
|
||||
|
||||
Deployment deployment = client.apps().deployments().load(getDeployment()).get();
|
||||
|
||||
String version = K8SUtils.getPomVersion();
|
||||
String currentImage = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(currentImage + ":" + version);
|
||||
|
||||
client.apps().deployments().inNamespace(NAMESPACE).create(deployment);
|
||||
deploymentName = deployment.getMetadata().getName();
|
||||
|
||||
Service service = client.services().load(getService()).get();
|
||||
serviceName = service.getMetadata().getName();
|
||||
client.services().inNamespace(NAMESPACE).create(service);
|
||||
|
||||
Ingress ingress = client.network().v1().ingresses().load(getIngress()).get();
|
||||
ingressName = ingress.getMetadata().getName();
|
||||
client.network().v1().ingresses().inNamespace(NAMESPACE).create(ingress);
|
||||
|
||||
Fabric8Utils.waitForDeployment(client,
|
||||
"spring-cloud-kubernetes-fabric8-client-secrets-deployment-event-reload", NAMESPACE, 2, 600);
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static InputStream getService() {
|
||||
return Fabric8Utils.inputStream("service.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getDeployment() {
|
||||
return Fabric8Utils.inputStream("deployment.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getIngress() {
|
||||
return Fabric8Utils.inputStream("ingress.yaml");
|
||||
}
|
||||
|
||||
private static InputStream getSecret() {
|
||||
return Fabric8Utils.inputStream("secret.yaml");
|
||||
}
|
||||
|
||||
private WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
private RetryBackoffSpec retrySpec() {
|
||||
return Retry.fixedDelay(120, Duration.ofSeconds(1)).filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-fabric8-client-secrets-deployment-event-reload
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
spec:
|
||||
serviceAccountName: spring-cloud-kubernetes-serviceaccount
|
||||
containers:
|
||||
- name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
imagePullPolicy: IfNotPresent
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/readiness
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: 8080
|
||||
path: /actuator/health/liveness
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: spring-cloud-kubernetes-fabric8-client-secrets-ingress-event-reload
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
port:
|
||||
number: 8080
|
||||
@@ -0,0 +1,14 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
||||
<logger name="org.testcontainers" level="INFO"/>
|
||||
<logger name="com.github.dockerjava" level="WARN"/>
|
||||
</configuration>
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: event-reload
|
||||
namespace: default
|
||||
data:
|
||||
# from.properties.key=initial
|
||||
application.properties: |
|
||||
ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg==
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
name: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: spring-cloud-kubernetes-fabric8-client-secrets-event-reload
|
||||
type: ClusterIP
|
||||
Reference in New Issue
Block a user