From 527fe5766553599c5bc2aadc67c8d7b84c014486 Mon Sep 17 00:00:00 2001 From: wind57 Date: Mon, 22 Sep 2025 13:44:18 +0300 Subject: [PATCH 1/3] fix-2008: started work Signed-off-by: wind57 --- .../PollingReloadConfigMapAndSecretTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java 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..f7b3ade05c --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * Proves that this + * issue is fixed. + * @author wind57 + */ +class PollingReloadConfigMapAndSecretTest { +} From 66ee75c94c988c57bc6450aab78b490d6ef9186d Mon Sep 17 00:00:00 2001 From: wind57 Date: Mon, 22 Sep 2025 22:27:35 +0300 Subject: [PATCH 2/3] fix Signed-off-by: wind57 --- .../config/reload/ConfigReloadUtil.java | 9 - .../fabric8/config/reload_it/App.java | 32 ++++ .../PollingReloadConfigMapAndSecretTest.java | 174 +++++++++++++++++- .../polling-reload-configmap-and-secret.yaml | 24 +++ 4 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml 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/reload_it/App.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java new file mode 100644 index 0000000000..8bc9f0d593 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java @@ -0,0 +1,32 @@ +/* + * 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author wind57 + */ +@SpringBootApplication +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 index f7b3ade05c..1f2e525099 100644 --- 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 @@ -16,10 +16,182 @@ 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.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 + * 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 From 97e5eeb465bc3a5298c3673ca334b13080b434fe Mon Sep 17 00:00:00 2001 From: wind57 Date: Mon, 22 Sep 2025 22:56:47 +0300 Subject: [PATCH 3/3] fix test Signed-off-by: wind57 --- .../fabric8/config/example/App.java | 2 +- .../fabric8/config/reload_it/App.java | 32 ------------------- .../PollingReloadConfigMapAndSecretTest.java | 1 + 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java 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/App.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java deleted file mode 100644 index 8bc9f0d593..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/App.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author wind57 - */ -@SpringBootApplication -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 index 1f2e525099..4f117f2f4a 100644 --- 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 @@ -45,6 +45,7 @@ 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;