Reload functionality clean-up part 4 (#1036)

This commit is contained in:
erabii
2022-07-07 15:35:30 +03:00
committed by GitHub
parent 2ed17deada
commit 797af0209c
46 changed files with 1811 additions and 310 deletions

View File

@@ -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);
}
}

View File

@@ -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.");
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 {
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: event-reload
namespace: default
data:
# from.properties.key=initial
application.properties: |
ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg==

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: poll-reload
namespace: default
data:
application.properties: |
from.properties.key=initial

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: event-reload
namespace: default
data:
# from.properties.key=initial
application.properties: |
ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg==

View File

@@ -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