From 52d47e20cc5c22a6f886b427e96f6a76969fbbd5 Mon Sep 17 00:00:00 2001 From: wind57 Date: Sat, 29 Mar 2025 20:30:36 +0200 Subject: [PATCH 1/4] first Signed-off-by: wind57 --- .../config/reload/ConfigReloadUtil.java | 9 ++ .../config/reload/ConfigReloadUtilTests.java | 51 ++++++-- ...8sClientSecretMountBootstrapPollingIT.java | 118 ++++++++++++++++++ 3 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java 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 c8df2821..f066da4c 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,6 +27,7 @@ import org.apache.commons.logging.LogFactory; 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; @@ -110,6 +111,10 @@ public final class ConfigReloadUtil { // 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()); @@ -120,6 +125,10 @@ public final class ConfigReloadUtil { // 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-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java index 280d95f9..2d8430f7 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java @@ -27,6 +27,8 @@ import org.junit.jupiter.api.Test; import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MapPropertySource; @@ -127,8 +129,8 @@ class ConfigReloadUtilTests { MockEnvironment environment = new MockEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); propertySources.addFirst(new OneComposite()); - propertySources.addFirst(new PlainPropertySource("plain")); - propertySources.addFirst(new OneBootstrap(new EnumerablePropertySource<>("enumerable") { + propertySources.addFirst(new PlainPropertySource<>("plain")); + propertySources.addFirst(new OneBootstrap<>(new EnumerablePropertySource<>("enumerable") { @Override public String[] getPropertyNames() { return new String[0]; @@ -143,11 +145,35 @@ class ConfigReloadUtilTests { List result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class, environment); - Assertions.assertEquals(4, result.size()); + Assertions.assertEquals(3, result.size()); Assertions.assertEquals("b", result.get(0).getProperty("a")); Assertions.assertEquals("plain", result.get(1).getProperty("")); - Assertions.assertEquals("from-bootstrap", result.get(2).getProperty("")); - Assertions.assertEquals("from-inner-two-composite", result.get(3).getProperty("")); + Assertions.assertEquals("from-inner-two-composite", result.get(2).getProperty("")); + } + + @Test + void testSecretsPropertySource() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addFirst(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b")))); + + List result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class, + environment); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getProperty("a")).isEqualTo("b"); + } + + @Test + void testBootstrapSecretsPropertySource() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources + .addFirst(new OneBootstrap<>(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b"))))); + + List result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class, + environment); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getProperty("a")).isEqualTo("b"); } private static final class OneComposite extends CompositePropertySource { @@ -171,12 +197,12 @@ class ConfigReloadUtilTests { @Override public Collection> getPropertySources() { - return List.of(new PlainPropertySource("from-inner-two-composite")); + return List.of(new PlainPropertySource<>("from-inner-two-composite")); } } - private static final class PlainPropertySource extends PropertySource { + private static final class PlainPropertySource extends PropertySource { private PlainPropertySource(String name) { super(name); @@ -189,15 +215,18 @@ class ConfigReloadUtilTests { } - private static final class OneBootstrap extends BootstrapPropertySource { + private static final class OneBootstrap extends BootstrapPropertySource { - private OneBootstrap(EnumerablePropertySource delegate) { + private final EnumerablePropertySource delegate; + + private OneBootstrap(EnumerablePropertySource delegate) { super(delegate); + this.delegate = delegate; } @Override - public PropertySource getDelegate() { - return new PlainPropertySource("from-bootstrap"); + public PropertySource getDelegate() { + return delegate; } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java new file mode 100644 index 00000000..e4094c20 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2025 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.k8s.client.reload.it; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Secret; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.builder; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.retrySpec; + +/** + * @author wind57 + */ +class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; + + private static final String NAMESPACE = "default"; + + private static final K3sContainer K3S = Commons.container(); + + private static Util util; + + private static CoreV1Api coreV1Api; + + @BeforeAll + static void beforeAllLocal() throws Exception { + K3S.start(); + Commons.validateImage(IMAGE_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); + + util = new Util(K3S); + coreV1Api = new CoreV1Api(); + util.setUp(NAMESPACE); + manifestsSecret(Phase.CREATE, util, NAMESPACE, IMAGE_NAME); + } + + @AfterAll + static void afterAll() { + manifestsSecret(Phase.DELETE, util, NAMESPACE, IMAGE_NAME); + } + + /** + *
+	 *     - we have bootstrap enabled
+	 *     - we will 'locate' property sources from secrets.
+	 *     - there are no explicit secrets to search for, but what we will also read,
+	 *     	 is 'spring.cloud.kubernetes.secret.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see mount/deployment-with-secret.yaml)
+	 *     - we first assert that we are actually reading the path based source
+	 *
+	 *     - we then change the secret content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + @Test + void test() throws Exception { + WebClient webClient = builder().baseUrl("http://localhost:32321/secret").build(); + String result = webClient.method(HttpMethod.GET) + .retrieve() + .bodyToMono(String.class) + .retryWhen(retrySpec()) + .block(); + + // we first read the initial value from the configmap + assertThat(result).isEqualTo("initial"); + + // replace data in secret and wait for k8s to pick it up + // our polling will detect that and restart the app + V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); + secret.setData(Map.of("from.properties.secret.key", "as-mount-changed".getBytes(StandardCharsets.UTF_8))); + coreV1Api.replaceNamespacedSecret("secret-reload", NAMESPACE, secret, null, null, null, null); + + Commons.waitForLogStatement("Detected change in config maps/secrets, reload will be triggered", K3S, + IMAGE_NAME); + + await().atMost(Duration.ofSeconds(120)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> (Boolean) webClient.method(HttpMethod.GET) + .retrieve() + .bodyToMono(String.class) + .retryWhen(retrySpec()) + .block() + .equals("as-mount-changed")); + } + +} From a07196d216ef282602a72ed493bbc7f46dcb6203 Mon Sep 17 00:00:00 2001 From: wind57 Date: Sat, 29 Mar 2025 21:04:23 +0200 Subject: [PATCH 2/4] fix Signed-off-by: wind57 --- .../reload/it/K8sClientSecretMountBootstrapPollingIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java index e4094c20..c6b0f88f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java @@ -103,11 +103,11 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { coreV1Api.replaceNamespacedSecret("secret-reload", NAMESPACE, secret, null, null, null, null); Commons.waitForLogStatement("Detected change in config maps/secrets, reload will be triggered", K3S, - IMAGE_NAME); + IMAGE_NAME); await().atMost(Duration.ofSeconds(120)) .pollInterval(Duration.ofSeconds(1)) - .until(() -> (Boolean) webClient.method(HttpMethod.GET) + .until(() -> webClient.method(HttpMethod.GET) .retrieve() .bodyToMono(String.class) .retryWhen(retrySpec()) From c050c8042d0ae90f73b2d5c2a348ddec3ed13420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:01:03 +0000 Subject: [PATCH 3/4] Bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.2 to 3.5.3 Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.5.2 to 3.5.3. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.2...surefire-3.5.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../kubernetes-leader-election-example/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index c065c2ad..9ce2e1b9 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -75,7 +75,7 @@ maven-surefire-plugin - 3.5.2 + 3.5.3 maven-failsafe-plugin From e6adb11d278bcb073a6dbb627eecc0d630654ec4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:03:31 +0000 Subject: [PATCH 4/4] Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.2 to 3.5.3 Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.2 to 3.5.3. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.2...surefire-3.5.3) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../kubernetes-leader-election-example/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index c065c2ad..fcf48e30 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -79,7 +79,7 @@ maven-failsafe-plugin - 3.5.2 + 3.5.3