From 0e73b14478386c01489d5c950c8a314cdc3a3788 Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 20 Mar 2025 14:51:29 +0200 Subject: [PATCH 01/13] dirty Signed-off-by: wind57 --- .../src/main/resources/application-three.yaml | 6 +- .../src/main/resources/application-two.yaml | 6 +- .../src/main/resources/bootstrap-three.yaml | 11 +- .../src/main/resources/bootstrap-two.yaml | 11 +- .../configmap/K8sClientConfigMapReloadIT.java | 249 ------------------ .../K8sClientConfigMapReloadITUtil.java | 8 - .../K8sClientConfigMapEventTriggeredIT.java | 136 ++++++++++ ...sClientConfigMapLabelEventTriggeredIT.java | 185 +++++++++++++ .../it/configmap/K8sClientReloadBase.java | 115 ++++++++ .../test/resources/META-INF/spring.factories | 2 + .../tests/commons/native_client/Util.java | 30 +++ 11 files changed, 480 insertions(+), 279 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/configmap/K8sClientConfigMapEventTriggeredIT.java 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/configmap/K8sClientConfigMapLabelEventTriggeredIT.java 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/configmap/K8sClientReloadBase.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml index fc367fcf..72f6a44c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml @@ -1,7 +1,3 @@ -logging: - level: - root: DEBUG - spring: application: name: event-reload @@ -9,7 +5,7 @@ spring: kubernetes: reload: enabled: true - strategy: shutdown + strategy: refresh mode: event namespaces: - right diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml index 43ae2737..f8071e88 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-two.yaml @@ -1,7 +1,3 @@ -logging: - level: - root: DEBUG - spring: application: name: event-reload @@ -9,7 +5,7 @@ spring: kubernetes: reload: enabled: true - strategy: shutdown + strategy: refresh mode: event namespaces: - right diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml index 5c5f3605..af51a86e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml @@ -1,15 +1,14 @@ -logging: - level: - root: DEBUG - spring: cloud: kubernetes: config: sources: - - namespace: left - name: left-configmap - namespace: right name: right-configmap - namespace: right name: right-configmap-with-label + + # otherwise on context refresh we lose this property + # and test fails, since beans are not wired. + main: + cloud-platform: kubernetes diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml index 9265fce1..4c5a11e3 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-two.yaml @@ -1,13 +1,12 @@ -logging: - level: - root: DEBUG - spring: cloud: kubernetes: config: sources: - - namespace: left - name: left-configmap - namespace: right name: right-configmap + + # otherwise on context refresh we lose this property + # and test fails, since beans are not wired. + main: + cloud-platform: kubernetes diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java index 9814a906..be593d37 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java @@ -19,8 +19,6 @@ package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; import java.time.Duration; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; @@ -33,7 +31,6 @@ import io.kubernetes.client.openapi.models.V1Service; 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 org.springframework.cloud.kubernetes.integration.tests.commons.Commons; @@ -46,9 +43,7 @@ import static org.awaitility.Awaitility.await; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.BootstrapEnabledPollingReloadConfigMapMountDelegate.testBootstrapEnabledPollingReloadConfigMapMount; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.DataChangesInConfigMapReloadDelegate.testSimple; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.builder; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchOne; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchThree; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchTwo; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.retrySpec; import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.PollingReloadConfigMapMountDelegate.testPollingReloadConfigMapMount; @@ -92,259 +87,15 @@ class K8sClientConfigMapReloadIT { util.deleteNamespace("right"); } - /** - *
-	 *     - there are two namespaces : left and right
-	 *     - each of the namespaces has one configmap
-	 *     - we watch the "left" namespace, but make a change in the configmap in the right namespace
-	 *     - as such, no event is triggered and "left-configmap" stays as-is
-	 * 
- */ - @Test - void testInformFromOneNamespaceEventNotTriggered() throws Exception { - manifests(Phase.CREATE); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", "spring-k8s-client-reload"); - - WebClient webClient = builder().baseUrl("http://localhost/left").build(); - String result = webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - // we first read the initial value from the left-configmap - Assertions.assertEquals("left-initial", result); - - // then read the value from the right-configmap - webClient = builder().baseUrl("http://localhost/right").build(); - result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()).block(); - Assertions.assertEquals("right-initial", result); - - // then deploy a new version of right-configmap - V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace("right").name("right-configmap")) - .withData(Map.of("right.value", "right-after-change")) - .build(); - - replaceConfigMap(rightConfigMapAfterChange, "right-configmap"); - - // wait dummy for 5 seconds - LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5)); - - webClient = builder().baseUrl("http://localhost/left").build(); - result = webClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec()).block(); - // left configmap has not changed, no restart of app has happened - Assertions.assertEquals("left-initial", result); - - testAllOther(); - - } - // since we patch each deployment with "replace" strategy, any of the above can be // commented out and debugged individually. private void testAllOther() throws Exception { - testInformFromOneNamespaceEventTriggered(); - testInform(); - testInformFromOneNamespaceEventTriggeredSecretsDisabled(); testSimple(DOCKER_IMAGE, DEPLOYMENT_NAME, K3S); testPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); testBootstrapEnabledPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); } - /** - *
-	 *     - there are two namespaces : left and right
-	 *     - each of the namespaces has one configmap
-	 *     - we watch the "right" namespace and make a change in the configmap in the same namespace
-	 *     - as such, event is triggered and we see the updated value
-	 * 
- */ - void testInformFromOneNamespaceEventTriggered() throws Exception { - recreateConfigMaps(); - patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", DEPLOYMENT_NAME); - - // read the value from the right-configmap - WebClient webClient = builder().baseUrl("http://localhost/right").build(); - String result = webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-initial", result); - - // then deploy a new version of right-configmap - V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace("right").name("right-configmap")) - .withData(Map.of("right.value", "right-after-change")) - .build(); - - replaceConfigMap(rightConfigMapAfterChange, "right-configmap"); - - String[] resultAfterChange = new String[1]; - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/right").build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - resultAfterChange[0] = innerResult; - return innerResult != null; - }); - Assertions.assertEquals("right-after-change", resultAfterChange[0]); - } - - /** - *
-	*     - there are two namespaces : left and right (though we do not care about the left one)
-	*     - left has one configmap : left-configmap
-	*     - right has two configmaps: right-configmap, right-configmap-with-label
-	*     - we watch the "right" namespace, but enable tagging; which means that only
-	*       right-configmap-with-label triggers changes.
-	* 
- */ - void testInform() throws Exception { - recreateConfigMaps(); - V1ConfigMap rightWithLabelConfigMap = (V1ConfigMap) util.yaml("right-configmap-with-label.yaml"); - util.createAndWait("right", rightWithLabelConfigMap, null); - patchTwo(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); - - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", DEPLOYMENT_NAME); - - // read the initial value from the right-configmap - WebClient rightWebClient = builder().baseUrl("http://localhost/right").build(); - String rightResult = rightWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-initial", rightResult); - - // then read the initial value from the right-with-label-configmap - WebClient rightWithLabelWebClient = builder().baseUrl("http://localhost/with-label").build(); - String rightWithLabelResult = rightWithLabelWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-with-label-initial", rightWithLabelResult); - - // then deploy a new version of right-configmap - V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace("right").name("right-configmap")) - .withData(Map.of("right.value", "right-after-change")) - .build(); - - replaceConfigMap(rightConfigMapAfterChange, "right-configmap"); - - // sleep for 5 seconds - LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5)); - - // nothing changes in our app, because we are watching only labeled configmaps - rightResult = rightWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-initial", rightResult); - - // then deploy a new version of right-with-label-configmap - V1ConfigMap rightWithLabelConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace("right").name("right-configmap-with-label")) - .withData(Map.of("right.with.label.value", "right-with-label-after-change")) - .build(); - - replaceConfigMap(rightWithLabelConfigMapAfterChange, "right-configmap-with-label"); - - // since we have changed a labeled configmap, app will restart and pick up the new - // value - String[] resultAfterChange = new String[1]; - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/with-label").build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - resultAfterChange[0] = innerResult; - return innerResult != null; - }); - Assertions.assertEquals("right-with-label-after-change", resultAfterChange[0]); - - // right-configmap now will see the new value also, but only because the other - // configmap has triggered the restart - rightResult = rightWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-after-change", rightResult); - util.deleteAndWait("right", rightWithLabelConfigMap, null); - } - - /** - *
-	 *     - there are two namespaces : left and right
-	 *     - each of the namespaces has one configmap
-	 *     - we watch the "right" namespace and make a change in the configmap in the same namespace
-	 *     - as such, event is triggered and we see the updated value
-	 * 
- */ - void testInformFromOneNamespaceEventTriggeredSecretsDisabled() throws Exception { - recreateConfigMaps(); - patchThree(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", DEPLOYMENT_NAME); - - // read the value from the right-configmap - WebClient webClient = builder().baseUrl("http://localhost/right").build(); - String result = webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - Assertions.assertEquals("right-initial", result); - - // then deploy a new version of right-configmap - V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace("right").name("right-configmap")) - .withData(Map.of("right.value", "right-after-change")) - .build(); - - replaceConfigMap(rightConfigMapAfterChange, "right-configmap"); - - String[] resultAfterChange = new String[1]; - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/right").build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - resultAfterChange[0] = innerResult; - return innerResult != null; - }); - Assertions.assertEquals("right-after-change", resultAfterChange[0]); - } - - private void recreateConfigMaps() { - V1ConfigMap leftConfigMap = (V1ConfigMap) util.yaml("left-configmap.yaml"); - V1ConfigMap rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); - - util.deleteAndWait("left", leftConfigMap, null); - util.deleteAndWait("right", rightConfigMap, null); - - util.createAndWait("left", leftConfigMap, null); - util.createAndWait("right", rightConfigMap, null); - } - private static void manifests(Phase phase) { try { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java index 2043b40e..f7e7197d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java @@ -401,14 +401,6 @@ final class K8sClientConfigMapReloadITUtil { } """; - static void patchOne(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); - } - - static void patchTwo(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_TWO, POD_LABELS); - } - static void patchThree(String deploymentName, String namespace, String imageName) { patchWithReplace(imageName, deploymentName, namespace, BODY_THREE, POD_LABELS); } 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/configmap/K8sClientConfigMapEventTriggeredIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapEventTriggeredIT.java new file mode 100644 index 00000000..55a420ff --- /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/configmap/K8sClientConfigMapEventTriggeredIT.java @@ -0,0 +1,136 @@ +/* + * 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.configmap; + +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +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.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.k8s.client.reload.App; +import org.springframework.cloud.kubernetes.k8s.client.reload.RightProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.TestPropertySource; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +@SpringBootTest(classes = { App.class, K8sClientConfigMapEventTriggeredIT.TestConfig.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.main.cloud-platform=kubernetes", "spring.profiles.active=two", + "spring.cloud.bootstrap.enabled=true", + "logging.level.org.springframework.cloud.kubernetes.client.config.reload=debug" }) +class K8sClientConfigMapEventTriggeredIT extends K8sClientReloadBase { + + private static final MockedStatic KUBERNETES_CLIENT_UTILS_MOCKED_STATIC = Mockito + .mockStatic(KubernetesClientUtils.class); + + private static V1ConfigMap rightConfigMap; + + @Autowired + private RightProperties rightProperties; + + @Autowired + private CoreV1Api coreV1Api; + + @BeforeAll + static void beforeAllLocal() { + + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient) + .thenReturn(apiClient()); + + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC + .when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(), + Mockito.any(KubernetesNamespaceProvider.class))) + .thenReturn(NAMESPACE_RIGHT); + + util.createNamespace(NAMESPACE_RIGHT); + rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); + util.createAndWait(NAMESPACE_RIGHT, rightConfigMap, null); + } + + @AfterAll + static void afterAllLocal() { + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.close(); + util.deleteAndWait(NAMESPACE_RIGHT, rightConfigMap, null); + util.deleteNamespace(NAMESPACE_RIGHT); + } + + /** + *
+	 *     - there is one namespace : right
+	 *     - namespaces has one configmap
+	 *     - we watch this namespace and make a change in the configmap
+	 *     - as such, event is triggered and we see the updated value
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + + assertReloadLogStatements("added configmap informer for namespace : right with filter : null", + "added secret informer for namespace", output); + + Assertions.assertThat(rightProperties.getValue()).isEqualTo("right-initial"); + + // then deploy a new version of right-configmap + V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap")) + .withData(Map.of("right.value", "right-after-change")) + .build(); + + replaceConfigMap(coreV1Api, rightConfigMapAfterChange); + + await().atMost(Duration.ofSeconds(60)) + .pollDelay(Duration.ofSeconds(1)) + .until(() -> output.getOut().contains("ConfigMap right-configmap was updated in namespace right")); + + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> rightProperties.getValue().equals("right-after-change")); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + ApiClient client() { + return apiClient(); + } + + } + +} 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/configmap/K8sClientConfigMapLabelEventTriggeredIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapLabelEventTriggeredIT.java new file mode 100644 index 00000000..f16eda4f --- /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/configmap/K8sClientConfigMapLabelEventTriggeredIT.java @@ -0,0 +1,185 @@ +/* + * 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.configmap; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +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.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.k8s.client.reload.App; +import org.springframework.cloud.kubernetes.k8s.client.reload.RightProperties; +import org.springframework.cloud.kubernetes.k8s.client.reload.RightWithLabelsProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.TestPropertySource; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +@SpringBootTest(classes = { App.class, K8sClientConfigMapLabelEventTriggeredIT.TestConfig.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.main.cloud-platform=kubernetes", "spring.profiles.active=three", + "spring.cloud.bootstrap.enabled=true", + "logging.level.org.springframework.cloud.kubernetes.client.config.reload=debug" }) +class K8sClientConfigMapLabelEventTriggeredIT extends K8sClientReloadBase { + + private static final MockedStatic KUBERNETES_CLIENT_UTILS_MOCKED_STATIC = Mockito + .mockStatic(KubernetesClientUtils.class); + + private static V1ConfigMap rightConfigMap; + + private static V1ConfigMap rightConfigMapWithLabel; + + @Autowired + private RightProperties rightProperties; + + @Autowired + private RightWithLabelsProperties rightWithLabelsProperties; + + @Autowired + private CoreV1Api coreV1Api; + + @BeforeAll + static void beforeAllLocal() { + + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient) + .thenReturn(apiClient()); + + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC + .when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(), + Mockito.any(KubernetesNamespaceProvider.class))) + .thenReturn(NAMESPACE_RIGHT); + + util.createNamespace(NAMESPACE_RIGHT); + rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); + rightConfigMapWithLabel = (V1ConfigMap) util.yaml("right-configmap-with-label.yaml"); + util.createAndWait(NAMESPACE_RIGHT, rightConfigMap, null); + util.createAndWait(NAMESPACE_RIGHT, rightConfigMapWithLabel, null); + } + + @AfterAll + static void afterAllLocal() { + + KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.close(); + + util.deleteAndWait(NAMESPACE_RIGHT, rightConfigMap, null); + util.deleteAndWait(NAMESPACE_RIGHT, rightConfigMapWithLabel, null); + util.deleteNamespace(NAMESPACE_RIGHT); + } + + /** + *
+	 *     - we have one namespace : 'right'.
+	 *     - it has two configmaps : 'right-configmap' and 'right-configmap-with-label'
+	 *     - we watch 'right' namespace, but enable tagging; which means that only
+	 *       right-configmap-with-label triggers a change.
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + + assertReloadLogStatements( + "added configmap informer for namespace : " + + "right with filter : spring.cloud.kubernetes.config.informer.enabled=true", + "added secret informer for namespace", output); + + // read the initial value from the right-configmap + Assertions.assertThat(rightProperties.getValue()).isEqualTo("right-initial"); + + // read the initial value from the right-configmap-with-label + Assertions.assertThat(rightWithLabelsProperties.getValue()).isEqualTo("right-with-label-initial"); + + // then deploy a new version of right-configmap + V1ConfigMap rightConfigMapAfterChange = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap")) + .withData(Map.of("right.value", "right-after-change")) + .build(); + + replaceConfigMap(coreV1Api, rightConfigMapAfterChange); + + // sleep for 5 seconds + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5)); + Assertions.assertThat(rightProperties.getValue()).isEqualTo("right-initial"); + + // then deploy a new version of right-configmap-with-label + // but only add a label, this does not trigger a refresh + V1ConfigMap rightWithLabelConfigMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap-with-label") + .labels(Map.of("spring.cloud.kubernetes.config.informer.enabled", "true", + "custom.label", "spring-k8s"))) + .build(); + + await().atMost(Duration.ofSeconds(60)) + .pollDelay(Duration.ofSeconds(1)) + .until(() -> output.getOut().contains("data in configmap has not changed, will not reload")); + + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> rightWithLabelsProperties.getValue().equals("right-with-label-after-change")); + + // then deploy a new version of right-configmap-with-label + // that changes data also + V1ConfigMap rightWithLabelConfigMapAfterChange = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap-with-label") + .labels(Map.of("spring.cloud.kubernetes.config.informer.enabled", "true"))) + .withData(Map.of("right.with.label.value", "right-with-label-after-change")) + .build(); + + replaceConfigMap(coreV1Api, rightWithLabelConfigMapAfterChange); + + await().atMost(Duration.ofSeconds(60)) + .pollDelay(Duration.ofSeconds(1)) + .until(() -> output.getOut().contains("ConfigMap right-configmap-with-label was updated in namespace right")); + + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> rightWithLabelsProperties.getValue().equals("right-with-label-after-change")); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + ApiClient client() { + return apiClient(); + } + + } + +} 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/configmap/K8sClientReloadBase.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientReloadBase.java new file mode 100644 index 00000000..cc7b078c --- /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/configmap/K8sClientReloadBase.java @@ -0,0 +1,115 @@ +/* + * 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.configmap; + +import java.io.IOException; +import java.io.StringReader; +import java.time.Duration; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.util.Config; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; + +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +@ExtendWith(OutputCaptureExtension.class) +abstract class K8sClientReloadBase { + + protected static final String NAMESPACE_RIGHT = "right"; + + protected static final K3sContainer K3S = Commons.container(); + + protected static Util util; + + @BeforeAll + protected static void beforeAll() { + K3S.start(); + util = new Util(K3S); + } + + protected static ApiClient apiClient() { + String kubeConfigYaml = K3S.getKubeConfigYaml(); + + ApiClient client; + try { + client = Config.fromConfig(new StringReader(kubeConfigYaml)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + return new CoreV1Api(client).getApiClient(); + } + + /** + * assert that 'left' is present, and IFF it is, assert that 'right' is not + */ + static void assertReloadLogStatements(String left, String right, CapturedOutput output) { + + await().atMost(Duration.ofSeconds(30)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> { + boolean leftIsPresent = output.getOut().contains(left); + if (leftIsPresent) { + boolean rightIsPresent = output.getOut().contains(right); + return !rightIsPresent; + } + return false; + }); + } + + /** + * assert that 'left' is present, and IFF it is, assert that 'right' is not + */ + static void assertLogStatements(CapturedOutput output, String text) { + + await().atMost(Duration.ofSeconds(30)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> { + boolean leftIsPresent = output.getOut().contains(left); + if (leftIsPresent) { + boolean rightIsPresent = output.getOut().contains(right); + return !rightIsPresent; + } + return false; + }); + } + + protected static void replaceConfigMap(CoreV1Api api, V1ConfigMap configMap) { + try { + api.replaceNamespacedConfigMap(configMap.getMetadata().getName(), configMap.getMetadata().getNamespace(), + configMap, null, null, null, null); + } + catch (ApiException e) { + System.out.println(e.getResponseBody()); + throw new RuntimeException(e); + } + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories new file mode 100644 index 00000000..ac5985bb --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.kubernetes.k8s.client.reload.it.configmap.K8sClientConfigMapEventTriggeredIT.TestConfig diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java index 0c3cd190..9fc85e51 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java @@ -33,10 +33,12 @@ import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.apis.ApiregistrationV1Api; 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.apis.RbacAuthorizationV1Api; +import io.kubernetes.client.openapi.models.V1APIService; import io.kubernetes.client.openapi.models.V1ClusterRole; import io.kubernetes.client.openapi.models.V1ClusterRoleBinding; import io.kubernetes.client.openapi.models.V1ConfigMap; @@ -436,10 +438,38 @@ public final class Util { } public void deleteNamespace(String name) { + + // sometimes we get errors like : + + // "message": "Discovery failed for some groups, + // 1 failing: unable to retrieve the complete list of server APIs: + // metrics.k8s.io/v1beta1: stale GroupVersion discovery: metrics.k8s.io/v1beta1" + + // but even when it works OK, the finalizers are slowing down the deletion + ApiregistrationV1Api apiInstance = new ApiregistrationV1Api(coreV1Api.getApiClient()); + List apiServices; try { + apiServices = apiInstance.listAPIService(null, null, null, null, null, null, null, null, null, null, null) + .getItems(); + + apiServices.stream() + .map(apiService -> apiService.getMetadata().getName()) + .filter(apiServiceName -> apiServiceName.contains("metrics.k8s.io")) + .findFirst() + .ifPresent(apiServiceName -> { + try { + apiInstance.deleteAPIService(apiServiceName, null, null, null, null, null, null); + } + catch (ApiException e) { + System.out.println(e.getResponseBody()); + throw new RuntimeException(e); + } + }); + coreV1Api.deleteNamespace(name, null, null, null, null, null, null); } catch (ApiException e) { + System.out.println(e.getResponseBody()); throw new RuntimeException(e); } From 7c441dfb2b3b2ab6114cc1db14a3ac2814db02dd Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 21 Mar 2025 14:54:17 +0200 Subject: [PATCH 02/13] dirty Signed-off-by: wind57 --- ...ric8ConfigMapMountPollingBootstrapIT.java} | 4 +- .../k8s/client/reload/Controller.java | 37 +- .../k8s/client/reload/SecretsController.java | 39 -- .../src/main/resources/application-mount.yaml | 2 +- .../src/main/resources/application-one.yaml | 16 - .../resources/application-with-bootstrap.yaml | 2 +- .../src/main/resources/bootstrap-one.yaml | 13 - .../DataChangesInConfigMapReloadDelegate.java | 137 ------ .../configmap/K8sClientConfigMapReloadIT.java | 136 ------ .../K8sClientConfigMapReloadITUtil.java | 445 ------------------ .../PollingReloadConfigMapMountDelegate.java | 100 ---- .../K8sClientConfigMapEventTriggeredIT.java | 2 +- ...sClientConfigMapLabelEventTriggeredIT.java | 20 +- .../K8sClientConfigMapMountPollingIT.java} | 89 ++-- .../{configmap => }/K8sClientReloadBase.java | 75 +-- ...8sClientSecretMountBootstrapPollingIT.java | 125 +++++ .../DataChangesInSecretsReloadDelegate.java | 126 ----- .../secret/K8sClientSecretsReloadIT.java | 159 ------- .../secret/K8sClientSecretsReloadITUtil.java | 102 ---- .../src/test/resources/ingress.yaml | 16 - .../src/test/resources/left-configmap.yaml | 7 - .../configmap.yaml} | 2 +- .../deployment-with-secret.yaml} | 24 +- .../deployment.yaml} | 26 +- .../test/resources/{ => mount}/secret.yaml | 2 +- .../resources/mount/service-with-secret.yaml | 15 + .../src/test/resources/mount/service.yaml | 15 + .../src/test/resources/service.yaml | 14 - 28 files changed, 323 insertions(+), 1427 deletions(-) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/{Fabric8ConfigMapMountMountPollingBootstrapIT.java => Fabric8ConfigMapMountPollingBootstrapIT.java} (97%) delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/{configmap => }/K8sClientConfigMapEventTriggeredIT.java (99%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/{configmap => }/K8sClientConfigMapLabelEventTriggeredIT.java (92%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/{configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java => it/K8sClientConfigMapMountPollingIT.java} (54%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/{configmap => }/K8sClientReloadBase.java (60%) 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 delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/{configmap-mount.yaml => mount/configmap.yaml} (82%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/{deployment.yaml => mount/deployment-with-secret.yaml} (62%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/{deployment-with-secret.yaml => mount/deployment.yaml} (54%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/{ => mount}/secret.yaml (88%) create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service-with-secret.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountMountPollingBootstrapIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountPollingBootstrapIT.java similarity index 97% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountMountPollingBootstrapIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountPollingBootstrapIT.java index 71389a57..d99b0b11 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountMountPollingBootstrapIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/reload/Fabric8ConfigMapMountPollingBootstrapIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -44,7 +44,7 @@ import static org.springframework.cloud.kubernetes.integration.tests.commons.Com /** * @author wind57 */ -class Fabric8ConfigMapMountMountPollingBootstrapIT { +class Fabric8ConfigMapMountPollingBootstrapIT { private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-reload"; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java index e2eb6435..660ed1dc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java @@ -25,40 +25,23 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class Controller { - private final LeftProperties leftProperties; - - private final RightProperties rightProperties; - - private final RightWithLabelsProperties rightWithLabelsProperties; - private final ConfigMapProperties configMapProperties; - public Controller(LeftProperties leftProperties, RightProperties rightProperties, - RightWithLabelsProperties rightWithLabelsProperties, ConfigMapProperties configMapProperties) { - this.leftProperties = leftProperties; - this.rightProperties = rightProperties; - this.rightWithLabelsProperties = rightWithLabelsProperties; + private final SecretsProperties secretsProperties; + + public Controller(ConfigMapProperties configMapProperties, SecretsProperties secretsProperties) { this.configMapProperties = configMapProperties; + this.secretsProperties = secretsProperties; } - @GetMapping("/left") - public String left() { - return leftProperties.getValue(); - } - - @GetMapping("/right") - public String right() { - return rightProperties.getValue(); - } - - @GetMapping("/with-label") - public String witLabel() { - return rightWithLabelsProperties.getValue(); - } - - @GetMapping("/mount") + @GetMapping("/configmap") public String key() { return configMapProperties.getKey(); } + @GetMapping("/secret") + public String secret() { + return configMapProperties.getKey(); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java deleted file mode 100644 index b0e0597a..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsController.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.k8s.client.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-k8s-client-reload/src/main/resources/application-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml index cd1765f5..62548724 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml @@ -1,6 +1,6 @@ spring: application: - name: poll-reload-mount + name: poll-reload cloud: kubernetes: reload: diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml deleted file mode 100644 index 988bfb21..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-one.yaml +++ /dev/null @@ -1,16 +0,0 @@ -logging: - level: - root: DEBUG - -spring: - application: - name: event-reload - cloud: - kubernetes: - reload: - enabled: true - strategy: shutdown - mode: event - namespaces: - - left - monitoring-config-maps: true diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml index c997322d..220edfde 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml @@ -5,8 +5,8 @@ spring: kubernetes: reload: enabled: true - monitoring-config-maps: true strategy: shutdown mode: polling period: 5000 + monitoring-secrets: true diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml deleted file mode 100644 index 9265fce1..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-one.yaml +++ /dev/null @@ -1,13 +0,0 @@ -logging: - level: - root: DEBUG - -spring: - cloud: - kubernetes: - config: - sources: - - namespace: left - name: left-configmap - - namespace: right - name: right-configmap diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java deleted file mode 100644 index c527145d..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/DataChangesInConfigMapReloadDelegate.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2013-2023 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.configmap; - -import java.time.Duration; -import java.util.Map; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; -import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; -import org.junit.jupiter.api.Assertions; -import org.testcontainers.k3s.K3sContainer; - -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.http.HttpMethod; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -final class DataChangesInConfigMapReloadDelegate { - - private static final String NAMESPACE = "default"; - - private static final String LEFT_NAMESPACE = "left"; - - /** - *
-	 *     - configMap with no labels and data: left.value = left-initial exists in namespace left
-	 *     - we assert that we can read it correctly first, by invoking localhost/left
-	 *
-	 *     - then we change the configmap by adding a label, this in turn does not
-	 *       change the result of localhost/left, because the data has not changed.
-	 *
-	 *     - then we change data inside the config map, and we must see the updated value
-	 * 
- */ - static void testSimple(String dockerImage, String deploymentName, K3sContainer k3sContainer) { - - K8sClientConfigMapReloadITUtil.patchFour(deploymentName, NAMESPACE, dockerImage); - Commons.assertReloadLogStatements("added configmap informer for namespace", - "added secret informer for namespace", deploymentName); - - WebClient webClient = K8sClientConfigMapReloadITUtil.builder() - .baseUrl("http://localhost/" + LEFT_NAMESPACE) - .build(); - String result = webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) - .block(); - - // we first read the initial value from the left-configmap - Assertions.assertEquals("left-initial", result); - - // then deploy a new version of left-configmap, but without changing its data, - // only add a label - V1ConfigMap configMap = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")) - .withNamespace("left") - .withName("left-configmap") - .build()) - .withData(Map.of("left.value", "left-initial")) - .build(); - - replaceConfigMap(configMap); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = K8sClientConfigMapReloadITUtil.builder() - .baseUrl("http://localhost/" + LEFT_NAMESPACE) - .build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) - .block(); - return "left-initial".equals(innerResult); - }); - - String logs = K8sClientConfigMapReloadITUtil.logs(deploymentName, k3sContainer); - Assertions.assertTrue(logs.contains("ConfigMap left-configmap was updated in namespace left")); - Assertions.assertTrue(logs.contains("data in configmap has not changed, will not reload")); - - // change data - configMap = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")) - .withNamespace("left") - .withName("left-configmap") - .build()) - .withData(Map.of("left.value", "left-after-change")) - .build(); - - replaceConfigMap(configMap); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = K8sClientConfigMapReloadITUtil.builder() - .baseUrl("http://localhost/" + LEFT_NAMESPACE) - .build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) - .block(); - return "left-after-change".equals(innerResult); - }); - - } - - private static void replaceConfigMap(V1ConfigMap configMap) { - try { - new CoreV1Api().replaceNamespacedConfigMap("left-configmap", LEFT_NAMESPACE, configMap, null, null, null, - null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java deleted file mode 100644 index be593d37..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadIT.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.k8s.client.reload.configmap; - -import java.time.Duration; -import java.util.Map; -import java.util.Set; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1Service; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -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.awaitility.Awaitility.await; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.BootstrapEnabledPollingReloadConfigMapMountDelegate.testBootstrapEnabledPollingReloadConfigMapMount; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.DataChangesInConfigMapReloadDelegate.testSimple; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.builder; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.patchThree; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.K8sClientConfigMapReloadITUtil.retrySpec; -import static org.springframework.cloud.kubernetes.k8s.client.reload.configmap.PollingReloadConfigMapMountDelegate.testPollingReloadConfigMapMount; - -/** - * @author wind57 - */ -class K8sClientConfigMapReloadIT { - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; - - private static final String DEPLOYMENT_NAME = "spring-k8s-client-reload"; - - private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + Commons.pomVersion(); - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static CoreV1Api api; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - util.createNamespace("left"); - util.createNamespace("right"); - util.setUpClusterWide(NAMESPACE, Set.of("left", "right")); - util.setUp(NAMESPACE); - api = new CoreV1Api(); - } - - @AfterAll - static void afterAll() { - util.deleteClusterWide(NAMESPACE, Set.of("left", "right")); - manifests(Phase.DELETE); - util.deleteNamespace("left"); - util.deleteNamespace("right"); - } - - // since we patch each deployment with "replace" strategy, any of the above can be - // commented out and debugged individually. - private void testAllOther() throws Exception { - testSimple(DOCKER_IMAGE, DEPLOYMENT_NAME, K3S); - testPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); - testBootstrapEnabledPollingReloadConfigMapMount(DEPLOYMENT_NAME, K3S, util, DOCKER_IMAGE); - - } - - private static void manifests(Phase phase) { - - try { - - V1ConfigMap leftConfigMap = (V1ConfigMap) util.yaml("left-configmap.yaml"); - V1ConfigMap rightConfigMap = (V1ConfigMap) util.yaml("right-configmap.yaml"); - V1ConfigMap mountConfigMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - - V1Deployment deployment = (V1Deployment) util.yaml("deployment.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, mountConfigMap, null); - util.createAndWait("left", leftConfigMap, null); - util.createAndWait("right", rightConfigMap, null); - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - - if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, mountConfigMap, null); - util.deleteAndWait("left", leftConfigMap, null); - util.deleteAndWait("right", rightConfigMap, null); - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - - } - catch (Exception e) { - throw new RuntimeException(e); - } - - } - - private static void replaceConfigMap(V1ConfigMap configMap, String name) throws ApiException { - api.replaceNamespacedConfigMap(name, "right", configMap, null, null, null, null); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java deleted file mode 100644 index f7e7197d..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/K8sClientConfigMapReloadITUtil.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright 2013-2023 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.configmap; - -import java.time.Duration; -import java.util.Map; -import java.util.Objects; - -import org.testcontainers.containers.Container; -import org.testcontainers.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; - -/** - * @author wind57 - */ -final class K8sClientConfigMapReloadITUtil { - - private static final Map POD_LABELS = Map.of("app", "spring-k8s-client-reload"); - - private K8sClientConfigMapReloadITUtil() { - } - - private static final String BODY_ONE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "two" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_TWO = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "three" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_THREE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "two" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", - "value": "FALSE" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_FOUR = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "one" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED", - "value": "FALSE" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_FIVE = """ - { - "spec": { - "template": { - "spec": { - "volumes": [ - { - "configMap": { - "defaultMode": 420, - "name": "poll-reload-as-mount" - }, - "name": "config-map-volume" - } - ], - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "volumeMounts": [ - { - "mountPath": "/tmp", - "name": "config-map-volume" - } - ], - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "mount" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "FALSE" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", - "value": "DEBUG" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_SIX = """ - { - "spec": { - "template": { - "spec": { - "volumes": [ - { - "configMap": { - "defaultMode": 420, - "name": "poll-reload-as-mount" - }, - "name": "config-map-volume" - } - ], - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "volumeMounts": [ - { - "mountPath": "/tmp", - "name": "config-map-volume" - } - ], - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "with-bootstrap" - }, - { - "name": "SPRING_CLOUD_BOOTSTRAP_ENABLED", - "value": "TRUE" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS", - "value": "DEBUG" - } - ] - }] - } - } - } - } - """; - - static void patchThree(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_THREE, POD_LABELS); - } - - static void patchFour(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_FOUR, POD_LABELS); - } - - static void patchFive(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_FIVE, POD_LABELS); - } - - static void patchSix(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_SIX, POD_LABELS); - } - - static WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - static RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(120, Duration.ofSeconds(1)).filter(Objects::nonNull); - } - - static String logs(String appLabelValue, K3sContainer k3sContainer) { - try { - String appPodName = k3sContainer - .execInContainer("sh", "-c", - "kubectl get pods -l app=" + appLabelValue + " -o=name --no-headers | tr -d '\n'") - .getStdout(); - - Container.ExecResult execResult = k3sContainer.execInContainer("sh", "-c", - "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java deleted file mode 100644 index 3092e925..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/PollingReloadConfigMapMountDelegate.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2013-2023 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.configmap; - -import java.time.Duration; -import java.util.Map; - -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import org.junit.jupiter.api.Assertions; -import org.testcontainers.k3s.K3sContainer; - -import org.springframework.cloud.kubernetes.commons.config.Constants; -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -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.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -final class PollingReloadConfigMapMountDelegate { - - private PollingReloadConfigMapMountDelegate() { - - } - - /** - *
-	 *     - we have "spring.config.import: kubernetes", which means we will 'locate' property sources
-	 *       from config maps.
-	 *     - the property above means that at the moment we will be searching for config maps that only
-	 *       match the application name, in this specific test there is no such config map.
-	 *     - what we will also read, is 'spring.cloud.kubernetes.config.paths', which we have set to
-	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see BODY_FIVE)
-	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
-	 *
-	 *     - we then change the config map content, wait for k8s to pick it up and replace them
-	 *     - our polling will then detect that change, and trigger a reload.
-	 * 
- */ - static void testPollingReloadConfigMapMount(String deploymentName, K3sContainer k3sContainer, Util util, - String imageName) throws Exception { - - K8sClientConfigMapReloadITUtil.patchFive(deploymentName, "default", imageName); - - // (1) - Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", k3sContainer, - deploymentName); - - // (2) - Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", k3sContainer, - deploymentName); - - // (3) - WebClient webClient = K8sClientConfigMapReloadITUtil.builder().baseUrl("http://localhost/mount").build(); - String result = webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) - .block(); - - // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); - - // replace data in configmap and wait for k8s to pick it up - // our polling will detect that and restart the app - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=as-mount-changed")); - new CoreV1Api().replaceNamespacedConfigMap("poll-reload-as-mount", "default", configMap, null, null, null, - null); - - await().timeout(Duration.ofSeconds(180)) - .until(() -> webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) - .block() - .equals("as-mount-changed")); - - } - -} 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/configmap/K8sClientConfigMapEventTriggeredIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapEventTriggeredIT.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapEventTriggeredIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapEventTriggeredIT.java index 55a420ff..1831ca4a 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapEventTriggeredIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapEventTriggeredIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.k8s.client.reload.it.configmap; +package org.springframework.cloud.kubernetes.k8s.client.reload.it; import java.time.Duration; import java.util.Map; 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/configmap/K8sClientConfigMapLabelEventTriggeredIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapLabelEventTriggeredIT.java similarity index 92% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapLabelEventTriggeredIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapLabelEventTriggeredIT.java index f16eda4f..303ecbd3 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientConfigMapLabelEventTriggeredIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapLabelEventTriggeredIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.k8s.client.reload.it.configmap; +package org.springframework.cloud.kubernetes.k8s.client.reload.it; import java.time.Duration; import java.util.Map; @@ -139,23 +139,28 @@ class K8sClientConfigMapLabelEventTriggeredIT extends K8sClientReloadBase { // then deploy a new version of right-configmap-with-label // but only add a label, this does not trigger a refresh V1ConfigMap rightWithLabelConfigMap = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap-with-label") - .labels(Map.of("spring.cloud.kubernetes.config.informer.enabled", "true", - "custom.label", "spring-k8s"))) + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT) + .name("right-configmap-with-label") + .labels(Map.of("spring.cloud.kubernetes.config.informer.enabled", "true", "custom.label", + "spring-k8s"))) + .withData(Map.of("right.with.label.value", "right-with-label-initial")) .build(); + replaceConfigMap(coreV1Api, rightWithLabelConfigMap); + await().atMost(Duration.ofSeconds(60)) .pollDelay(Duration.ofSeconds(1)) .until(() -> output.getOut().contains("data in configmap has not changed, will not reload")); await().atMost(Duration.ofSeconds(60)) .pollInterval(Duration.ofSeconds(1)) - .until(() -> rightWithLabelsProperties.getValue().equals("right-with-label-after-change")); + .until(() -> rightWithLabelsProperties.getValue().equals("right-with-label-initial")); // then deploy a new version of right-configmap-with-label // that changes data also V1ConfigMap rightWithLabelConfigMapAfterChange = new V1ConfigMapBuilder() - .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT).name("right-configmap-with-label") + .withMetadata(new V1ObjectMeta().namespace(NAMESPACE_RIGHT) + .name("right-configmap-with-label") .labels(Map.of("spring.cloud.kubernetes.config.informer.enabled", "true"))) .withData(Map.of("right.with.label.value", "right-with-label-after-change")) .build(); @@ -164,7 +169,8 @@ class K8sClientConfigMapLabelEventTriggeredIT extends K8sClientReloadBase { await().atMost(Duration.ofSeconds(60)) .pollDelay(Duration.ofSeconds(1)) - .until(() -> output.getOut().contains("ConfigMap right-configmap-with-label was updated in namespace right")); + .until(() -> output.getOut() + .contains("ConfigMap right-configmap-with-label was updated in namespace right")); await().atMost(Duration.ofSeconds(60)) .pollInterval(Duration.ofSeconds(1)) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java similarity index 54% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java index 358ae43f..8a9d9bc0 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/configmap/BootstrapEnabledPollingReloadConfigMapMountDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -14,92 +14,111 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.k8s.client.reload.configmap; +package org.springframework.cloud.kubernetes.k8s.client.reload.it; import java.time.Duration; import java.util.Map; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; -import org.junit.jupiter.api.Assertions; +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.commons.config.Constants; 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 */ -final class BootstrapEnabledPollingReloadConfigMapMountDelegate { +class K8sClientConfigMapMountPollingIT 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); + manifests(Phase.CREATE, util, NAMESPACE, IMAGE_NAME); + } + + @AfterAll + static void afterAll() { + manifests(Phase.DELETE, util, NAMESPACE, IMAGE_NAME); + } + /** *
-	 *     - we have bootstrap enabled, which means we will 'locate' property sources
-	 *       from config maps.
+	 *     - we have bootstrap disabled
+	 *     - we will 'locate' property sources from config maps.
 	 *     - there are no explicit config maps to search for, but what we will also read,
 	 *     	 is 'spring.cloud.kubernetes.config.paths', which we have set to
 	 *     	 '/tmp/application.properties'
-	 *       in this test. That is populated by the volumeMounts (see deployment-mount.yaml)
+	 *       in this test. That is populated by the volumeMounts (see mount/deployment.yaml)
 	 *     - we first assert that we are actually reading the path based source via (1), (2) and (3).
 	 *
 	 *     - we then change the config map content, wait for k8s to pick it up and replace them
 	 *     - our polling will then detect that change, and trigger a reload.
 	 * 
*/ - static void testBootstrapEnabledPollingReloadConfigMapMount(String deploymentName, K3sContainer k3sContainer, - Util util, String imageName) throws Exception { - - recreateMountConfigMap(util); - K8sClientConfigMapReloadITUtil.patchSix(deploymentName, "default", imageName); - + @Test + void test() throws Exception { // (1) - Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", k3sContainer, - deploymentName); - + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", K3S, IMAGE_NAME); // (2) - Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", k3sContainer, - deploymentName); - + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", K3S, + IMAGE_NAME); // (3) - WebClient webClient = K8sClientConfigMapReloadITUtil.builder().baseUrl("http://localhost/mount").build(); + WebClient webClient = builder().baseUrl("http://localhost:32321/configmap").build(); String result = webClient.method(HttpMethod.GET) .retrieve() .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) + .retryWhen(retrySpec()) .block(); // we first read the initial value from the configmap - Assertions.assertEquals("as-mount-initial", result); + assertThat(result).isEqualTo("as-mount-initial"); // replace data in configmap and wait for k8s to pick it up // our polling will detect that and restart the app - V1ConfigMap configMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); + V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/configmap.yaml"); configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=as-mount-changed")); - new CoreV1Api().replaceNamespacedConfigMap("poll-reload-as-mount", NAMESPACE, configMap, null, null, null, - null); + coreV1Api.replaceNamespacedConfigMap("poll-reload", NAMESPACE, configMap, null, null, null, null); - await().timeout(Duration.ofSeconds(180)) + 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(() -> webClient.method(HttpMethod.GET) .retrieve() .bodyToMono(String.class) - .retryWhen(K8sClientConfigMapReloadITUtil.retrySpec()) + .retryWhen(retrySpec()) .block() .equals("as-mount-changed")); - - } - - private static void recreateMountConfigMap(Util util) { - V1ConfigMap mountConfigMap = (V1ConfigMap) util.yaml("configmap-mount.yaml"); - - util.deleteAndWait("default", mountConfigMap, null); - util.createAndWait("default", mountConfigMap, null); } } 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/configmap/K8sClientReloadBase.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java similarity index 60% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientReloadBase.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java index cc7b078c..5655ab7f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/configmap/K8sClientReloadBase.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.k8s.client.reload.it.configmap; +package org.springframework.cloud.kubernetes.k8s.client.reload.it; import java.io.IOException; import java.io.StringReader; @@ -24,6 +24,9 @@ import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.util.Config; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,6 +35,7 @@ import org.testcontainers.k3s.K3sContainer; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; 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 static org.testcontainers.shaded.org.awaitility.Awaitility.await; @@ -72,33 +76,14 @@ abstract class K8sClientReloadBase { */ static void assertReloadLogStatements(String left, String right, CapturedOutput output) { - await().atMost(Duration.ofSeconds(30)) - .pollInterval(Duration.ofSeconds(1)) - .until(() -> { - boolean leftIsPresent = output.getOut().contains(left); - if (leftIsPresent) { - boolean rightIsPresent = output.getOut().contains(right); - return !rightIsPresent; - } - return false; - }); - } - - /** - * assert that 'left' is present, and IFF it is, assert that 'right' is not - */ - static void assertLogStatements(CapturedOutput output, String text) { - - await().atMost(Duration.ofSeconds(30)) - .pollInterval(Duration.ofSeconds(1)) - .until(() -> { - boolean leftIsPresent = output.getOut().contains(left); - if (leftIsPresent) { - boolean rightIsPresent = output.getOut().contains(right); - return !rightIsPresent; - } - return false; - }); + await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean leftIsPresent = output.getOut().contains(left); + if (leftIsPresent) { + boolean rightIsPresent = output.getOut().contains(right); + return !rightIsPresent; + } + return false; + }); } protected static void replaceConfigMap(CoreV1Api api, V1ConfigMap configMap) { @@ -112,4 +97,38 @@ abstract class K8sClientReloadBase { } } + protected static void manifests(Phase phase, Util util, String namespace, String imageName) { + + V1Deployment deployment = (V1Deployment) util.yaml("mount/deployment.yaml"); + V1Service service = (V1Service) util.yaml("mount/service.yaml"); + V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/configmap.yaml"); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(namespace, configMap, null); + util.createAndWait(namespace, imageName, deployment, service, null, true); + } + else { + util.deleteAndWait(namespace, configMap, null); + util.deleteAndWait(namespace, deployment, service, null); + } + + } + + protected static void manifestsSecret(Phase phase, Util util, String namespace, String imageName) { + + V1Deployment deployment = (V1Deployment) util.yaml("mount/deployment-with-secret.yaml"); + V1Service service = (V1Service) util.yaml("mount/service-with-secret.yaml"); + V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(namespace, null, secret); + util.createAndWait(namespace, imageName, deployment, service, null, true); + } + else { + util.deleteAndWait(namespace, null, secret); + util.deleteAndWait(namespace, deployment, service, null); + } + + } + } 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..87568eb0 --- /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,125 @@ +/* + * 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.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +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.commons.config.Constants; +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 via (1), (2) and (3).
+	 *
+	 *     - 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 { + // (1) + Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", K3S, IMAGE_NAME); + // (2) + Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", K3S, + IMAGE_NAME); + // (3) + 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("as-mount-initial"); + + // replace data in configmap and wait for k8s to pick it up + // our polling will detect that and restart the app + V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/secret.yaml"); + configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=as-mount-changed")); + coreV1Api.replaceNamespacedConfigMap("poll-reload", NAMESPACE, configMap, null, null, null, null); + + System.out.println("Waiting for reload change to be observed"); + 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(() -> webClient.method(HttpMethod.GET) + .retrieve() + .bodyToMono(String.class) + .retryWhen(retrySpec()) + .block() + .equals("as-mount-changed")); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java deleted file mode 100644 index ba0ab2fa..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/DataChangesInSecretsReloadDelegate.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2013-2023 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.secret; - -import java.time.Duration; -import java.util.Map; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.openapi.models.V1SecretBuilder; -import org.junit.jupiter.api.Assertions; -import org.testcontainers.k3s.K3sContainer; - -import org.springframework.cloud.kubernetes.commons.config.Constants; -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.http.HttpMethod; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.awaitility.Awaitility.await; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.builder; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.retrySpec; - -final class DataChangesInSecretsReloadDelegate { - - private static final String NAMESPACE = "default"; - - /** - *
-	 *     - secret with no labels and data: from.properties.key = initial exists in namespace default
-	 *     - we assert that we can read it correctly first, by invoking localhost/key.
-	 *
-	 *     - then we change the secret by adding a label, this in turn does not
-	 *       change the result of localhost/key, because the data has not changed.
-	 *
-	 *     - then we change data inside the secret, and we must see the updated value.
-	 * 
- */ - static void testDataChangesInSecretsReload(K3sContainer k3sContainer, String deploymentName) { - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", deploymentName); - - WebClient webClient = builder().baseUrl("http://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 left-configmap, but without changing its data, - // only add a label - V1Secret secret = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")) - .withNamespace(NAMESPACE) - .withName("event-reload") - .build()) - .withData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=initial".getBytes())) - .build(); - - replaceSecret(secret, "event-reload"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - return "initial".equals(innerResult); - }); - - Commons.waitForLogStatement("Secret event-reload was updated in namespace default", k3sContainer, - deploymentName); - Commons.waitForLogStatement("data in secret has not changed, will not reload", k3sContainer, deploymentName); - - // change data - secret = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("new-label", "abc")) - .withNamespace(NAMESPACE) - .withName("event-reload") - .build()) - .withData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=change-initial".getBytes())) - .build(); - - replaceSecret(secret, "event-reload"); - - await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(90)).until(() -> { - WebClient innerWebClient = builder().baseUrl("http://localhost/key").build(); - String innerResult = innerWebClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - return "change-initial".equals(innerResult); - }); - - } - - private static void replaceSecret(V1Secret secret, String name) { - try { - new CoreV1Api().replaceNamespacedSecret(name, NAMESPACE, secret, null, null, null, null); - } - catch (ApiException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java deleted file mode 100644 index 91f83032..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadIT.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2013-2023 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.secret; - -import java.time.Duration; -import java.util.Map; - -import io.kubernetes.client.openapi.apis.CoreV1Api; -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.BeforeAll; -import org.junit.jupiter.api.Test; -import org.testcontainers.k3s.K3sContainer; - -import org.springframework.cloud.kubernetes.commons.config.Constants; -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.awaitility.Awaitility.await; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.DataChangesInSecretsReloadDelegate.testDataChangesInSecretsReload; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.builder; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.patchOne; -import static org.springframework.cloud.kubernetes.k8s.client.reload.secret.K8sClientSecretsReloadITUtil.retrySpec; - -/** - * @author wind57 - */ -class K8sClientSecretsReloadIT { - - private static final String PROPERTY_URL = "http://localhost:80/key"; - - private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; - - private static final String NAMESPACE = "default"; - - private static final String DEPLOYMENT_NAME = "spring-k8s-client-reload"; - - private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + Commons.pomVersion(); - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - private static CoreV1Api coreV1Api; - - @BeforeAll - static void setup() throws Exception { - K3S.start(); - Commons.validateImage(IMAGE_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); - util = new Util(K3S); - coreV1Api = new CoreV1Api(); - util.setUp(NAMESPACE); - configK8sClientIt(Phase.CREATE); - } - - @AfterAll - static void afterAll() { - configK8sClientIt(Phase.DELETE); - } - - @Test - void testSecretReload() throws Exception { - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", DEPLOYMENT_NAME); - testSecretEventReload(); - - testAllOther(); - } - - private void testAllOther() throws Exception { - recreateSecret(); - patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); - testSecretReloadConfigDisabled(); - - recreateSecret(); - patchOne(DEPLOYMENT_NAME, NAMESPACE, DOCKER_IMAGE); - testDataChangesInSecretsReload(K3S, DEPLOYMENT_NAME); - } - - void testSecretReloadConfigDisabled() throws Exception { - Commons.assertReloadLogStatements("added secret informer for namespace", - "added configmap informer for namespace", DEPLOYMENT_NAME); - testSecretEventReload(); - } - - void testSecretEventReload() throws Exception { - - WebClient.Builder builder = builder(); - WebClient secretClient = builder.baseUrl(PROPERTY_URL).build(); - - await().timeout(Duration.ofSeconds(120)) - .pollInterval(Duration.ofSeconds(2)) - .until(() -> secretClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block() - .equals("initial")); - - V1Secret v1Secret = (V1Secret) util.yaml("secret.yaml"); - Map secretData = v1Secret.getData(); - secretData.replace(Constants.APPLICATION_PROPERTIES, "from.properties.key: after-change".getBytes()); - v1Secret.setData(secretData); - coreV1Api.replaceNamespacedSecret("event-reload", NAMESPACE, v1Secret, null, null, null, null); - - 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 void recreateSecret() { - V1Secret secret = (V1Secret) util.yaml("secret.yaml"); - util.deleteAndWait(NAMESPACE, null, secret); - util.createAndWait(NAMESPACE, null, secret); - } - - private static void configK8sClientIt(Phase phase) { - V1Deployment deployment = (V1Deployment) util.yaml("deployment-with-secret.yaml"); - V1Service service = (V1Service) util.yaml("service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml"); - V1Secret secret = (V1Secret) util.yaml("secret.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - util.createAndWait(NAMESPACE, null, secret); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - util.deleteAndWait(NAMESPACE, null, secret); - } - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java deleted file mode 100644 index b5b3039a..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/secret/K8sClientSecretsReloadITUtil.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2013-2023 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.secret; - -import java.time.Duration; -import java.util.Map; -import java.util.Objects; - -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; - -/** - * @author wind57 - */ -final class K8sClientSecretsReloadITUtil { - - private static final Map POD_LABELS = Map.of("app", "spring-k8s-client-reload"); - - private static final String BODY_ONE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-k8s-client-reload", - "image": "image_name_here", - "livenessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/liveness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "readinessProbe": { - "failureThreshold": 3, - "httpGet": { - "path": "/actuator/health/readiness", - "port": 8080, - "scheme": "HTTP" - }, - "periodSeconds": 10, - "successThreshold": 1, - "timeoutSeconds": 1 - }, - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_CONFIG_ENABLED", - "value": "FALSE" - }, - { - "name": "SPRING_PROFILES_ACTIVE", - "value": "with-secret" - } - ] - }] - } - } - } - } - """; - - private K8sClientSecretsReloadITUtil() { - - } - - static WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - static RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(60, Duration.ofSeconds(2)).filter(Objects::nonNull); - } - - static void patchOne(String deploymentName, String namespace, String imageName) { - patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml deleted file mode 100644 index 4beef36c..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spring-k8s-client-ingress-reload - namespace: default -spec: - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: spring-k8s-client-reload - port: - number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml deleted file mode 100644 index a72d8af5..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/left-configmap.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: left-configmap - namespace: left -data: - left.value: "left-initial" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml similarity index 82% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml index f2ea29a5..dd14c314 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: poll-reload-as-mount + name: poll-reload namespace: default data: application.properties: | diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml similarity index 62% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml index dde4e3ff..1ac93cf9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml @@ -1,19 +1,19 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-k8s-client-reload + name: spring-cloud-kubernetes-k8s-client-reload spec: selector: matchLabels: - app: spring-k8s-client-reload + app: spring-cloud-kubernetes-k8s-client-reload template: metadata: labels: - app: spring-k8s-client-reload + app: spring-cloud-kubernetes-k8s-client-reload spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-k8s-client-reload + - name: spring-cloud-kubernetes-k8s-client-reload image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload imagePullPolicy: IfNotPresent readinessProbe: @@ -27,9 +27,19 @@ spec: ports: - containerPort: 8080 env: - - name: SPRING_PROFILES_ACTIVE - value: one - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD value: DEBUG + - name: SPRING_PROFILES_ACTIVE + value: "with-bootstrap" - name: SPRING_CLOUD_BOOTSTRAP_ENABLED - value: "TRUE" + value: TRUE + + volumeMounts: + - mountPath: /tmp + name: "secret-volume" + + volumes: + - name: "secret-volume" + secret: + defaultMode: 420 + secretName: secret-reload diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml similarity index 54% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml index e1e6ac3f..22184fa4 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/deployment-with-secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml @@ -1,19 +1,19 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: spring-k8s-client-reload + name: spring-cloud-kubernetes-k8s-client-reload spec: selector: matchLabels: - app: spring-k8s-client-reload + app: spring-cloud-kubernetes-k8s-client-reload template: metadata: labels: - app: spring-k8s-client-reload + app: spring-cloud-kubernetes-k8s-client-reload spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - - name: spring-k8s-client-reload + - name: spring-cloud-kubernetes-k8s-client-reload image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload imagePullPolicy: IfNotPresent readinessProbe: @@ -27,7 +27,21 @@ spec: ports: - containerPort: 8080 env: + - name: SPRING_PROFILES_ACTIVE + value: mount - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_CONFIG_RELOAD value: DEBUG - - name: SPRING_PROFILES_ACTIVE - value: "with-secret" + - name: SPRING_CLOUD_BOOTSTRAP_ENABLED + value: FALSE + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS + value: DEBUG + + volumeMounts: + - mountPath: /tmp + name: "config-map-volume" + + volumes: + - name: "config-map-volume" + configMap: + defaultMode: 420 + name: "poll-reload" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml similarity index 88% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml index 9ad19aad..26c9c3e2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: event-reload + name: secret-reload namespace: default data: # from.properties.key=initial diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service-with-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service-with-secret.yaml new file mode 100644 index 00000000..9a8302f3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service-with-secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-k8s-client-reload + name: spring-cloud-kubernetes-k8s-client-reload +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + nodePort: 32321 + selector: + app: spring-cloud-kubernetes-k8s-client-reload + type: NodePort diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service.yaml new file mode 100644 index 00000000..9a8302f3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-k8s-client-reload + name: spring-cloud-kubernetes-k8s-client-reload +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + nodePort: 32321 + selector: + app: spring-cloud-kubernetes-k8s-client-reload + type: NodePort diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml deleted file mode 100644 index fae9311f..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-k8s-client-reload - name: spring-k8s-client-reload -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-k8s-client-reload - type: ClusterIP From 1e70cab4e364ede0b82624ab6423d75a2b79150f Mon Sep 17 00:00:00 2001 From: wind57 Date: Tue, 25 Mar 2025 19:58:44 +0200 Subject: [PATCH 03/13] dirty Signed-off-by: wind57 --- .../config/SecretsPropertySourceLocator.java | 12 ++++++---- .../client/reload/ConfigMapProperties.java | 2 +- .../k8s/client/reload/Controller.java | 2 +- .../k8s/client/reload/SecretsProperties.java | 2 +- .../src/main/resources/application-mount.yaml | 2 -- .../resources/bootstrap-with-bootstrap.yaml | 2 +- .../it/K8sClientConfigMapMountPollingIT.java | 14 +++++------ ...8sClientSecretMountBootstrapPollingIT.java | 24 ++++++++----------- .../src/test/resources/mount/configmap.yaml | 4 ++-- .../mount/deployment-with-secret.yaml | 7 ++++-- .../src/test/resources/mount/deployment.yaml | 2 +- .../src/test/resources/mount/secret.yaml | 4 ++-- 12 files changed, 39 insertions(+), 38 deletions(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 07365b3f..87692ba6 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -123,12 +124,16 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca protected void putPathConfig(CompositePropertySource composite) { - if (!properties.paths().isEmpty()) { + Set uniquePaths = new LinkedHashSet<>(properties.paths()); + + if (!uniquePaths.isEmpty()) { LOG.warn( "path support is deprecated and will be removed in a future release. Please use spring.config.import"); } - this.properties.paths().stream().map(Paths::get).filter(Files::exists).flatMap(x -> { + LOG.debug("paths property sources : " + uniquePaths); + + uniquePaths.stream().map(Paths::get).filter(Files::exists).flatMap(x -> { try { return Files.walk(x); } @@ -136,8 +141,7 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca LOG.warn("Error walking properties files", e); return null; } - }) - .filter(Objects::nonNull) + }).filter(Objects::nonNull) .filter(Files::isRegularFile) .collect(new SecretsPropertySourceCollector()) .forEach(composite::addPropertySource); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java index f6e65df3..bc97eed9 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/ConfigMapProperties.java @@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author wind57 */ -@ConfigurationProperties("from.properties") +@ConfigurationProperties("from.properties.configmap") public class ConfigMapProperties { private String key; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java index 660ed1dc..ffee7d53 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java @@ -41,7 +41,7 @@ public class Controller { @GetMapping("/secret") public String secret() { - return configMapProperties.getKey(); + return secretsProperties.getKey(); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java index 43615d87..bcea2864 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java @@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author wind57 */ -@ConfigurationProperties("from.properties") +@ConfigurationProperties("from.properties.secret") public class SecretsProperties { private String key; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml index 62548724..ed67e042 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml @@ -12,6 +12,4 @@ spring: config: paths: - /tmp/application.properties - config: - import: "kubernetes:" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml index 0aed2eb9..8fcaa31b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml @@ -1,6 +1,6 @@ spring: cloud: kubernetes: - config: + secrets: paths: - /tmp/application.properties 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/K8sClientConfigMapMountPollingIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java index 8a9d9bc0..8337028f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientConfigMapMountPollingIT.java @@ -105,20 +105,20 @@ class K8sClientConfigMapMountPollingIT extends K8sClientReloadBase { // replace data in configmap and wait for k8s to pick it up // our polling will detect that and restart the app V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/configmap.yaml"); - configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=as-mount-changed")); - coreV1Api.replaceNamespacedConfigMap("poll-reload", NAMESPACE, configMap, null, null, null, null); + configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.configmap.key=as-mount-changed")); + coreV1Api.replaceNamespacedConfigMap("configmap-reload", NAMESPACE, configMap, 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(() -> webClient.method(HttpMethod.GET) + await().atMost(Duration.ofSeconds(120)).pollInterval(Duration.ofSeconds(1)).until(() -> { + String local = webClient.method(HttpMethod.GET) .retrieve() .bodyToMono(String.class) .retryWhen(retrySpec()) - .block() - .equals("as-mount-changed")); + .block(); + return "as-mount-changed".equals(local); + }); } } 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 87568eb0..aaaafe53 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 @@ -16,11 +16,13 @@ package org.springframework.cloud.kubernetes.k8s.client.reload.it; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; import java.util.Map; import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; +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; @@ -78,7 +80,7 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { * 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 via (1), (2) and (3). + * - 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. @@ -86,12 +88,6 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { */ @Test void test() throws Exception { - // (1) - Commons.waitForLogStatement("paths property sources : [/tmp/application.properties]", K3S, IMAGE_NAME); - // (2) - Commons.waitForLogStatement("will add file-based property source : /tmp/application.properties", K3S, - IMAGE_NAME); - // (3) WebClient webClient = builder().baseUrl("http://localhost:32321/secret").build(); String result = webClient.method(HttpMethod.GET) .retrieve() @@ -100,15 +96,15 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { .block(); // we first read the initial value from the configmap - assertThat(result).isEqualTo("as-mount-initial"); + assertThat(result).isEqualTo("initial"); - // replace data in configmap and wait for k8s to pick it up + // replace data in secret and wait for k8s to pick it up // our polling will detect that and restart the app - V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/secret.yaml"); - configMap.setData(Map.of(Constants.APPLICATION_PROPERTIES, "from.properties.key=as-mount-changed")); - coreV1Api.replaceNamespacedConfigMap("poll-reload", NAMESPACE, configMap, null, null, null, null); + V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); + secret.setData(Map.of(Constants.APPLICATION_PROPERTIES, Base64.getEncoder() + .encode("from.properties.secret.key=as-mount-changed".getBytes(StandardCharsets.UTF_8)))); + coreV1Api.readNamespacedSecret("secret-reload", NAMESPACE, null); - System.out.println("Waiting for reload change to be observed"); Commons.waitForLogStatement("Detected change in config maps/secrets, reload will be triggered", K3S, IMAGE_NAME); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml index dd14c314..d6b8885f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml @@ -1,8 +1,8 @@ apiVersion: v1 kind: ConfigMap metadata: - name: poll-reload + name: configmap-reload # different from the application name namespace: default data: application.properties: | - from.properties.key=as-mount-initial + from.properties.configmap.key=as-mount-initial diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml index 1ac93cf9..9110c1f5 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml @@ -14,7 +14,7 @@ spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - name: spring-cloud-kubernetes-k8s-client-reload - image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload:1 imagePullPolicy: IfNotPresent readinessProbe: httpGet: @@ -32,11 +32,14 @@ spec: - name: SPRING_PROFILES_ACTIVE value: "with-bootstrap" - name: SPRING_CLOUD_BOOTSTRAP_ENABLED - value: TRUE + value: "TRUE" + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS + value: DEBUG volumeMounts: - mountPath: /tmp name: "secret-volume" + readOnly: true volumes: - name: "secret-volume" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml index 22184fa4..9261862f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment.yaml @@ -44,4 +44,4 @@ spec: - name: "config-map-volume" configMap: defaultMode: 420 - name: "poll-reload" + name: "configmap-reload" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml index 26c9c3e2..ffcd14ee 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml @@ -4,6 +4,6 @@ metadata: name: secret-reload namespace: default data: - # from.properties.key=initial + # from.properties.secret.key=initial application.properties: | - ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFs + ZnJvbS5wcm9wZXJ0aWVzLnNlY3JldC5rZXk9aW5pdGlhbA== From 074404cd606ada7f093b9cbf0256ad4e0b2cfb0f Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 26 Mar 2025 18:33:29 +0200 Subject: [PATCH 04/13] dirty Signed-off-by: wind57 --- .../commons/config/SecretsPropertySourceLocator.java | 10 +++++++--- .../cloud/kubernetes/k8s/client/reload/App.java | 2 +- .../cloud/kubernetes/k8s/client/reload/Controller.java | 4 ++-- .../{SecretsProperties.java => SecretProperties.java} | 2 +- .../src/main/resources/application-with-bootstrap.yaml | 1 - .../src/main/resources/bootstrap-with-bootstrap.yaml | 4 ++++ .../k8s/client/reload/it/K8sClientReloadBase.java | 2 +- .../test/resources/mount/deployment-with-secret.yaml | 2 +- 8 files changed, 17 insertions(+), 10 deletions(-) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/{SecretsProperties.java => SecretProperties.java} (96%) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 87692ba6..1770ef4d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -21,13 +21,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; @@ -103,6 +104,7 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca } cache.discardAll(); + System.out.println("composite : " + Arrays.toString(composite.getPropertyNames())); return composite; } return null; @@ -141,7 +143,8 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca LOG.warn("Error walking properties files", e); return null; } - }).filter(Objects::nonNull) + }) + .filter(Objects::nonNull) .filter(Files::isRegularFile) .collect(new SecretsPropertySourceCollector()) .forEach(composite::addPropertySource); @@ -192,8 +195,9 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca try { String content = new String(Files.readAllBytes(filePath)).trim(); + System.out.println("content is : " + content); String sourceName = fileName.toLowerCase(Locale.ROOT); - SourceData sourceData = new SourceData(sourceName, Collections.singletonMap(fileName, content)); + SourceData sourceData = new SourceData(sourceName, Map.of(fileName, content)); return new SecretsPropertySource(sourceData); } catch (IOException e) { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java index 23d76328..85c0d11c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/App.java @@ -25,7 +25,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties */ @SpringBootApplication @EnableConfigurationProperties({ LeftProperties.class, RightProperties.class, RightWithLabelsProperties.class, - ConfigMapProperties.class, SecretsProperties.class }) + ConfigMapProperties.class, SecretProperties.class }) public class App { public static void main(String[] args) { diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java index ffee7d53..94f0a31b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/Controller.java @@ -27,9 +27,9 @@ public class Controller { private final ConfigMapProperties configMapProperties; - private final SecretsProperties secretsProperties; + private final SecretProperties secretsProperties; - public Controller(ConfigMapProperties configMapProperties, SecretsProperties secretsProperties) { + public Controller(ConfigMapProperties configMapProperties, SecretProperties secretsProperties) { this.configMapProperties = configMapProperties; this.secretsProperties = secretsProperties; } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretProperties.java similarity index 96% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretProperties.java index bcea2864..413e1bb0 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretsProperties.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/java/org/springframework/cloud/kubernetes/k8s/client/reload/SecretProperties.java @@ -22,7 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author wind57 */ @ConfigurationProperties("from.properties.secret") -public class SecretsProperties { +public class SecretProperties { private String key; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml index 220edfde..052f2286 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml @@ -9,4 +9,3 @@ spring: mode: polling period: 5000 monitoring-secrets: true - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml index 8fcaa31b..a45a51e0 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml @@ -4,3 +4,7 @@ spring: secrets: paths: - /tmp/application.properties + enabled: true + + config: + enabled: false 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/K8sClientReloadBase.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java index 5655ab7f..cc91acce 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientReloadBase.java @@ -116,9 +116,9 @@ abstract class K8sClientReloadBase { protected static void manifestsSecret(Phase phase, Util util, String namespace, String imageName) { + V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); V1Deployment deployment = (V1Deployment) util.yaml("mount/deployment-with-secret.yaml"); V1Service service = (V1Service) util.yaml("mount/service-with-secret.yaml"); - V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); if (phase.equals(Phase.CREATE)) { util.createAndWait(namespace, null, secret); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml index 9110c1f5..8633b5ba 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/deployment-with-secret.yaml @@ -14,7 +14,7 @@ spec: serviceAccountName: spring-cloud-kubernetes-serviceaccount containers: - name: spring-cloud-kubernetes-k8s-client-reload - image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload:1 + image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-reload imagePullPolicy: IfNotPresent readinessProbe: httpGet: From e67cc1ac3ba3440d667ec5bd4ea74d299eb1e7a5 Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 27 Mar 2025 08:25:40 +0200 Subject: [PATCH 05/13] remove sout Signed-off-by: wind57 --- .../kubernetes/commons/config/SecretsPropertySourceLocator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 1770ef4d..1294d4ca 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -195,7 +195,6 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca try { String content = new String(Files.readAllBytes(filePath)).trim(); - System.out.println("content is : " + content); String sourceName = fileName.toLowerCase(Locale.ROOT); SourceData sourceData = new SourceData(sourceName, Map.of(fileName, content)); return new SecretsPropertySource(sourceData); From b851f9c1277f49ddb633478c1253e4db313ae19f Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 27 Mar 2025 14:09:38 +0200 Subject: [PATCH 06/13] dirty Signed-off-by: wind57 --- .../src/main/resources/bootstrap-with-bootstrap.yaml | 4 +++- .../reload/it/K8sClientSecretMountBootstrapPollingIT.java | 2 +- .../src/test/resources/mount/secret.yaml | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml index a45a51e0..8808c39e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-with-bootstrap.yaml @@ -3,7 +3,9 @@ spring: kubernetes: secrets: paths: - - /tmp/application.properties + # at the moment, we do not support reading properties/yaml/yml + # files when mounting via 'paths' + - /tmp/from.properties.secret.key enabled: true config: 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 aaaafe53..f067424a 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,7 +103,7 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); secret.setData(Map.of(Constants.APPLICATION_PROPERTIES, Base64.getEncoder() .encode("from.properties.secret.key=as-mount-changed".getBytes(StandardCharsets.UTF_8)))); - coreV1Api.readNamespacedSecret("secret-reload", NAMESPACE, null); + 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); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml index ffcd14ee..7f0084b3 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml @@ -5,5 +5,4 @@ metadata: namespace: default data: # from.properties.secret.key=initial - application.properties: | - ZnJvbS5wcm9wZXJ0aWVzLnNlY3JldC5rZXk9aW5pdGlhbA== + from.properties.secret.key: aW5pdGlhbA== From 71c9c893f43cef3555d6e869ea91cd05eb2cfe6b Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 28 Mar 2025 11:27:39 +0200 Subject: [PATCH 07/13] fix tests Signed-off-by: wind57 --- .../kubernetes/commons/config/reload/ConfigReloadUtil.java | 7 +++++++ .../src/main/resources/application-mount.yaml | 3 +++ .../src/main/resources/application-with-bootstrap.yaml | 2 +- .../reload/it/K8sClientSecretMountBootstrapPollingIT.java | 5 +---- 4 files changed, 12 insertions(+), 5 deletions(-) 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..3640bb7b 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; @@ -120,6 +121,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); + } } } @@ -179,6 +184,8 @@ public final class ConfigReloadUtil { for (int i = 0; i < k8sSources.size(); i++) { MapPropertySource k8sSource = k8sSources.get(i); MapPropertySource appSource = appSources.get(i); + System.out.println("k8sSource (abc): " + k8sSource.getSource()); + System.out.println("appSource (abc): " + appSource.getSource()); if (changed(k8sSource, appSource)) { LOG.debug(() -> "found change in : " + k8sSource); return true; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml index ed67e042..ab5b15fa 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-mount.yaml @@ -13,3 +13,6 @@ spring: paths: - /tmp/application.properties + config: + import: "kubernetes:" + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml index 052f2286..9002bcf6 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml @@ -5,7 +5,7 @@ spring: kubernetes: reload: enabled: true - strategy: shutdown + strategy: refresh mode: polling period: 5000 monitoring-secrets: true 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 f067424a..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 @@ -18,7 +18,6 @@ package org.springframework.cloud.kubernetes.k8s.client.reload.it; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.Base64; import java.util.Map; import io.kubernetes.client.openapi.apis.CoreV1Api; @@ -28,7 +27,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.k3s.K3sContainer; -import org.springframework.cloud.kubernetes.commons.config.Constants; 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; @@ -101,8 +99,7 @@ class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { // 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(Constants.APPLICATION_PROPERTIES, Base64.getEncoder() - .encode("from.properties.secret.key=as-mount-changed".getBytes(StandardCharsets.UTF_8)))); + 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, From 92405f28a90943213769ec622800f29b1502c511 Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 28 Mar 2025 11:41:30 +0200 Subject: [PATCH 08/13] cleanup Signed-off-by: wind57 --- .../commons/config/SecretsPropertySourceLocator.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 1294d4ca..681f5bab 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -104,8 +104,7 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca } cache.discardAll(); - System.out.println("composite : " + Arrays.toString(composite.getPropertyNames())); - return composite; + return composite;g } return null; } From bd1599bb6393e11c9d513253d0294cd109393bd8 Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 28 Mar 2025 11:41:48 +0200 Subject: [PATCH 09/13] cleanup Signed-off-by: wind57 --- .../kubernetes/commons/config/SecretsPropertySourceLocator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 681f5bab..5db6fb78 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -104,7 +104,7 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca } cache.discardAll(); - return composite;g + return composite; } return null; } From 96fca8d737f5b164346b862ab0b39a8defcf63a3 Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 28 Mar 2025 11:43:59 +0200 Subject: [PATCH 10/13] cleanup Signed-off-by: wind57 --- .../kubernetes/commons/config/SecretsPropertySourceLocator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 5db6fb78..cecd3232 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -21,7 +21,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; From 2057672da385f2540f33fdf1588bf9bc303526de Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 28 Mar 2025 14:10:46 +0200 Subject: [PATCH 11/13] fix tests Signed-off-by: wind57 --- .../src/test/resources/META-INF/spring.factories | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories index ac5985bb..99371953 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/META-INF/spring.factories @@ -1,2 +1,2 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.kubernetes.k8s.client.reload.it.configmap.K8sClientConfigMapEventTriggeredIT.TestConfig +org.springframework.cloud.kubernetes.k8s.client.reload.it.K8sClientConfigMapEventTriggeredIT.TestConfig From f4ae57287ceea7306146dfab22c187a53d1b4f50 Mon Sep 17 00:00:00 2001 From: wind57 Date: Sat, 29 Mar 2025 06:58:55 +0200 Subject: [PATCH 12/13] first Signed-off-by: wind57 --- .../config/reload/ConfigReloadUtil.java | 4 - ...8sClientSecretMountBootstrapPollingIT.java | 118 ------------------ 2 files changed, 122 deletions(-) delete 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 3640bb7b..cf1dbd92 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 @@ -121,10 +121,6 @@ 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-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 deleted file mode 100644 index c6b0f88f..00000000 --- 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 +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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(() -> webClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block() - .equals("as-mount-changed")); - } - -} From b54bde72e9521cd7af9e05893bcb47542c3dbe15 Mon Sep 17 00:00:00 2001 From: wind57 Date: Sat, 29 Mar 2025 07:00:32 +0200 Subject: [PATCH 13/13] clean-up Signed-off-by: wind57 --- .../kubernetes/commons/config/reload/ConfigReloadUtil.java | 3 --- 1 file changed, 3 deletions(-) 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 cf1dbd92..c8df2821 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.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; @@ -180,8 +179,6 @@ public final class ConfigReloadUtil { for (int i = 0; i < k8sSources.size(); i++) { MapPropertySource k8sSource = k8sSources.get(i); MapPropertySource appSource = appSources.get(i); - System.out.println("k8sSource (abc): " + k8sSource.getSource()); - System.out.println("appSource (abc): " + appSource.getSource()); if (changed(k8sSource, appSource)) { LOG.debug(() -> "found change in : " + k8sSource); return true;