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 07365b3fe5..cecd3232ea 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 @@ -22,11 +22,12 @@ import java.nio.file.Paths; import java.util.ArrayList; 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; @@ -123,12 +124,16 @@ protected abstract SecretsPropertySource getPropertySource(ConfigurableEnvironme 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); } @@ -189,7 +194,7 @@ private SecretsPropertySource property(Path filePath) { try { String content = new String(Files.readAllBytes(filePath)).trim(); 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-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 c8df282107..3640bb7b08 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.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 @@ else if (propertySource instanceof MountConfigMapPropertySource mountConfigMapPr // we know that the type is correct here managedSources.add((S) mountConfigMapPropertySource); } + else if (propertySource instanceof SecretsPropertySource secretsPropertySource) { + // we know that the type is correct here + managedSources.add((S) secretsPropertySource); + } } } @@ -179,6 +184,8 @@ static boolean changed(List k8sSources, List "found change in : " + k8sSource); return true; 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 71389a57d7..d99b0b11a8 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 @@ /** * @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/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 23d76328cf..85c0d11caa 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 @@ */ @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/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 f6e65df380..bc97eed926 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 @@ /** * @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 e2eb643534..94f0a31b35 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 @@ @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; - this.configMapProperties = configMapProperties; - } - - @GetMapping("/left") - public String left() { - return leftProperties.getValue(); - } - - @GetMapping("/right") - public String right() { - return rightProperties.getValue(); - } + private final SecretProperties secretsProperties; - @GetMapping("/with-label") - public String witLabel() { - return rightWithLabelsProperties.getValue(); + public Controller(ConfigMapProperties configMapProperties, SecretProperties secretsProperties) { + this.configMapProperties = configMapProperties; + this.secretsProperties = secretsProperties; } - @GetMapping("/mount") + @GetMapping("/configmap") public String key() { return configMapProperties.getKey(); } + @GetMapping("/secret") + public String secret() { + 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/SecretProperties.java similarity index 91% 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 43615d873f..413e1bb0b6 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 @@ -21,8 +21,8 @@ /** * @author wind57 */ -@ConfigurationProperties("from.properties") -public class SecretsProperties { +@ConfigurationProperties("from.properties.secret") +public class SecretProperties { 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/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 b0e0597a80..0000000000 --- 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 cd1765f5c8..ab5b15fad7 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: @@ -12,6 +12,7 @@ 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/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 988bfb2127..0000000000 --- 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-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-three.yaml index fc367fcff5..72f6a44c1e 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 43ae273796..f8071e884a 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/application-with-bootstrap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-bootstrap.yaml index c997322d16..9002bcf66c 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,7 @@ spring: kubernetes: reload: enabled: true - monitoring-config-maps: 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/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 9265fce12e..0000000000 --- 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/main/resources/bootstrap-three.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/bootstrap-three.yaml index 5c5f360585..af51a86e42 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 9265fce12e..4c5a11e33c 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/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 0aed2eb9dd..8808c39e78 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,12 @@ spring: cloud: kubernetes: - config: + 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: + 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/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 c527145d9f..0000000000 --- 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 9814a9066b..0000000000 --- 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,385 +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 java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; - -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.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.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; - -/** - * @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"); - } - - /** - *
-	 *     - 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 { - - 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 2043b40e0f..0000000000 --- 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,453 +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 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); - } - - 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 3092e92535..0000000000 --- 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/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 new file mode 100644 index 0000000000..1831ca4ac9 --- /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/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; + +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/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 new file mode 100644 index 0000000000..303ecbd36d --- /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/K8sClientConfigMapLabelEventTriggeredIT.java @@ -0,0 +1,191 @@ +/* + * 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 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"))) + .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-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") + .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/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 51% 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 358ae43f81..8337028f4e 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"); - 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); + V1ConfigMap configMap = (V1ConfigMap) util.yaml("mount/configmap.yaml"); + 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().timeout(Duration.ofSeconds(180)) - .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(K8sClientConfigMapReloadITUtil.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); + .retryWhen(retrySpec()) + .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/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 new file mode 100644 index 0000000000..cc91accef9 --- /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/K8sClientReloadBase.java @@ -0,0 +1,134 @@ +/* + * 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.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.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; +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; + +/** + * @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; + }); + } + + 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); + } + } + + 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) { + + 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"); + + 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 0000000000..c6b0f88f9d --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/java/org/springframework/cloud/kubernetes/k8s/client/reload/it/K8sClientSecretMountBootstrapPollingIT.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.k8s.client.reload.it; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Secret; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.builder; +import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.retrySpec; + +/** + * @author wind57 + */ +class K8sClientSecretMountBootstrapPollingIT extends K8sClientReloadBase { + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-reload"; + + private static final String NAMESPACE = "default"; + + private static final K3sContainer K3S = Commons.container(); + + private static Util util; + + private static CoreV1Api coreV1Api; + + @BeforeAll + static void beforeAllLocal() throws Exception { + K3S.start(); + Commons.validateImage(IMAGE_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); + + util = new Util(K3S); + coreV1Api = new CoreV1Api(); + util.setUp(NAMESPACE); + manifestsSecret(Phase.CREATE, util, NAMESPACE, IMAGE_NAME); + } + + @AfterAll + static void afterAll() { + manifestsSecret(Phase.DELETE, util, NAMESPACE, IMAGE_NAME); + } + + /** + *
+	 *     - we have bootstrap enabled
+	 *     - we will 'locate' property sources from secrets.
+	 *     - there are no explicit secrets to search for, but what we will also read,
+	 *     	 is 'spring.cloud.kubernetes.secret.paths', which we have set to
+	 *     	 '/tmp/application.properties'
+	 *       in this test. That is populated by the volumeMounts (see mount/deployment-with-secret.yaml)
+	 *     - we first assert that we are actually reading the path based source
+	 *
+	 *     - we then change the secret content, wait for k8s to pick it up and replace them
+	 *     - our polling will then detect that change, and trigger a reload.
+	 * 
+ */ + @Test + void test() throws Exception { + WebClient webClient = builder().baseUrl("http://localhost:32321/secret").build(); + String result = webClient.method(HttpMethod.GET) + .retrieve() + .bodyToMono(String.class) + .retryWhen(retrySpec()) + .block(); + + // we first read the initial value from the configmap + assertThat(result).isEqualTo("initial"); + + // replace data in secret and wait for k8s to pick it up + // our polling will detect that and restart the app + V1Secret secret = (V1Secret) util.yaml("mount/secret.yaml"); + secret.setData(Map.of("from.properties.secret.key", "as-mount-changed".getBytes(StandardCharsets.UTF_8))); + coreV1Api.replaceNamespacedSecret("secret-reload", NAMESPACE, secret, null, null, null, null); + + Commons.waitForLogStatement("Detected change in config maps/secrets, reload will be triggered", K3S, + IMAGE_NAME); + + await().atMost(Duration.ofSeconds(120)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> 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 ba0ab2fa24..0000000000 --- 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 91f83032b5..0000000000 --- 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 b5b3039adc..0000000000 --- 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/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 0000000000..993719534b --- /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.K8sClientConfigMapEventTriggeredIT.TestConfig 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/configmap-mount.yaml deleted file mode 100644 index f2ea29a5c2..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/configmap-mount.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: poll-reload-as-mount - namespace: default -data: - application.properties: | - from.properties.key=as-mount-initial 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 4beef36c5e..0000000000 --- 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 a72d8af5dc..0000000000 --- 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/mount/configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml new file mode 100644 index 0000000000..d6b8885fb3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-reload # different from the application name + namespace: default +data: + application.properties: | + from.properties.configmap.key=as-mount-initial 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 58% 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 dde4e3ff9b..8633b5baa4 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,22 @@ 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" + - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS + value: DEBUG + + volumeMounts: + - mountPath: /tmp + name: "secret-volume" + readOnly: true + + 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 e1e6ac3f1b..9261862f07 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: "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 new file mode 100644 index 0000000000..7f0084b352 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/mount/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-reload + namespace: default +data: + # from.properties.secret.key=initial + from.properties.secret.key: aW5pdGlhbA== 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 0000000000..9a8302f37d --- /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 0000000000..9a8302f37d --- /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/secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml deleted file mode 100644 index 9ad19aadfc..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/test/resources/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: event-reload - namespace: default -data: - # from.properties.key=initial - application.properties: | - ZnJvbS5wcm9wZXJ0aWVzLmtleT1pbml0aWFs 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 fae9311ffd..0000000000 --- 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 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 0c3cd19027..9fc85e5190 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.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 void deleteClusterWide(String serviceAccountNamespace, Set namesp } 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); }