diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java index 1f63362b96..1f40ec783c 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java @@ -27,7 +27,6 @@ import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; -import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -111,10 +110,6 @@ else if (source instanceof MountConfigMapPropertySource mountConfigMapPropertySo // we know that the type is correct here managedSources.add((S) mountConfigMapPropertySource); } - else if (source instanceof SecretsPropertySource secretsPropertySource) { - // we know that the type is correct here - managedSources.add((S) secretsPropertySource); - } else if (source instanceof BootstrapPropertySource bootstrapPropertySource) { PropertySource propertySource = bootstrapPropertySource.getDelegate(); LOG.debug(() -> "bootstrap delegate class : " + propertySource.getClass()); @@ -125,10 +120,6 @@ else if (propertySource instanceof MountConfigMapPropertySource mountConfigMapPr // we know that the type is correct here managedSources.add((S) mountConfigMapPropertySource); } - else if (propertySource instanceof SecretsPropertySource secretsPropertySource) { - // we know that the type is correct here - managedSources.add((S) secretsPropertySource); - } } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java index 69e7a97131..c5ed535567 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java @@ -25,7 +25,7 @@ */ @EnableConfigurationProperties(GreetingProperties.class) @SpringBootApplication -class App { +public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java new file mode 100644 index 0000000000..4f117f2f4a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2012-present 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.config.reload_it; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.example.App; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Proves that + * this + * issue is fixed. + * + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.config.enabled=true", + "spring.cloud.bootstrap.name=polling-reload-configmap-and-secret", + "spring.main.cloud-platform=KUBERNETES", "spring.application.name=polling-reload-configmap-and-secret", + "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.client.namespace=spring-k8s", + "logging.level.org.springframework.cloud.kubernetes.commons.config.reload=debug" }, + classes = { PollingReloadConfigMapAndSecretTest.TestConfig.class, App.class }) +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class PollingReloadConfigMapAndSecretTest { + + private static final String NAMESPACE = "spring-k8s"; + + private static final AtomicBoolean STRATEGY_FOR_SECRET_CALLED = new AtomicBoolean(false); + + private static KubernetesClient mockClient; + + @Autowired + private ConfigurableEnvironment environment; + + @BeforeAll + static void beforeAll() { + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + // namespace: spring-k8s, name: secret-a + Map secretA = Collections.singletonMap("one", + Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8))); + createSecret("secret-a", secretA); + + // namespace: spring-k8s, name: secret-b + Map secretB = Collections.singletonMap("two", + Base64.getEncoder().encodeToString("b".getBytes(StandardCharsets.UTF_8))); + createSecret("secret-b", secretB); + + // namespace: spring-k8s, name: configmap-a + Map configMapA = Collections.singletonMap("one", "a"); + createConfigMap("configmap-a", configMapA); + + // namespace: spring-k8s, name: configmap-b + Map configMapB = Collections.singletonMap("two", "b"); + createConfigMap("configmap-b", configMapB); + + } + + @Test + void test(CapturedOutput output) { + + Set sources = environment.getPropertySources() + .stream() + .map(PropertySource::getName) + .collect(Collectors.toSet()); + assertThat(sources).contains("bootstrapProperties-configmap.configmap-b.spring-k8s", + "bootstrapProperties-configmap.configmap-a.spring-k8s", + "bootstrapProperties-secret.secret-b.spring-k8s", "bootstrapProperties-secret.secret-a.spring-k8s"); + + // 1. first, wait for a cycle where we see the configmaps as being the same + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> output.getOut() + .contains("Reloadable condition was not satisfied, reload will not be triggered")); + + // 2. then change a configmap, so the cycle seems them as different and triggers a + // reload + Map configMapA = Collections.singletonMap("one", "aa"); + replaceConfigMap("configmap-a", configMapA); + + // 3. reload is triggered + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(STRATEGY_FOR_SECRET_CALLED::get); + + } + + private static void createSecret(String name, Map data) { + mockClient.secrets() + .inNamespace(NAMESPACE) + .resource(new SecretBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .create(); + } + + private static void createConfigMap(String name, Map data) { + mockClient.configMaps() + .inNamespace(NAMESPACE) + .resource(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .create(); + } + + private static void replaceConfigMap(String name, Map data) { + mockClient.configMaps() + .inNamespace(NAMESPACE) + .resource(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .createOrReplace(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + Fabric8ConfigMapPropertySource.class, fabric8ConfigMapPropertySourceLocator, scheduler); + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, true, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(200), Set.of(NAMESPACE), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigurationUpdateStrategy secretConfigurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> STRATEGY_FOR_SECRET_CALLED.set(true)); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml new file mode 100644 index 0000000000..102672e48c --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml @@ -0,0 +1,24 @@ +spring: + application: + name: polling-reload-configmap-and-secret + cloud: + kubernetes: + reload: + enabled: true + monitoring-config-maps: true + monitoring-secrets: true + mode: polling + config: + namespace: spring-k8s + sources: + - name: configmap-a + - name: configmap-b + enable-api: true + include-profile-specific-sources: false + secrets: + namespace: spring-k8s + sources: + - name: secret-a + - name: secret-b + enable-api: true + include-profile-specific-sources: false