diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfiguration.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfiguration.java index 368010ad45..896defdceb 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfiguration.java @@ -23,7 +23,6 @@ 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.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.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 { - - /** - * 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()); - } + @Bean + @ConditionalOnBean(KubernetesClientConfigMapPropertySourceLocator.class) + @Conditional(PollingReloadDetectionMode.class) + public ConfigurationChangeDetector configMapPropertyChangePollingWatcher(ConfigReloadProperties properties, + ConfigurationUpdateStrategy strategy, + KubernetesClientConfigMapPropertySourceLocator configMapPropertySourceLocator, + AbstractEnvironment environment, TaskSchedulerWrapper 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 PollingConfigMapChangeDetector(environment, properties, strategy, + KubernetesClientConfigMapPropertySource.class, configMapPropertySourceLocator, + taskScheduler.getTaskScheduler()); + } - return new PollingSecretsChangeDetector(environment, properties, strategy, - KubernetesClientSecretsPropertySource.class, secretsPropertySourceLocator, - 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) { - /** - * 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 PollingSecretsChangeDetector(environment, properties, strategy, + KubernetesClientSecretsPropertySource.class, secretsPropertySourceLocator, + taskScheduler.getTaskScheduler()); + } - return new KubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment, properties, strategy, - configMapPropertySourceLocator, 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) { - /** - * 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 KubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment, properties, strategy, + configMapPropertySourceLocator, 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); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java index 82a8e811af..6df2af1d91 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedConfigMapChangeDetector.java @@ -25,12 +25,9 @@ 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 @@ */ 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 KubernetesClientEventBasedConfigMapChangeDetector(CoreV1Api coreV1Api, Co // 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 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() { + 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 void unwatch() { } 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."); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java index 45b2490ed3..213660ad92 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientEventBasedSecretsChangeDetector.java @@ -24,12 +24,9 @@ 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 @@ */ 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 KubernetesClientEventBasedSecretsChangeDetector(CoreV1Api coreV1Api, Conf // 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 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() { + 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 void onDelete(V1Secret obj, boolean deletedFinalStateUnknown) { } 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(); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadAutoConfiguration.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadAutoConfiguration.java index c64c621f1f..856119eb34 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadAutoConfiguration.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadAutoConfiguration.java @@ -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 @@ */ @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 TaskSchedulerWrapper taskScheduler() { @Bean @ConditionalOnMissingBean public ConfigurationUpdateStrategy configurationUpdateStrategy(ConfigReloadProperties properties, - ConfigurableApplicationContext ctx, @Autowired(required = false) RestartEndpoint restarter, - ContextRefresher refresher) { + ConfigurableApplicationContext ctx, Optional 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(); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java index e626c84d98..810ac2f925 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java @@ -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 propertySourceClass, @@ -56,21 +57,22 @@ public PollingConfigMapChangeDetector(AbstractEnvironment environment, ConfigRel 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 currentConfigMapSources = findPropertySources(propertySourceClass); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java index adc780a55a..6a3f017674 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java @@ -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 propertySourceClass, @@ -56,21 +57,22 @@ public PollingSecretsChangeDetector(AbstractEnvironment environment, ConfigReloa 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 currentSecretSources = locateMapPropertySources(this.propertySourceLocator, this.environment); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/condition/ConditionalOnKubernetesReloadEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/condition/ConditionalOnKubernetesReloadEnabled.java new file mode 100644 index 0000000000..821ff45444 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/condition/ConditionalOnKubernetesReloadEnabled.java @@ -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: + * spring.cloud.kubernetes.reload.enabled. + * + * @author wind57 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(name = "spring.cloud.kubernetes.reload.enabled", havingValue = "true") +public @interface ConditionalOnKubernetesReloadEnabled { + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretsWatcherChangeDetector.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretsWatcherChangeDetector.java index ffe7bf36cf..b4fd9f9817 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretsWatcherChangeDetector.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/SecretsWatcherChangeDetector.java @@ -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; diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8ConfigReloadAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8ConfigReloadAutoConfiguration.java index 37f1b4889c..7297f22e8c 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8ConfigReloadAutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8ConfigReloadAutoConfiguration.java @@ -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.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.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 @@ */ @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 { diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedConfigMapChangeDetector.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedConfigMapChangeDetector.java index 8b0edae9e1..be4843fd7d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedConfigMapChangeDetector.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedConfigMapChangeDetector.java @@ -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 watches; + private final KubernetesClient kubernetesClient; + + private final boolean monitoringConfigMaps; - private KubernetesClient kubernetesClient; + private SharedIndexInformer informer; public Fabric8EventBasedConfigMapChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties, KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy, @@ -60,80 +54,60 @@ public Fabric8EventBasedConfigMapChangeDetector(AbstractEnvironment environment, 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; - - if (this.properties.isMonitoringConfigMaps()) { - try { - String name = "config-maps-watch-event"; - this.watches.put(name, this.kubernetesClient.configMaps().watch(new Watcher() { - @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 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); - } - } + 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 (activated) { - this.log.info("Kubernetes event-based configMap change detector activated"); + @Override + public void onUpdate(ConfigMap oldConfigMap, ConfigMap newConfigMap) { + onEvent(newConfigMap); + } + + @Override + public void onDelete(ConfigMap configMap, boolean deletedFinalStateUnknown) { + onEvent(configMap); + } + + // 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 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(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedSecretsChangeDetector.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedSecretsChangeDetector.java index b3a4c5e21d..0147314e86 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedSecretsChangeDetector.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/reload/Fabric8EventBasedSecretsChangeDetector.java @@ -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 @@ */ public class Fabric8EventBasedSecretsChangeDetector extends ConfigurationChangeDetector { - private Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator; + private final Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator; + + private final KubernetesClient kubernetesClient; - private Map watches; + private final boolean monitorSecrets; - private KubernetesClient kubernetesClient; + private SharedIndexInformer informer; public Fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment, ConfigReloadProperties properties, KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy, @@ -60,79 +54,60 @@ public Fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment, C 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() { - @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 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); + } + @Override + public void onUpdate(Secret oldSecret, Secret newSecret) { + onEvent(newSecret); } - catch (Exception e) { - this.log.error("Error while closing the watch connection", e); + + @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(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/KubernetesConfigTestBase.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/KubernetesConfigTestBase.java index aa664a30f6..462d8b446b 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/KubernetesConfigTestBase.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/KubernetesConfigTestBase.java @@ -42,8 +42,7 @@ protected ConfigurableApplicationContext getContext() { } 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) diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index f2cc8862b0..2dd228f113 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -114,5 +114,8 @@ spring-cloud-kubernetes-client-reactive-discoveryclient-it spring-cloud-kubernetes-configuration-watcher-it spring-cloud-kubernetes-core-k8s-client-it - + spring-cloud-kubernetes-fabric8-client-configmap-polling-reload + spring-cloud-kubernetes-fabric8-client-secrets-event-reload + spring-cloud-kubernetes-client-secrets-event-reload + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml new file mode 100644 index 0000000000..b4215801ed --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/pom.xml @@ -0,0 +1,130 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-client-secrets-event-reload + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-config + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + io.kubernetes + client-java + + + io.kubernetes + client-java-extended + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + k3s + test + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + docker.io/springcloud/${project.artifactId}:${project.version} + paketobuildpacks/builder + + + + package + + build-image + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + ${testsToRun} + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java new file mode 100644 index 0000000000..72056a2300 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsApp.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java new file mode 100644 index 0000000000..7d40247bb6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsController.java @@ -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(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java new file mode 100644 index 0000000000..712eef589a --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsProperties.java @@ -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; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml new file mode 100644 index 0000000000..4356bd6ac6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/main/resources/application.yaml @@ -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 + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java new file mode 100644 index 0000000000..a1b32bb58a --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/secrets/event/reload/SecretsEventReloadIT.java @@ -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 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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml new file mode 100644 index 0000000000..74cdd1633f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml new file mode 100644 index 0000000000..7c74d61817 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..9e2848765f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/secret.yaml new file mode 100644 index 0000000000..bf3e730da4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: event-reload + namespace: default +data: + # from.properties.key=initial + application.properties: | + ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg== diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml new file mode 100644 index 0000000000..e1fb85db7b --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-secrets-event-reload/src/test/resources/service.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml new file mode 100644 index 0000000000..c0a6e6c82f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/pom.xml @@ -0,0 +1,107 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-fabric8-client-configmap-polling-reload + + + + + org.springframework.cloud + spring-cloud-kubernetes-fabric8-config + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + docker.io/springcloud/${project.artifactId}:${project.version} + paketobuildpacks/builder + + + + package + + build-image + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + ${testsToRun} + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java new file mode 100644 index 0000000000..8594a99cc6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapApp.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java new file mode 100644 index 0000000000..c59aa7fc93 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapController.java @@ -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(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java new file mode 100644 index 0000000000..0b6f7e61d7 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapProperties.java @@ -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; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application.yaml new file mode 100644 index 0000000000..baf0f12d35 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/main/resources/application.yaml @@ -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 + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java new file mode 100644 index 0000000000..0879ad9c91 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/polling/reload/ConfigMapPollingReloadIT.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml new file mode 100644 index 0000000000..194603083c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: poll-reload + namespace: default +data: + application.properties: | + from.properties.key=initial diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml new file mode 100644 index 0000000000..39f4572c07 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml new file mode 100644 index 0000000000..fd53e216c1 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..9e2848765f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml new file mode 100644 index 0000000000..2995e2b6b0 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-configmap-polling-reload/src/test/resources/service.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml new file mode 100644 index 0000000000..8b775e7c05 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/pom.xml @@ -0,0 +1,107 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 3.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-fabric8-client-secrets-event-reload + + + + + org.springframework.cloud + spring-cloud-kubernetes-fabric8-config + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + docker.io/springcloud/${project.artifactId}:${project.version} + paketobuildpacks/builder + + + + package + + build-image + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + ${testsToRun} + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java new file mode 100644 index 0000000000..cc49b3e4bc --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsApp.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java new file mode 100644 index 0000000000..5c3d3ddac7 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsController.java @@ -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(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java new file mode 100644 index 0000000000..c24bcb0dbf --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsProperties.java @@ -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; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/resources/application.yaml new file mode 100644 index 0000000000..d4b67d9cf2 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/main/resources/application.yaml @@ -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 + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java new file mode 100644 index 0000000000..6a84b0dfb5 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/secrets/event/reload/SecretsEventsReloadIT.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml new file mode 100644 index 0000000000..9145122921 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml new file mode 100644 index 0000000000..19e5d0df5c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..9e2848765f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml new file mode 100644 index 0000000000..bf3e730da4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: event-reload + namespace: default +data: + # from.properties.key=initial + application.properties: | + ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFsCg== diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml new file mode 100644 index 0000000000..239bc00bbb --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-secrets-event-reload/src/test/resources/service.yaml @@ -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