From 1bdd51a0f76ccc309e080584cbe5e6481ca87412 Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 7 Nov 2024 23:07:50 +0200 Subject: [PATCH 01/13] refactor tests Signed-off-by: wind57 --- .../Fabric8ConfigMapPropertySourceLocatorTests.java | 10 ++++++++-- .../config/Fabric8ConfigMapPropertySourceTests.java | 12 +++++++++--- .../Fabric8SecretsPropertySourceLocatorTests.java | 10 ++++++++-- .../Fabric8SecretsPropertySourceMockTests.java | 6 ++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java index e289bbc43e..68026db2d0 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; @@ -38,9 +39,14 @@ @EnableKubernetesMockClient class Fabric8ConfigMapPropertySourceLocatorTests { - private KubernetesMockServer mockServer; + private static KubernetesMockServer mockServer; - private KubernetesClient mockClient; + private static KubernetesClient mockClient; + + @BeforeAll + static void beforeAll() { + mockClient.getConfiguration().setRequestRetryBackoffLimit(1); + } @Test void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java index 482e8ba32d..8e550838ba 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java @@ -20,6 +20,7 @@ import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; @@ -42,6 +43,11 @@ class Fabric8ConfigMapPropertySourceTests { private static final ConfigUtils.Prefix DEFAULT = ConfigUtils.findPrefix("default", false, false, "irrelevant"); + @BeforeEach + void beforeEach() { + mockClient.getConfiguration().setRequestRetryBackoffLimit(1); + } + @AfterEach void afterEach() { new Fabric8ConfigMapsCache().discardAll(); @@ -51,7 +57,7 @@ void afterEach() { void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { String name = "my-config"; String namespace = "default"; - String path = String.format("/api/v1/namespaces/%s/configmaps", namespace); + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, true, DEFAULT, true); @@ -64,11 +70,11 @@ void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { String name = "my-config"; String namespace = "default"; - String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name); + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment()); assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(context)); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java index 5223490912..53c4d0a711 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; @@ -38,9 +39,14 @@ @EnableKubernetesMockClient class Fabric8SecretsPropertySourceLocatorTests { - KubernetesMockServer mockServer; + private static KubernetesMockServer mockServer; - KubernetesClient mockClient; + private static KubernetesClient mockClient; + + @BeforeAll + static void beforeAll() { + mockClient.getConfiguration().setRequestRetryBackoffInterval(1); + } @Test void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java index c048667484..a0a246eded 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; @@ -43,6 +44,11 @@ class Fabric8SecretsPropertySourceMockTests { private static KubernetesClient client; + @BeforeAll + static void beforeAll() { + client.getConfiguration().setRequestRetryBackoffInterval(1); + } + @Test void namedStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { final String name = "my-secret"; From 804458f317456f9fd86a33dcf5f16a29e4049dbd Mon Sep 17 00:00:00 2001 From: wind57 Date: Sun, 10 Nov 2024 21:57:47 +0200 Subject: [PATCH 02/13] started work --- .../commons/config/LabeledSourceData.java | 10 +- .../commons/config/NamedSourceData.java | 5 +- .../kubernetes/commons/config/SourceData.java | 6 + .../config/reload/ConfigReloadUtil.java | 7 + .../config/reload/ConfigReloadUtilTests.java | 12 + ...ic8ConfigMapErrorOnReadingSourceTests.java | 287 ++++++++++++++++++ ...abric8SecretErrorOnReadingSourceTests.java | 283 +++++++++++++++++ 7 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java index 7c3068995c..7742c9214f 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java @@ -21,8 +21,12 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException; import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR; +import static org.springframework.cloud.kubernetes.commons.config.SourceData.EMPTY_SOURCE_NAME_ON_ERROR; /** * @author wind57 @@ -32,10 +36,12 @@ */ public abstract class LabeledSourceData { + private static final Log LOG = LogFactory.getLog(LabeledSourceData.class); + public final SourceData compute(Map labels, ConfigUtils.Prefix prefix, String target, boolean profileSources, boolean failFast, String namespace, String[] activeProfiles) { - MultipleSourcesContainer data = MultipleSourcesContainer.empty(); + MultipleSourcesContainer data; try { Set profiles = Set.of(); @@ -73,7 +79,9 @@ public final SourceData compute(Map labels, ConfigUtils.Prefix p } } catch (Exception e) { + LOG.warn("failure in reading labeled sources"); onException(failFast, e); + return SourceData.emptyRecord(EMPTY_SOURCE_NAME_ON_ERROR); } String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR)); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java index c198e43316..f3e9ffa254 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java @@ -24,6 +24,7 @@ import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException; import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR; +import static org.springframework.cloud.kubernetes.commons.config.SourceData.EMPTY_SOURCE_NAME_ON_ERROR; /** * @author wind57 @@ -42,7 +43,7 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St // first comes non-profile based source sourceNames.add(sourceName); - MultipleSourcesContainer data = MultipleSourcesContainer.empty(); + MultipleSourcesContainer data; try { if (profileSources) { @@ -69,7 +70,9 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St } catch (Exception e) { + LOG.warn("failure in reading named sources"); onException(failFast, e); + return SourceData.emptyRecord(EMPTY_SOURCE_NAME_ON_ERROR); } String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR)); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java index b9eb46c0b2..11b6788698 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java @@ -26,6 +26,12 @@ */ public record SourceData(String sourceName, Map sourceData) { + /** + * source name that is generated when there is an error reading the underlying + * configmap(s) or secret(s). + */ + public static final String EMPTY_SOURCE_NAME_ON_ERROR = "source-generate-on-error"; + public static SourceData emptyRecord(String sourceName) { return new SourceData(sourceName, Map.of()); } 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 4279694418..658c3fc1fd 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.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -162,6 +163,12 @@ else if (propertySource instanceof CompositePropertySource source) { } static boolean changed(List k8sSources, List appSources) { + + if (k8sSources.stream().anyMatch(source -> source.getName().equals(SourceData.EMPTY_SOURCE_NAME_ON_ERROR))) { + LOG.info(() -> "there was an error while reading config maps/secrets, no reload will happen"); + return false; + } + if (k8sSources.size() != appSources.size()) { if (LOG.isDebugEnabled()) { LOG.debug("k8s property sources size: " + k8sSources.size()); diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java index 280d95f9d5..ccda13c6ff 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java @@ -27,6 +27,7 @@ import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MapPropertySource; @@ -150,6 +151,17 @@ public Object getProperty(String name) { Assertions.assertEquals("from-inner-two-composite", result.get(3).getProperty("")); } + @Test + void testEmptySourceNameOnError() { + Object value = new Object(); + Map rightMap = new HashMap<>(); + rightMap.put("key", value); + MapPropertySource left = new MapPropertySource(SourceData.EMPTY_SOURCE_NAME_ON_ERROR, Map.of()); + MapPropertySource right = new MapPropertySource("right", rightMap); + boolean changed = ConfigReloadUtil.changed(List.of(left), List.of(right)); + assertThat(changed).isFalse(); + } + private static final class OneComposite extends CompositePropertySource { private OneComposite() { diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java new file mode 100644 index 0000000000..5f90eaeb93 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java @@ -0,0 +1,287 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapListBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties.Source; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) +class Fabric8ConfigMapErrorOnReadingSourceTests { + + private static KubernetesMockServer mockServer; + + private static KubernetesClient mockClient; + + @BeforeAll + static void beforeAll() { + mockClient.getConfiguration().setRequestRetryBackoffLimit(0); + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void namedSingleConfigMapFails() { + String name = "my-config"; + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources() + .stream() + .findAny() + .orElseThrow(); + + assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void namedTwoConfigMapsOneFails() { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + ConfigMap configMapTwo = configMap(configMapNameTwo, Map.of()); + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + mockServer.expect() + .withPath(path) + .andReturn(200, new ConfigMapListBuilder().withItems(configMapTwo).build()) + .once(); + + Source sourceOne = new Source(configMapNameOne, namespace, Map.of(), null, null, null); + Source sourceTwo = new Source(configMapNameTwo, namespace, Map.of(), null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, + RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void namedTwoConfigMapsBothFail() { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + Source sourceOne = new Source(configMapNameOne, namespace, Map.of(), null, null, null); + Source sourceTwo = new Source(configMapNameTwo, namespace, Map.of(), null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, + RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void labeledSingleConfigMapFails(CapturedOutput output) { + Map labels = Map.of("a", "b"); + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; + + // one for the 'application' named configmap + // the other for the labeled config map + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2); + + Source configMapSource = new Source(null, namespace, labels, null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsOneFails(CapturedOutput output) { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + + Map configMapOneLabels = Map.of("one", "1"); + Map configMapTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + ConfigMap configMapOne = configMap(configMapNameOne, configMapOneLabels); + ConfigMap configMapTwo = configMap(configMapNameTwo, configMapTwoLabels); + + // one for 'application' named configmap and one for the first labeled configmap + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2); + mockServer.expect() + .withPath(path) + .andReturn(200, new ConfigMapListBuilder().withItems(configMapOne, configMapTwo).build()) + .once(); + + Source sourceOne = new Source(null, namespace, configMapOneLabels, null, null, null); + Source sourceTwo = new Source(null, namespace, configMapTwoLabels, null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, + false, RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsBothFail(CapturedOutput output) { + + Map configMapOneLabels = Map.of("one", "1"); + Map configMapTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + // one for 'application' named configmap and two for the labeled configmaps + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(3); + + Source sourceOne = new Source(null, namespace, configMapOneLabels, null, null, null); + Source sourceTwo = new Source(null, namespace, configMapTwoLabels, null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, + false, RetryProperties.DEFAULT); + + Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // all 3 sources ('application' named source, and two labeled sources) + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + + private ConfigMap configMap(String name, Map labels) { + return new ConfigMapBuilder().withNewMetadata().withName(name).withLabels(labels).endMetadata().build(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java new file mode 100644 index 0000000000..cd5cf86d4d --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java @@ -0,0 +1,283 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.SecretListBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties.Source; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) +class Fabric8SecretErrorOnReadingSourceTests { + + private static KubernetesMockServer mockServer; + + private static KubernetesClient mockClient; + + @BeforeAll + static void beforeAll() { + mockClient.getConfiguration().setRequestRetryBackoffLimit(0); + } + + /** + *
+	 *     we try to read all secrets in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void namedSingleSecretFails(CapturedOutput output) { + String name = "my-secret"; + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/secrets"; + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources() + .stream() + .findAny() + .orElseThrow(); + + assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output).contains("failure in reading named sources"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void namedTwoSecretsOneFails() { + String secretNameOne = "one"; + String secretNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/secrets"; + + Secret secretTwo = secret(secretNameTwo, Map.of()); + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + mockServer.expect().withPath(path).andReturn(200, new SecretListBuilder().withItems(secretTwo).build()).once(); + + Source sourceOne = new Source(secretNameOne, namespace, Map.of(), null, null, null); + Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("secret.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void namedTwoSecretsBothFail() { + String secretNameOne = "one"; + String secretNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/secrets"; + + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + Source sourceOne = new Source(secretNameOne, namespace, Map.of(), null, null, null); + Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     we try to read all secrets in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void labeledSingleSecretFails(CapturedOutput output) { + Map labels = Map.of("a", "b"); + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/secrets"; + + // one for the 'application' named secret + // the other for the labeled secret + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2); + + Source secretSource = new Source(null, namespace, labels, null, null, null); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, labels, List.of(), + List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void labeledTwoSecretsOneFails(CapturedOutput output) { + String secretNameOne = "one"; + String secretNameTwo = "two"; + + Map secretOneLabels = Map.of("one", "1"); + Map secretTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/secrets"; + + Secret secretOne = secret(secretNameOne, secretOneLabels); + Secret secretTwo = secret(secretNameTwo, secretTwoLabels); + + // one for 'application' named secret and one for the first labeled secret + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2); + mockServer.expect() + .withPath(path) + .andReturn(200, new SecretListBuilder().withItems(secretOne, secretTwo).build()) + .once(); + + Source sourceOne = new Source(null, namespace, secretOneLabels, null, null, null); + Source sourceTwo = new Source(null, namespace, secretTwoLabels, null, null, null); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, + Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, + true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("secret.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsBothFail(CapturedOutput output) { + + Map secretOneLabels = Map.of("one", "1"); + Map secretTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/secrets"; + + // one for 'application' named configmap and two for the labeled configmaps + mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(3); + + Source sourceOne = new Source(null, namespace, secretOneLabels, null, null, null); + Source sourceTwo = new Source(null, namespace, secretTwoLabels, null, null, null); + + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, + Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, + true, false, RetryProperties.DEFAULT); + + Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, + secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // all 3 sources ('application' named source, and two labeled sources) + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + + private Secret secret(String name, Map labels) { + return new SecretBuilder().withNewMetadata().withName(name).withLabels(labels).endMetadata().build(); + } + +} From 4c3df938e789af136be3360a0f3c8c8c0c48251a Mon Sep 17 00:00:00 2001 From: wind57 Date: Mon, 11 Nov 2024 21:52:51 +0200 Subject: [PATCH 03/13] add k8s client tests --- ...entConfigMapErrorOnReadingSourceTests.java | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java new file mode 100644 index 0000000000..91041c8352 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java @@ -0,0 +1,374 @@ +/* + * Copyright 2013-2024 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.client.config; + +import java.util.List; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +@ExtendWith(OutputCaptureExtension.class) +class KubernetesClientConfigMapErrorOnReadingSourceTests { + + private static final V1ConfigMapList SINGLE_CONFIGMAP_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata( + new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build()) + .build()); + + private static final V1ConfigMapList DOUBLE_CONFIGMAP_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata( + new V1ObjectMetaBuilder().withName("one").withNamespace("default").withResourceVersion("1").build()) + .build()) + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata( + new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build()) + .build()); + + private static WireMockServer wireMockServer; + + @BeforeAll + public static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + Configuration.setDefaultApiClient(client); + } + + @AfterAll + public static void after() { + wireMockServer.stop(); + } + + @AfterEach + public void afterEach() { + WireMock.reset(); + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void namedSingleConfigMapFails() { + String name = "my-config"; + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources() + .stream() + .findAny() + .orElseThrow(); + + assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void namedTwoConfigMapsOneFails(CapturedOutput output) { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .willSetStateTo("go-to-next")); + + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SINGLE_CONFIGMAP_LIST))) + .inScenario("started") + .whenScenarioStateIs("go-to-next") + .willSetStateTo("done")); + + ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace, + Map.of(), null, null, null); + ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace, + Map.of(), null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, + RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output.getOut()) + .doesNotContain("sourceName : two was requested, but not found in namespace : default"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void namedTwoConfigMapsBothFail(CapturedOutput output) { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .willSetStateTo("go-to-next")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .whenScenarioStateIs("go-to-next") + .willSetStateTo("done")); + + ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace, + Map.of(), null, null, null); + ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace, + Map.of(), null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, + RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output.getOut()) + .doesNotContain("sourceName : one was requested, but not found in namespace : default"); + assertThat(output.getOut()) + .doesNotContain("sourceName : two was requested, but not found in namespace : default"); + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail,
+	 *     thus generate a well defined name for the source.
+	 * 
+ */ + @Test + void labeledSingleConfigMapFails(CapturedOutput output) { + Map labels = Map.of("a", "b"); + String namespace = "spring-k8s"; + String path = "/api/v1/namespaces/" + namespace + "/configmaps"; + + // one for the 'application' named configmap + // the other for the labeled config map + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .willSetStateTo("go-to-next")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .whenScenarioStateIs("go-to-next") + .willSetStateTo("done")); + + ConfigMapConfigProperties.Source configMapSource = new ConfigMapConfigProperties.Source(null, namespace, labels, + null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsOneFails(CapturedOutput output) { + String configMapNameOne = "one"; + String configMapNameTwo = "two"; + + Map configMapOneLabels = Map.of("one", "1"); + Map configMapTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + // one for 'application' named configmap and one for the first labeled configmap + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .willSetStateTo("first")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .whenScenarioStateIs("first") + .willSetStateTo("second")); + + // one that passes + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(DOUBLE_CONFIGMAP_LIST))) + .inScenario("started") + .whenScenarioStateIs("second") + .willSetStateTo("done")); + + ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace, + configMapOneLabels, null, null, null); + ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace, + configMapTwoLabels, null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, + false, RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // two sources are present, one being empty + assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsBothFail(CapturedOutput output) { + + Map configMapOneLabels = Map.of("one", "1"); + Map configMapTwoLabels = Map.of("two", "2"); + + String namespace = "default"; + String path = "/api/v1/namespaces/default/configmaps"; + + // one for 'application' named configmap and two for the labeled configmaps + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .willSetStateTo("first")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .whenScenarioStateIs("first") + .willSetStateTo("second")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("started") + .whenScenarioStateIs("second") + .willSetStateTo("done")); + + ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace, + configMapOneLabels, null, null, null); + ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace, + configMapTwoLabels, null, null, null); + + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, + false, RetryProperties.DEFAULT); + + CoreV1Api api = new CoreV1Api(); + KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, + configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); + + CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); + List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); + + // all 3 sources ('application' named source, and two labeled sources) + assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + + assertThat(output).contains("failure in reading labeled sources"); + assertThat(output).contains("failure in reading named sources"); + + } + +} From c7c97fabd3afac6e466728f75b852f0db9a7ae3f Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 13 Nov 2024 06:15:41 +0200 Subject: [PATCH 04/13] dirty --- ...Fabric8ConfigMapPropertySourceLocator.java | 12 +++ .../config/reload/ReloadConfigMapTest.java | 93 +++++++++++++++++++ .../ConfigReloadAutoConfigurationTest.java | 4 +- .../KubernetesConfigConfigurationTest.java | 2 +- .../KubernetesConfigTestBase.java | 3 +- 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload => reload_simple}/ConfigReloadAutoConfigurationTest.java (98%) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload => reload_simple}/KubernetesConfigConfigurationTest.java (99%) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload => reload_simple}/KubernetesConfigTestBase.java (94%) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java new file mode 100644 index 0000000000..f9aeee9e76 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java @@ -0,0 +1,12 @@ +package org.springframework.cloud.kubernetes.fabric8.config; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; + +public class VisibleFabric8ConfigMapPropertySourceLocator extends Fabric8ConfigMapPropertySourceLocator { + + public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, KubernetesNamespaceProvider provider) { + super(client, properties, provider); + } +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java new file mode 100644 index 0000000000..08df6bb73b --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.reload; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.mock.env.MockEnvironment; + +import java.time.Duration; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +@SpringBootTest(properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.allow-bean-definition-overriding=true" }, + classes = { ReloadConfigMapTest.TestConfig.class }) +class ReloadConfigMapTest { + + private static boolean [] strategyCalled = new boolean[] { false }; + + @Test + void test() { + assertThat("1").isEqualTo("1"); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy) { + return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + Fabric8ConfigMapPropertySource.class, null, null); + } + + @Bean + @Primary + AbstractEnvironment environment() { + return new MockEnvironment(); + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, + ConfigReloadProperties.ReloadStrategy.REFRESH, ConfigReloadProperties.ReloadDetectionMode.POLLING, + Duration.ofMillis(15000), Set.of("non-default"), false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + + }); + } + + @Bean + @Primary + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator() { + return new Fabric8ConfigMapPropertySourceLocator(); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java similarity index 98% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java index b936510ede..45b83e5c9a 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload; +package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; import java.util.Comparator; import java.util.HashMap; @@ -35,6 +35,8 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationChangeDetector; import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedSecretsChangeDetector; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java similarity index 99% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java index d02bff8d18..171a1f5da6 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload; +package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java similarity index 94% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java index 1ff5685a2d..b4badda78e 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload; +package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; import java.util.Arrays; import java.util.stream.Stream; @@ -27,6 +27,7 @@ import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.bootstrap.BootstrapConfiguration; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadPropertiesAutoConfiguration; +import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8ConfigReloadAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; /** From c681f0acdf46145018bd7b950468caea86a23ff7 Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 13 Nov 2024 06:16:24 +0200 Subject: [PATCH 05/13] dirty --- .../ConfigReloadAutoConfigurationTest.java | 4 +--- .../KubernetesConfigConfigurationTest.java | 2 +- .../{reload_simple => reload}/KubernetesConfigTestBase.java | 3 +-- .../config/{reload => reload_it}/ReloadConfigMapTest.java | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload_simple => reload}/ConfigReloadAutoConfigurationTest.java (98%) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload_simple => reload}/KubernetesConfigConfigurationTest.java (99%) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload_simple => reload}/KubernetesConfigTestBase.java (94%) rename spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/{reload => reload_it}/ReloadConfigMapTest.java (99%) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java similarity index 98% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java index 45b83e5c9a..b936510ede 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/ConfigReloadAutoConfigurationTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ConfigReloadAutoConfigurationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; +package org.springframework.cloud.kubernetes.fabric8.config.reload; import java.util.Comparator; import java.util.HashMap; @@ -35,8 +35,6 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationChangeDetector; import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector; -import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedConfigMapChangeDetector; -import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedSecretsChangeDetector; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java similarity index 99% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java index 171a1f5da6..d02bff8d18 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigConfigurationTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigConfigurationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; +package org.springframework.cloud.kubernetes.fabric8.config.reload; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java similarity index 94% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java index b4badda78e..1ff5685a2d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_simple/KubernetesConfigTestBase.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/KubernetesConfigTestBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload_simple; +package org.springframework.cloud.kubernetes.fabric8.config.reload; import java.util.Arrays; import java.util.stream.Stream; @@ -27,7 +27,6 @@ import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.bootstrap.BootstrapConfiguration; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadPropertiesAutoConfiguration; -import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8ConfigReloadAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; /** diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java similarity index 99% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java index 08df6bb73b..abdc583f5d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/ReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.config.reload; +package org.springframework.cloud.kubernetes.fabric8.config.reload_it; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; From dca7b19ce733c77ea620aff3f01ff2c5a37c10f9 Mon Sep 17 00:00:00 2001 From: wind57 Date: Wed, 13 Nov 2024 23:58:54 +0200 Subject: [PATCH 06/13] add fabric8 it tests --- .../config/reload/ConfigReloadUtil.java | 4 +- ...Fabric8ConfigMapPropertySourceLocator.java | 26 +- ...leFabric8SecretsPropertySourceLocator.java | 36 +++ .../reload_it/EventReloadConfigMapTest.java | 219 +++++++++++++++++ .../reload_it/EventReloadSecretTest.java | 226 ++++++++++++++++++ .../reload_it/PollingReloadConfigMapTest.java | 209 ++++++++++++++++ .../reload_it/PollingReloadSecretTest.java | 216 +++++++++++++++++ .../config/reload_it/ReloadConfigMapTest.java | 93 ------- 8 files changed, 933 insertions(+), 96 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8SecretsPropertySourceLocator.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java create mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java delete mode 100644 spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java index 658c3fc1fd..d1914971cb 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 @@ -72,11 +72,11 @@ public static boolean reload(PropertySourceLocator locator, ConfigurableEnvironm boolean changed = changed(sourceFromK8s, existingSources); if (changed) { - LOG.info("Detected change in config maps/secrets"); + LOG.info("Detected change in config maps/secrets, reload will ne triggered"); return true; } else { - LOG.debug("No change detected in config maps/secrets, reload will not happen"); + LOG.debug("reloadable condition was not satisfied, reload will not be triggered"); } return false; diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java index f9aeee9e76..7dfdfa566f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.java @@ -1,12 +1,36 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.cloud.kubernetes.fabric8.config; import io.fabric8.kubernetes.client.KubernetesClient; + import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +/** + * Only needed to make Fabric8ConfigMapPropertySourceLocator visible for testing purposes + * + * @author wind57 + */ public class VisibleFabric8ConfigMapPropertySourceLocator extends Fabric8ConfigMapPropertySourceLocator { - public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, KubernetesNamespaceProvider provider) { + public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, + KubernetesNamespaceProvider provider) { super(client, properties, provider); } + } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8SecretsPropertySourceLocator.java new file mode 100644 index 0000000000..d0278e4d1f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8SecretsPropertySourceLocator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; + +/** + * Only needed to make Fabric8SecretsPropertySourceLocator visible for testing purposes + * + * @author wind57 + */ +public class VisibleFabric8SecretsPropertySourceLocator extends Fabric8SecretsPropertySourceLocator { + + public VisibleFabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties, + KubernetesNamespaceProvider provider) { + super(client, properties, provider); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java new file mode 100644 index 0000000000..3ad1d94c5a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.reload_it; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedConfigMapChangeDetector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +/** + * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not + * kick in, as we create our own config for the test here. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.reload.enabled=false", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { EventReloadConfigMapTest.TestConfig.class }) +@EnableKubernetesMockClient(crud = true) +@ExtendWith(OutputCaptureExtension.class) +public class EventReloadConfigMapTest { + + private static final boolean FAIL_FAST = false; + + private static final String CONFIG_MAP_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static KubernetesClient kubernetesClient; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + @BeforeAll + static void beforeAll() { + + kubernetesClient = Mockito.spy(kubernetesClient); + kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0); + + ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of()); + + // for the informer, when it starts + kubernetesClient.configMaps().inNamespace(NAMESPACE).resource(configMapOne).create(); + } + + @Test + @SuppressWarnings({ "unchecked" }) + void test(CapturedOutput output) { + + // we need to create this one before mocking calls + NonNamespaceOperation> operation = kubernetesClient.configMaps() + .inNamespace(NAMESPACE); + + // makes sure that when 'onEvent' is triggered (because we added a config map) + // the call to /api/v1/namespaces/spring-k8s/configmaps will fail with an + // Exception + MixedOperation> mixedOperation = Mockito + .mock(MixedOperation.class); + NonNamespaceOperation> mockedOperation = Mockito + .mock(NonNamespaceOperation.class); + Mockito.when(kubernetesClient.configMaps()).thenReturn(mixedOperation); + Mockito.when(mixedOperation.inNamespace(NAMESPACE)).thenReturn(mockedOperation); + Mockito.when(mockedOperation.list()).thenThrow(new RuntimeException("failed in reading configmap")); + + // create a different configmap that triggers even based reloading. + // the one we create, will trigger a call to + // /api/v1/namespaces/spring-k8s/configmaps + // that we mocked above to fail. + ConfigMap configMapTwo = configMap("not" + CONFIG_MAP_NAME, Map.of("a", "b")); + operation.resource(configMapTwo).create(); + + // we fail while reading 'configMapTwo' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // reset the mock and replace our configmap with some data, so that reload + // is triggered + Mockito.reset(kubernetesClient); + ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); + operation.resource(configMapOne).replace(); + + // it passes while reading 'configMapThatWillPass' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static ConfigMap configMap(String name, Map data) { + return new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + Fabric8EventBasedConfigMapChangeDetector fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator, + KubernetesNamespaceProvider namespaceProvider) { + return new Fabric8EventBasedConfigMapChangeDetector(environment, configReloadProperties, kubernetesClient, + configurationUpdateStrategy, fabric8ConfigMapPropertySourceLocator, namespaceProvider); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a Fabric8ConfigMapPropertySource, + // otherwise we can't properly test reload functionality + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, + configMapConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.EVENT, Duration.ofMillis(2000), Set.of(NAMESPACE), false, + Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigMapConfigProperties configMapConfigProperties() { + return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator( + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, configMapConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java new file mode 100644 index 0000000000..772ca71c1f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java @@ -0,0 +1,226 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.reload_it; + +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.SecretList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedSecretsChangeDetector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +/** + * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not + * kick in, as we create our own config for the test here. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.reload.enabled=false", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { EventReloadSecretTest.TestConfig.class }) +@EnableKubernetesMockClient(crud = true) +@ExtendWith(OutputCaptureExtension.class) + +class EventReloadSecretTest { + + private static final boolean FAIL_FAST = false; + + private static final String SECRET_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static KubernetesClient kubernetesClient; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + @BeforeAll + static void beforeAll() { + + kubernetesClient = Mockito.spy(kubernetesClient); + kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0); + + Secret secretOne = secret(SECRET_NAME, Map.of()); + + // for the informer, when it starts + kubernetesClient.secrets().inNamespace(NAMESPACE).resource(secretOne).create(); + } + + @Test + @SuppressWarnings({ "unchecked" }) + void test(CapturedOutput output) { + + // we need to create this one before mocking calls + NonNamespaceOperation> operation = kubernetesClient.secrets() + .inNamespace(NAMESPACE); + + // makes sure that when 'onEvent' is triggered (because we added a config map) + // the call to /api/v1/namespaces/spring-k8s/secrets will fail with an + // Exception + MixedOperation> mixedOperation = Mockito.mock(MixedOperation.class); + NonNamespaceOperation> mockedOperation = Mockito + .mock(NonNamespaceOperation.class); + Mockito.when(kubernetesClient.secrets()).thenReturn(mixedOperation); + Mockito.when(mixedOperation.inNamespace(NAMESPACE)).thenReturn(mockedOperation); + Mockito.when(mockedOperation.list()).thenThrow(new RuntimeException("failed in reading secret")); + + // create a different secret that triggers even based reloading. + // the one we create, will trigger a call to + // /api/v1/namespaces/spring-k8s/secrets + // that we mocked above to fail. + Secret secretTwo = secret("not" + SECRET_NAME, Map.of("a", "b")); + operation.resource(secretTwo).create(); + + // we fail while reading 'secretTwo' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // reset the mock and replace our secret with some data, so that reload + // is triggered + Mockito.reset(kubernetesClient); + Secret secretOne = secret(SECRET_NAME, Map.of("a", "b")); + operation.resource(secretOne).replace(); + + // it passes while reading 'secretOne' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static Secret secret(String name, Map data) { + Map encoded = data.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> new String(Base64.getEncoder().encode(e.getValue().getBytes())))); + return new SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + Fabric8EventBasedSecretsChangeDetector fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator, + KubernetesNamespaceProvider namespaceProvider) { + return new Fabric8EventBasedSecretsChangeDetector(environment, configReloadProperties, kubernetesClient, + configurationUpdateStrategy, fabric8SecretsPropertySourceLocator, namespaceProvider); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a + // Fabric8SecretsPropertySourceLocator, + // otherwise we can't properly test reload functionality + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, + secretsConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.EVENT, Duration.ofMillis(2000), Set.of(NAMESPACE), false, + Duration.ofSeconds(2)); + } + + @Bean + @Primary + SecretsConfigProperties secretsConfigProperties() { + return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator( + SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, secretsConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java new file mode 100644 index 0000000000..bee4cbc3c9 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.reload_it; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapListBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8ConfigMapPropertySourceLocator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not + * kick in, as we create our own config for the test here. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.reload.enabled=false", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadConfigMapTest.TestConfig.class }) +@EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) +class PollingReloadConfigMapTest { + + private static final boolean FAIL_FAST = false; + + private static final String CONFIG_MAP_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static KubernetesMockServer kubernetesMockServer; + + private static KubernetesClient kubernetesClient; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + @BeforeAll + static void beforeAll() { + + kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of()); + ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); + String path = "/api/v1/namespaces/spring-k8s/configmaps"; + kubernetesMockServer.expect() + .withPath(path) + .andReturn(200, new ConfigMapListBuilder().withItems(configMapOne).build()) + .once(); + + kubernetesMockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + kubernetesMockServer.expect() + .withPath(path) + .andReturn(200, new ConfigMapListBuilder().withItems(configMapTwo).build()) + .once(); + } + + /** + *
+	 *     - we have a PropertySource in the environment
+	 *     - first polling cycle tries to read the sources from k8s and fails
+	 *     - second polling cycle reads sources from k8s and finds a change
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + // we fail while reading 'configMapOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // it passes while reading 'configMapTwo' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static ConfigMap configMap(String name, Map data) { + return new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + Fabric8ConfigMapPropertySource.class, fabric8ConfigMapPropertySourceLocator, scheduler); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a Fabric8ConfigMapPropertySource, + // otherwise we can't properly test reload functionality + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, + configMapConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigMapConfigProperties configMapConfigProperties() { + return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator( + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, configMapConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java new file mode 100644 index 0000000000..dcc147f90e --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config.reload_it; + +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.SecretListBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySource; +import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8SecretsPropertySourceLocator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not + * kick in, as we create our own config for the test here. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.reload.enabled=false", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadSecretTest.TestConfig.class }) +@EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) + +public class PollingReloadSecretTest { + + private static final boolean FAIL_FAST = false; + + private static final String SECRET_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static KubernetesMockServer kubernetesMockServer; + + private static KubernetesClient kubernetesClient; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + @BeforeAll + static void beforeAll() { + + kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + Secret secretOne = secret(SECRET_NAME, Map.of()); + Secret secretTwo = secret(SECRET_NAME, Map.of("a", "b")); + String path = "/api/v1/namespaces/spring-k8s/secrets"; + kubernetesMockServer.expect() + .withPath(path) + .andReturn(200, new SecretListBuilder().withItems(secretOne).build()) + .once(); + + kubernetesMockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); + + kubernetesMockServer.expect() + .withPath(path) + .andReturn(200, new SecretListBuilder().withItems(secretTwo).build()) + .once(); + } + + /** + *
+	 *     - we have a PropertySource in the environment
+	 *     - first polling cycle tries to read the sources from k8s and fails
+	 *     - second polling cycle reads sources from k8s and finds a change
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + // we fail while reading 'secretOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // it passes while reading 'secretTwo' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static Secret secret(String name, Map data) { + Map encoded = data.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> new String(Base64.getEncoder().encode(e.getValue().getBytes())))); + return new SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + Fabric8SecretsPropertySource.class, fabric8SecretsPropertySourceLocator, scheduler); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a Fabric8SecretsPropertySource, + // otherwise we can't properly test reload functionality + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, + secretsConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, true, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + SecretsConfigProperties secretsConfigProperties() { + return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator( + SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, secretsConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java deleted file mode 100644 index abdc583f5d..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/ReloadConfigMapTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2012-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.fabric8.config.reload_it; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; -import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; -import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; -import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource; -import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.core.env.AbstractEnvironment; -import org.springframework.mock.env.MockEnvironment; - -import java.time.Duration; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author wind57 - */ -@SpringBootTest(properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.kubernetes.reload.enabled=true", - "spring.main.allow-bean-definition-overriding=true" }, - classes = { ReloadConfigMapTest.TestConfig.class }) -class ReloadConfigMapTest { - - private static boolean [] strategyCalled = new boolean[] { false }; - - @Test - void test() { - assertThat("1").isEqualTo("1"); - } - - @TestConfiguration - static class TestConfig { - - @Bean - @Primary - PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, - ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy) { - return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, - Fabric8ConfigMapPropertySource.class, null, null); - } - - @Bean - @Primary - AbstractEnvironment environment() { - return new MockEnvironment(); - } - - @Bean - @Primary - ConfigReloadProperties configReloadProperties() { - return new ConfigReloadProperties(true, true, false, - ConfigReloadProperties.ReloadStrategy.REFRESH, ConfigReloadProperties.ReloadDetectionMode.POLLING, - Duration.ofMillis(15000), Set.of("non-default"), false, Duration.ofSeconds(2)); - } - - @Bean - @Primary - ConfigurationUpdateStrategy configurationUpdateStrategy() { - return new ConfigurationUpdateStrategy("to-console", () -> { - - }); - } - - @Bean - @Primary - Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator() { - return new Fabric8ConfigMapPropertySourceLocator(); - } - - } - -} From 0c00b75e6205361d481a81242bc9aeee7ecfe5ee Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 14 Nov 2024 10:40:53 +0200 Subject: [PATCH 07/13] dirty --- ...ientSecretsPropertySourceLocatorTests.java | 70 ++++-- ...netesClientSecretsPropertySourceTests.java | 70 ++++-- .../reload_it/PollingReloadConfigMapTest.java | 231 ++++++++++++++++++ 3 files changed, 327 insertions(+), 44 deletions(-) create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java index b9ecb61cc0..097a89663d 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java @@ -54,28 +54,54 @@ class KubernetesClientSecretsPropertySourceLocatorTests { private static final String LIST_API = "/api/v1/namespaces/default/secrets"; - private static final String LIST_BODY = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n" - + "\t\"metadata\": {\n" + "\t\t\"selfLink\": \"/api/v1/secrets\",\n" - + "\t\t\"resourceVersion\": \"163035\"\n" + "\t},\n" + "\t\"items\": [{\n" + "\t\t\t\"metadata\": {\n" - + "\t\t\t\t\"name\": \"db-secret\",\n" + "\t\t\t\t\"namespace\": \"default\",\n" - + "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/db-secret\",\n" - + "\t\t\t\t\"uid\": \"59ba8e6a-a2d4-416c-b016-22597c193f23\",\n" - + "\t\t\t\t\"resourceVersion\": \"1462\",\n" + "\t\t\t\t\"creationTimestamp\": \"2020-10-28T14:45:02Z\",\n" - + "\t\t\t\t\"labels\": {\n" + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t}\n" - + "\t\t\t},\n" + "\t\t\t\"data\": {\n" + "\t\t\t\t\"password\": \"cDQ1NXcwcmQ=\",\n" - + "\t\t\t\t\"username\": \"dXNlcg==\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" + "\t\t},\n" - + "\t\t{\n" + "\t\t\t\"metadata\": {\n" + "\t\t\t\t\"name\": \"rabbit-password\",\n" - + "\t\t\t\t\"namespace\": \"default\",\n" - + "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/rabbit-password\",\n" - + "\t\t\t\t\"uid\": \"bc211cb4-e7ff-4556-b26e-c54911301740\",\n" - + "\t\t\t\t\"resourceVersion\": \"162708\",\n" - + "\t\t\t\t\"creationTimestamp\": \"2020-10-29T19:47:36Z\",\n" + "\t\t\t\t\"labels\": {\n" - + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t},\n" - + "\t\t\t\t\"annotations\": {\n" - + "\t\t\t\t\t\"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"spring.rabbitmq.password\\\":\\\"password\\\"},\\\"kind\\\":\\\"Secret\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"spring.cloud.kubernetes.secret\\\":\\\"true\\\"},\\\"name\\\":\\\"rabbit-password\\\",\\\"namespace\\\":\\\"default\\\"},\\\"type\\\":\\\"Opaque\\\"}\\n\"\n" - + "\t\t\t\t}\n" + "\t\t\t},\n" + "\t\t\t\"data\": {\n" - + "\t\t\t\t\"spring.rabbitmq.password\": \"cGFzc3dvcmQ=\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" - + "\t\t}\n" + "\t]\n" + "}"; + private static final String LIST_BODY = """ + { + \t"kind": "SecretList", + \t"apiVersion": "v1", + \t"metadata": { + \t\t"selfLink": "/api/v1/secrets", + \t\t"resourceVersion": "163035" + \t}, + \t"items": [{ + \t\t\t"metadata": { + \t\t\t\t"name": "db-secret", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", + \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", + \t\t\t\t"resourceVersion": "1462", + \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"password": "cDQ1NXcwcmQ=", + \t\t\t\t"username": "dXNlcg==" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t}, + \t\t{ + \t\t\t"metadata": { + \t\t\t\t"name": "rabbit-password", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", + \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", + \t\t\t\t"resourceVersion": "162708", + \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t}, + \t\t\t\t"annotations": { + \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t} + \t] + }"""; private static WireMockServer wireMockServer; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java index 4cc2b8d231..e8c3533e5e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java @@ -80,28 +80,54 @@ class KubernetesClientSecretsPropertySourceTests { private static final String LIST_API_WITH_LABEL = "/api/v1/namespaces/default/secrets"; - private static final String LIST_BODY = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n" - + "\t\"metadata\": {\n" + "\t\t\"selfLink\": \"/api/v1/secrets\",\n" - + "\t\t\"resourceVersion\": \"163035\"\n" + "\t},\n" + "\t\"items\": [{\n" + "\t\t\t\"metadata\": {\n" - + "\t\t\t\t\"name\": \"db-secret\",\n" + "\t\t\t\t\"namespace\": \"default\",\n" - + "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/db-secret\",\n" - + "\t\t\t\t\"uid\": \"59ba8e6a-a2d4-416c-b016-22597c193f23\",\n" - + "\t\t\t\t\"resourceVersion\": \"1462\",\n" + "\t\t\t\t\"creationTimestamp\": \"2020-10-28T14:45:02Z\",\n" - + "\t\t\t\t\"labels\": {\n" + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t}\n" - + "\t\t\t},\n" + "\t\t\t\"data\": {\n" + "\t\t\t\t\"password\": \"cDQ1NXcwcmQ=\",\n" - + "\t\t\t\t\"username\": \"dXNlcg==\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" + "\t\t},\n" - + "\t\t{\n" + "\t\t\t\"metadata\": {\n" + "\t\t\t\t\"name\": \"rabbit-password\",\n" - + "\t\t\t\t\"namespace\": \"default\",\n" - + "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/rabbit-password\",\n" - + "\t\t\t\t\"uid\": \"bc211cb4-e7ff-4556-b26e-c54911301740\",\n" - + "\t\t\t\t\"resourceVersion\": \"162708\",\n" - + "\t\t\t\t\"creationTimestamp\": \"2020-10-29T19:47:36Z\",\n" + "\t\t\t\t\"labels\": {\n" - + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t},\n" - + "\t\t\t\t\"annotations\": {\n" - + "\t\t\t\t\t\"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"spring.rabbitmq.password\\\":\\\"password\\\"},\\\"kind\\\":\\\"Secret\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"spring.cloud.kubernetes.secret\\\":\\\"true\\\"},\\\"name\\\":\\\"rabbit-password\\\",\\\"namespace\\\":\\\"default\\\"},\\\"type\\\":\\\"Opaque\\\"}\\n\"\n" - + "\t\t\t\t}\n" + "\t\t\t},\n" + "\t\t\t\"data\": {\n" - + "\t\t\t\t\"spring.rabbitmq.password\": \"cGFzc3dvcmQ=\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" - + "\t\t}\n" + "\t]\n" + "}"; + private static final String LIST_BODY = """ + { + \t"kind": "SecretList", + \t"apiVersion": "v1", + \t"metadata": { + \t\t"selfLink": "/api/v1/secrets", + \t\t"resourceVersion": "163035" + \t}, + \t"items": [{ + \t\t\t"metadata": { + \t\t\t\t"name": "db-secret", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", + \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", + \t\t\t\t"resourceVersion": "1462", + \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"password": "cDQ1NXcwcmQ=", + \t\t\t\t"username": "dXNlcg==" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t}, + \t\t{ + \t\t\t"metadata": { + \t\t\t\t"name": "rabbit-password", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", + \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", + \t\t\t\t"resourceVersion": "162708", + \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t}, + \t\t\t\t"annotations": { + \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t} + \t] + }"""; private static WireMockServer wireMockServer; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java new file mode 100644 index 0000000000..18c60b0f07 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2013-2024 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.client.config.reload_it; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +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.V1ConfigMapList; +import io.kubernetes.client.util.ClientBuilder; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not + * kick in, as we create our own config for the test here. + * + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.reload.enabled=false", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadConfigMapTest.TestConfig.class }) +@ExtendWith(OutputCaptureExtension.class) +class PollingReloadConfigMapTest { + + private static WireMockServer wireMockServer; + + private static final boolean FAIL_FAST = false; + + private static final String CONFIG_MAP_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + private static CoreV1Api coreV1Api; + + @BeforeAll + static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + Configuration.setDefaultApiClient(client); + coreV1Api = new CoreV1Api(); + + String path = "/api/v1/namespaces/spring-k8s/configmaps"; + V1ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of()); + V1ConfigMapList listOne = new V1ConfigMapList().addItemsItem(configMapOne); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("my-test").willSetStateTo("go-to-fail")); + + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("my-test").whenScenarioStateIs("go-to-fail").willSetStateTo("go-to-ok")); + + V1ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); + V1ConfigMapList listTwo = new V1ConfigMapList().addItemsItem(configMapTwo); + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo))) + .inScenario("my-test").whenScenarioStateIs("go-to-ok")); + + } + + @AfterAll + static void after() { + wireMockServer.stop(); + } + + /** + *
+	 *     - we have a PropertySource in the environment
+	 *     - first polling cycle tries to read the sources from k8s and fails
+	 *     - second polling cycle reads sources from k8s and finds a change
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + // we fail while reading 'configMapOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // it passes while reading 'configMapTwo' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static V1ConfigMap configMap(String name, Map data) { + return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata() + .withData(data).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator, scheduler); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a Fabric8ConfigMapPropertySource, + // otherwise we can't properly test reload functionality + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, + configMapConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigMapConfigProperties configMapConfigProperties() { + return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator( + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, + namespaceProvider); + } + + } + +} From 691c2ac254138a74222205f96726d2661e43d28e Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 14 Nov 2024 11:03:11 +0200 Subject: [PATCH 08/13] dirty --- ...ubernetesClientBootstrapConfiguration.java | 2 + .../reload_it/PollingReloadConfigMapTest.java | 8 +- .../reload_it/PollingReloadSecretTest.java | 239 ++++++++++++++++++ .../reload_it/EventReloadConfigMapTest.java | 6 +- .../reload_it/EventReloadSecretTest.java | 6 +- .../reload_it/PollingReloadConfigMapTest.java | 6 +- .../reload_it/PollingReloadSecretTest.java | 6 +- 7 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientBootstrapConfiguration.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientBootstrapConfiguration.java index 91ae4eb408..9d6992d4be 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientBootstrapConfiguration.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientBootstrapConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.KubernetesBootstrapConfiguration; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.cloud.util.ConditionalOnBootstrapEnabled; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -41,6 +42,7 @@ @Configuration(proxyBeanMethods = false) @AutoConfigureAfter(KubernetesBootstrapConfiguration.class) @Import({ KubernetesCommonsAutoConfiguration.class, KubernetesClientAutoConfiguration.class }) +@ConditionalOnBootstrapEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) public class KubernetesClientBootstrapConfiguration { diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java index 18c60b0f07..d7e74a12ad 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -62,14 +62,10 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; /** - * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not - * kick in, as we create our own config for the test here. - * * @author wind57 */ @SpringBootTest( - properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", - "spring.cloud.kubernetes.reload.enabled=false", + properties = {"spring.main.allow-bean-definition-overriding=true", "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, classes = { PollingReloadConfigMapTest.TestConfig.class }) @ExtendWith(OutputCaptureExtension.class) @@ -108,9 +104,11 @@ static void setup() { stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) .inScenario("my-test").willSetStateTo("go-to-fail")); + // first reload call fails stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) .inScenario("my-test").whenScenarioStateIs("go-to-fail").willSetStateTo("go-to-ok")); + // second reload call passes V1ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); V1ConfigMapList listTwo = new V1ConfigMapList().addItemsItem(configMapTwo); stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo))) diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java new file mode 100644 index 0000000000..7fc2f57a4e --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2013-2024 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.client.config.reload_it; + +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +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.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.ClientBuilder; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = {"spring.main.allow-bean-definition-overriding=true", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadConfigMapTest.TestConfig.class }) +@ExtendWith(OutputCaptureExtension.class) +class PollingReloadSecretTest { + + private static WireMockServer wireMockServer; + + private static final boolean FAIL_FAST = false; + + private static final String SECRET_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + private static CoreV1Api coreV1Api; + + @BeforeAll + static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + Configuration.setDefaultApiClient(client); + coreV1Api = new CoreV1Api(); + + String path = "/api/v1/namespaces/spring-k8s/secrets"; + V1Secret secretOne = secret(SECRET_NAME, Map.of()); + V1SecretList listOne = new V1SecretList().addItemsItem(secretOne); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("my-test").willSetStateTo("go-to-fail")); + + // first reload call fails + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("my-test").whenScenarioStateIs("go-to-fail").willSetStateTo("go-to-ok")); + + V1Secret secretTwo = secret(SECRET_NAME, Map.of("a", "b")); + V1SecretList listTwo = new V1SecretList().addItemsItem(secretTwo); + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo))) + .inScenario("my-test").whenScenarioStateIs("go-to-ok")); + + } + + @AfterAll + static void after() { + wireMockServer.stop(); + } + + /** + *
+	 *     - we have a PropertySource in the environment
+	 *     - first polling cycle tries to read the sources from k8s and fails
+	 *     - second polling cycle reads sources from k8s and finds a change
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + // we fail while reading 'configMapOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // it passes while reading 'configMapTwo' + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static V1Secret secret(String name, Map data) { + + Map encoded = data.entrySet().stream().collect( + Collectors.toMap(e -> e.getKey(), e -> Base64.getEncoder().encode(e.getValue().getBytes())) + ); + + return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata() + .withData(encoded).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator, scheduler); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a Fabric8ConfigMapPropertySource, + // otherwise we can't properly test reload functionality + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, + configMapConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigMapConfigProperties configMapConfigProperties() { + return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator( + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java index 3ad1d94c5a..d14efbabab 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java @@ -54,14 +54,10 @@ import org.springframework.mock.env.MockEnvironment; /** - * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not - * kick in, as we create our own config for the test here. - * * @author wind57 */ @SpringBootTest( - properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", - "spring.cloud.kubernetes.reload.enabled=false", + properties = { "spring.main.allow-bean-definition-overriding=true", "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, classes = { EventReloadConfigMapTest.TestConfig.class }) @EnableKubernetesMockClient(crud = true) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java index 772ca71c1f..546ae964b4 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java @@ -56,14 +56,10 @@ import org.springframework.mock.env.MockEnvironment; /** - * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not - * kick in, as we create our own config for the test here. - * * @author wind57 */ @SpringBootTest( - properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", - "spring.cloud.kubernetes.reload.enabled=false", + properties = { "spring.main.allow-bean-definition-overriding=true", "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, classes = { EventReloadSecretTest.TestConfig.class }) @EnableKubernetesMockClient(crud = true) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java index bee4cbc3c9..372ca8be31 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java @@ -53,14 +53,10 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** - * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not - * kick in, as we create our own config for the test here. - * * @author wind57 */ @SpringBootTest( - properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", - "spring.cloud.kubernetes.reload.enabled=false", + properties = { "spring.main.allow-bean-definition-overriding=true", "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, classes = { PollingReloadConfigMapTest.TestConfig.class }) @EnableKubernetesMockClient diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java index dcc147f90e..68667637ce 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java @@ -55,14 +55,10 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** - * set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not - * kick in, as we create our own config for the test here. - * * @author wind57 */ @SpringBootTest( - properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true", - "spring.cloud.kubernetes.reload.enabled=false", + properties = { "spring.main.allow-bean-definition-overriding=true", "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, classes = { PollingReloadSecretTest.TestConfig.class }) @EnableKubernetesMockClient From e21af5944c506710a36d5479f6ffc1a152505f9a Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 14 Nov 2024 11:19:52 +0200 Subject: [PATCH 09/13] dirty --- .../reload_it/PollingReloadSecretTest.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java index 7fc2f57a4e..7c4d266c73 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java @@ -49,12 +49,16 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.core.env.AbstractEnvironment; @@ -168,13 +172,13 @@ static class TestConfig { @Bean @Primary - PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment, + PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment environment, ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, - KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator) { + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.initialize(); - return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, - KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator, scheduler); + return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + KubernetesClientSecretsPropertySource.class, kubernetesClientSecretsPropertySourceLocator, scheduler); } @Bean @@ -207,8 +211,8 @@ ConfigReloadProperties configReloadProperties() { @Bean @Primary - ConfigMapConfigProperties configMapConfigProperties() { - return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, + SecretsConfigProperties configMapConfigProperties() { + return new SecretsConfigProperties(true, List.of(), List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT); } @@ -228,7 +232,7 @@ ConfigurationUpdateStrategy configurationUpdateStrategy() { @Bean @Primary - KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator( + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator( ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, namespaceProvider); From 709414614b88f3053be061fc4d1b3b67a524335b Mon Sep 17 00:00:00 2001 From: wind57 Date: Mon, 18 Nov 2024 19:19:32 +0200 Subject: [PATCH 10/13] dirty --- ...ientSecretsPropertySourceLocatorTests.java | 94 +++---- ...netesClientSecretsPropertySourceTests.java | 94 +++---- ...ientEventBasedConfigMapChangeDetector.java | 45 +++ ...ClientEventBasedSecretsChangeDetector.java | 46 ++++ .../reload_it/EventReloadConfigMapTest.java | 255 +++++++++++++++++ .../reload_it/EventReloadSecretTest.java | 260 ++++++++++++++++++ .../reload_it/PollingReloadConfigMapTest.java | 41 +-- .../reload_it/PollingReloadSecretTest.java | 71 +++-- 8 files changed, 757 insertions(+), 149 deletions(-) create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedConfigMapChangeDetector.java create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedSecretsChangeDetector.java create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java create mode 100644 spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java index 097a89663d..5bcf6ca005 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java @@ -55,53 +55,53 @@ class KubernetesClientSecretsPropertySourceLocatorTests { private static final String LIST_API = "/api/v1/namespaces/default/secrets"; private static final String LIST_BODY = """ - { - \t"kind": "SecretList", - \t"apiVersion": "v1", - \t"metadata": { - \t\t"selfLink": "/api/v1/secrets", - \t\t"resourceVersion": "163035" - \t}, - \t"items": [{ - \t\t\t"metadata": { - \t\t\t\t"name": "db-secret", - \t\t\t\t"namespace": "default", - \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", - \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", - \t\t\t\t"resourceVersion": "1462", - \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", - \t\t\t\t"labels": { - \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" - \t\t\t\t} - \t\t\t}, - \t\t\t"data": { - \t\t\t\t"password": "cDQ1NXcwcmQ=", - \t\t\t\t"username": "dXNlcg==" - \t\t\t}, - \t\t\t"type": "Opaque" - \t\t}, - \t\t{ - \t\t\t"metadata": { - \t\t\t\t"name": "rabbit-password", - \t\t\t\t"namespace": "default", - \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", - \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", - \t\t\t\t"resourceVersion": "162708", - \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", - \t\t\t\t"labels": { - \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" - \t\t\t\t}, - \t\t\t\t"annotations": { - \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" - \t\t\t\t} - \t\t\t}, - \t\t\t"data": { - \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" - \t\t\t}, - \t\t\t"type": "Opaque" - \t\t} - \t] - }"""; + { + \t"kind": "SecretList", + \t"apiVersion": "v1", + \t"metadata": { + \t\t"selfLink": "/api/v1/secrets", + \t\t"resourceVersion": "163035" + \t}, + \t"items": [{ + \t\t\t"metadata": { + \t\t\t\t"name": "db-secret", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", + \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", + \t\t\t\t"resourceVersion": "1462", + \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"password": "cDQ1NXcwcmQ=", + \t\t\t\t"username": "dXNlcg==" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t}, + \t\t{ + \t\t\t"metadata": { + \t\t\t\t"name": "rabbit-password", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", + \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", + \t\t\t\t"resourceVersion": "162708", + \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t}, + \t\t\t\t"annotations": { + \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t} + \t] + }"""; private static WireMockServer wireMockServer; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java index e8c3533e5e..50173d3918 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java @@ -81,53 +81,53 @@ class KubernetesClientSecretsPropertySourceTests { private static final String LIST_API_WITH_LABEL = "/api/v1/namespaces/default/secrets"; private static final String LIST_BODY = """ - { - \t"kind": "SecretList", - \t"apiVersion": "v1", - \t"metadata": { - \t\t"selfLink": "/api/v1/secrets", - \t\t"resourceVersion": "163035" - \t}, - \t"items": [{ - \t\t\t"metadata": { - \t\t\t\t"name": "db-secret", - \t\t\t\t"namespace": "default", - \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", - \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", - \t\t\t\t"resourceVersion": "1462", - \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", - \t\t\t\t"labels": { - \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" - \t\t\t\t} - \t\t\t}, - \t\t\t"data": { - \t\t\t\t"password": "cDQ1NXcwcmQ=", - \t\t\t\t"username": "dXNlcg==" - \t\t\t}, - \t\t\t"type": "Opaque" - \t\t}, - \t\t{ - \t\t\t"metadata": { - \t\t\t\t"name": "rabbit-password", - \t\t\t\t"namespace": "default", - \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", - \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", - \t\t\t\t"resourceVersion": "162708", - \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", - \t\t\t\t"labels": { - \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" - \t\t\t\t}, - \t\t\t\t"annotations": { - \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" - \t\t\t\t} - \t\t\t}, - \t\t\t"data": { - \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" - \t\t\t}, - \t\t\t"type": "Opaque" - \t\t} - \t] - }"""; + { + \t"kind": "SecretList", + \t"apiVersion": "v1", + \t"metadata": { + \t\t"selfLink": "/api/v1/secrets", + \t\t"resourceVersion": "163035" + \t}, + \t"items": [{ + \t\t\t"metadata": { + \t\t\t\t"name": "db-secret", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret", + \t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23", + \t\t\t\t"resourceVersion": "1462", + \t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"password": "cDQ1NXcwcmQ=", + \t\t\t\t"username": "dXNlcg==" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t}, + \t\t{ + \t\t\t"metadata": { + \t\t\t\t"name": "rabbit-password", + \t\t\t\t"namespace": "default", + \t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password", + \t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740", + \t\t\t\t"resourceVersion": "162708", + \t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z", + \t\t\t\t"labels": { + \t\t\t\t\t"spring.cloud.kubernetes.secret": "true" + \t\t\t\t}, + \t\t\t\t"annotations": { + \t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n" + \t\t\t\t} + \t\t\t}, + \t\t\t"data": { + \t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ=" + \t\t\t}, + \t\t\t"type": "Opaque" + \t\t} + \t] + }"""; private static WireMockServer wireMockServer; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedConfigMapChangeDetector.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedConfigMapChangeDetector.java new file mode 100644 index 0000000000..3045521363 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedConfigMapChangeDetector.java @@ -0,0 +1,45 @@ +/* + * 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.client.config; + +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.cloud.kubernetes.client.config.reload.KubernetesClientEventBasedConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author wind57 + */ +public class VisibleKubernetesClientEventBasedConfigMapChangeDetector + extends KubernetesClientEventBasedConfigMapChangeDetector { + + public VisibleKubernetesClientEventBasedConfigMapChangeDetector(CoreV1Api coreV1Api, + ConfigurableEnvironment environment, ConfigReloadProperties properties, + ConfigurationUpdateStrategy strategy, KubernetesClientConfigMapPropertySourceLocator propertySourceLocator, + KubernetesNamespaceProvider kubernetesNamespaceProvider) { + super(coreV1Api, environment, properties, strategy, propertySourceLocator, kubernetesNamespaceProvider); + } + + public void onEvent(KubernetesObject kubernetesObject) { + super.onEvent(kubernetesObject); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedSecretsChangeDetector.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedSecretsChangeDetector.java new file mode 100644 index 0000000000..8cc7540e35 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/VisibleKubernetesClientEventBasedSecretsChangeDetector.java @@ -0,0 +1,46 @@ +/* + * 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.client.config; + +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.cloud.kubernetes.client.config.reload.KubernetesClientEventBasedSecretsChangeDetector; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author wind57 + */ +public class VisibleKubernetesClientEventBasedSecretsChangeDetector + extends KubernetesClientEventBasedSecretsChangeDetector { + + public VisibleKubernetesClientEventBasedSecretsChangeDetector(CoreV1Api coreV1Api, + ConfigurableEnvironment environment, ConfigReloadProperties properties, + ConfigurationUpdateStrategy strategy, KubernetesClientSecretsPropertySourceLocator propertySourceLocator, + KubernetesNamespaceProvider kubernetesNamespaceProvider) { + super(coreV1Api, environment, properties, strategy, propertySourceLocator, kubernetesNamespaceProvider); + } + + @Override + public void onEvent(KubernetesObject kubernetesObject) { + super.onEvent(kubernetesObject); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java new file mode 100644 index 0000000000..a0d458f710 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java @@ -0,0 +1,255 @@ +/* + * Copyright 2013-2024 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.client.config.reload_it; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +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.V1ConfigMapList; +import io.kubernetes.client.util.ClientBuilder; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedConfigMapChangeDetector; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { EventReloadConfigMapTest.TestConfig.class }) +@ExtendWith(OutputCaptureExtension.class) +class EventReloadConfigMapTest { + + private static final boolean FAIL_FAST = false; + + private static WireMockServer wireMockServer; + + private static final String CONFIG_MAP_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + private static CoreV1Api coreV1Api; + + private static final MockedStatic MOCK_STATIC = Mockito + .mockStatic(KubernetesClientUtils.class); + + @Autowired + private VisibleKubernetesClientEventBasedConfigMapChangeDetector kubernetesClientEventBasedConfigMapChangeDetector; + + @BeforeAll + static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + MOCK_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient).thenReturn(client); + MOCK_STATIC + .when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(), + Mockito.any())) + .thenReturn(NAMESPACE); + Configuration.setDefaultApiClient(client); + coreV1Api = new CoreV1Api(); + + String path = "/api/v1/namespaces/spring-k8s/configmaps"; + V1ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of()); + V1ConfigMapList listOne = new V1ConfigMapList().addItemsItem(configMapOne); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("mine-test") + .willSetStateTo("go-to-fail")); + + // first call will fail + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("mine-test") + .whenScenarioStateIs("go-to-fail") + .willSetStateTo("go-to-ok")); + + // second call passes (change data so that reload is triggered) + configMapOne = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); + listOne = new V1ConfigMapList().addItemsItem(configMapOne); + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("mine-test") + .whenScenarioStateIs("go-to-ok") + .willSetStateTo("done")); + } + + @AfterAll + static void after() { + MOCK_STATIC.close(); + wireMockServer.stop(); + } + + /** + *
+	 * 	- 'configmap.mine.spring-k8s' already exists in the environment
+	 * 	-  we simulate that another configmap is created, so a request goes to k8s to find any potential
+	 * 	   differences. This request is mocked to fail.
+	 * 	- then our configmap is changed and the request passes
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + V1ConfigMap configMapNotMine = configMap("not" + CONFIG_MAP_NAME, Map.of()); + kubernetesClientEventBasedConfigMapChangeDetector.onEvent(configMapNotMine); + + // we fail while reading 'configMapOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // trigger the call again + V1ConfigMap configMapMine = configMap(CONFIG_MAP_NAME, Map.of()); + kubernetesClientEventBasedConfigMapChangeDetector.onEvent(configMapMine); + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static V1ConfigMap configMap(String name, Map data) { + return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + VisibleKubernetesClientEventBasedConfigMapChangeDetector kubernetesClientEventBasedConfigMapChangeDetector( + AbstractEnvironment environment, ConfigReloadProperties configReloadProperties, + ConfigurationUpdateStrategy configurationUpdateStrategy, + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator, + KubernetesNamespaceProvider namespaceProvider) { + return new VisibleKubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment, + configReloadProperties, configurationUpdateStrategy, kubernetesClientConfigMapPropertySourceLocator, + namespaceProvider); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a + // KubernetesClientConfigMapPropertySource, + // otherwise we can't properly test reload functionality + ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, FAIL_FAST, + RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, + configMapConfigProperties, namespaceProvider) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigMapConfigProperties configMapConfigProperties() { + return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator( + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, + namespaceProvider); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java new file mode 100644 index 0000000000..cc4af32b82 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java @@ -0,0 +1,260 @@ +/* + * Copyright 2013-2024 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.client.config.reload_it; + +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.ClientBuilder; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedSecretsChangeDetector; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { EventReloadSecretTest.TestConfig.class }) +@ExtendWith(OutputCaptureExtension.class) +class EventReloadSecretTest { + + private static final boolean FAIL_FAST = false; + + private static WireMockServer wireMockServer; + + private static final String SECRET_NAME = "mine"; + + private static final String NAMESPACE = "spring-k8s"; + + private static final boolean[] strategyCalled = new boolean[] { false }; + + private static CoreV1Api coreV1Api; + + private static final MockedStatic MOCK_STATIC = Mockito + .mockStatic(KubernetesClientUtils.class); + + @Autowired + private VisibleKubernetesClientEventBasedSecretsChangeDetector kubernetesClientEventBasedSecretsChangeDetector; + + @BeforeAll + static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + MOCK_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient).thenReturn(client); + MOCK_STATIC + .when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(), + Mockito.any())) + .thenReturn(NAMESPACE); + Configuration.setDefaultApiClient(client); + coreV1Api = new CoreV1Api(); + + String path = "/api/v1/namespaces/spring-k8s/secrets"; + V1Secret secretOne = secret(SECRET_NAME, Map.of()); + V1SecretList listOne = new V1SecretList().addItemsItem(secretOne); + + // needed so that our environment is populated with 'something' + // this call is done in the method that returns the AbstractEnvironment + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("mine-test") + .willSetStateTo("go-to-fail")); + + // first call will fail + stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) + .inScenario("mine-test") + .whenScenarioStateIs("go-to-fail") + .willSetStateTo("go-to-ok")); + + // second call passes (change data so that reload is triggered) + secretOne = secret(SECRET_NAME, Map.of("a", "b")); + listOne = new V1SecretList().addItemsItem(secretOne); + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) + .inScenario("mine-test") + .whenScenarioStateIs("go-to-ok") + .willSetStateTo("done")); + } + + @AfterAll + static void after() { + MOCK_STATIC.close(); + wireMockServer.stop(); + } + + /** + *
+	 * 	- 'secret.mine.spring-k8s' already exists in the environment
+	 * 	-  we simulate that another secret is created, so a request goes to k8s to find any potential
+	 * 	   differences. This request is mocked to fail.
+	 * 	   - then our secret is changed and the request passes
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + V1Secret secretNotMine = secret("not" + SECRET_NAME, Map.of()); + kubernetesClientEventBasedSecretsChangeDetector.onEvent(secretNotMine); + + // we fail while reading 'configMapOne' + Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { + boolean one = output.getOut().contains("failure in reading named sources"); + boolean two = output.getOut() + .contains("there was an error while reading config maps/secrets, no reload will happen"); + boolean three = output.getOut() + .contains("reloadable condition was not satisfied, reload will not be triggered"); + boolean updateStrategyNotCalled = !strategyCalled[0]; + return one && two && three && updateStrategyNotCalled; + }); + + // trigger the call again + V1Secret secretMine = secret(SECRET_NAME, Map.of()); + kubernetesClientEventBasedSecretsChangeDetector.onEvent(secretMine); + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> strategyCalled[0]); + } + + private static V1Secret secret(String name, Map data) { + Map encoded = data.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Base64.getEncoder().encode(e.getValue().getBytes()))); + + return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build(); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + VisibleKubernetesClientEventBasedSecretsChangeDetector kubernetesClientEventBasedSecretsChangeDetector( + AbstractEnvironment environment, ConfigReloadProperties configReloadProperties, + ConfigurationUpdateStrategy configurationUpdateStrategy, + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator, + KubernetesNamespaceProvider namespaceProvider) { + return new VisibleKubernetesClientEventBasedSecretsChangeDetector(coreV1Api, environment, + configReloadProperties, configurationUpdateStrategy, kubernetesClientSecretsPropertySourceLocator, + namespaceProvider); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a + // KubernetesClientConfigMapPropertySource, + // otherwise we can't properly test reload functionality + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT); + KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); + + PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, + namespaceProvider, secretsConfigProperties) + .locate(mockEnvironment); + + mockEnvironment.getPropertySources().addFirst(propertySource); + return mockEnvironment; + } + + @Bean + @Primary + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + SecretsConfigProperties secretsConfigProperties() { + return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); + } + + @Bean + @Primary + KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) { + return new KubernetesNamespaceProvider(environment); + } + + @Bean + @Primary + ConfigurationUpdateStrategy configurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> { + strategyCalled[0] = true; + }); + } + + @Bean + @Primary + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator( + SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientSecretsPropertySourceLocator(coreV1Api, namespaceProvider, + secretsConfigProperties); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java index d7e74a12ad..0e55494d6e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -31,12 +31,12 @@ import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; import io.kubernetes.client.openapi.models.V1ConfigMapList; import io.kubernetes.client.util.ClientBuilder; - import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.system.CapturedOutput; @@ -65,9 +65,9 @@ * @author wind57 */ @SpringBootTest( - properties = {"spring.main.allow-bean-definition-overriding=true", - "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, - classes = { PollingReloadConfigMapTest.TestConfig.class }) + properties = { "spring.main.allow-bean-definition-overriding=true", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadConfigMapTest.TestConfig.class }) @ExtendWith(OutputCaptureExtension.class) class PollingReloadConfigMapTest { @@ -102,17 +102,21 @@ static void setup() { // needed so that our environment is populated with 'something' // this call is done in the method that returns the AbstractEnvironment stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) - .inScenario("my-test").willSetStateTo("go-to-fail")); + .inScenario("my-test") + .willSetStateTo("go-to-fail")); // first reload call fails stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) - .inScenario("my-test").whenScenarioStateIs("go-to-fail").willSetStateTo("go-to-ok")); + .inScenario("my-test") + .whenScenarioStateIs("go-to-fail") + .willSetStateTo("go-to-ok")); // second reload call passes V1ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b")); V1ConfigMapList listTwo = new V1ConfigMapList().addItemsItem(configMapTwo); stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo))) - .inScenario("my-test").whenScenarioStateIs("go-to-ok")); + .inScenario("my-test") + .whenScenarioStateIs("go-to-ok")); } @@ -149,8 +153,7 @@ void test(CapturedOutput output) { } private static V1ConfigMap configMap(String name, Map data) { - return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata() - .withData(data).build(); + return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build(); } @TestConfiguration @@ -164,7 +167,8 @@ PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironmen ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.initialize(); return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, - KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator, scheduler); + KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator, + scheduler); } @Bean @@ -173,14 +177,15 @@ AbstractEnvironment environment() { MockEnvironment mockEnvironment = new MockEnvironment(); mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); - // simulate that environment already has a Fabric8ConfigMapPropertySource, + // simulate that environment already has a + // KubernetesClientConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, - configMapConfigProperties, namespaceProvider) + configMapConfigProperties, namespaceProvider) .locate(mockEnvironment); mockEnvironment.getPropertySources().addFirst(propertySource); @@ -191,15 +196,15 @@ AbstractEnvironment environment() { @Primary ConfigReloadProperties configReloadProperties() { return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, - ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), - false, Duration.ofSeconds(2)); + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); } @Bean @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT); } @Bean @@ -219,9 +224,9 @@ ConfigurationUpdateStrategy configurationUpdateStrategy() { @Bean @Primary KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator( - ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, - namespaceProvider); + namespaceProvider); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java index 7c4d266c73..4173ac8da2 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collector; import java.util.stream.Collectors; import com.github.tomakehurst.wiremock.WireMockServer; @@ -30,34 +29,27 @@ import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.JSON; 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.V1ConfigMapList; import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1SecretBuilder; import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.util.ClientBuilder; - import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; -import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector; import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; @@ -75,9 +67,9 @@ * @author wind57 */ @SpringBootTest( - properties = {"spring.main.allow-bean-definition-overriding=true", - "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, - classes = { PollingReloadConfigMapTest.TestConfig.class }) + properties = { "spring.main.allow-bean-definition-overriding=true", + "logging.level.org.springframework.cloud.kubernetes.commons.config=debug" }, + classes = { PollingReloadSecretTest.TestConfig.class }) @ExtendWith(OutputCaptureExtension.class) class PollingReloadSecretTest { @@ -112,16 +104,20 @@ static void setup() { // needed so that our environment is populated with 'something' // this call is done in the method that returns the AbstractEnvironment stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne))) - .inScenario("my-test").willSetStateTo("go-to-fail")); + .inScenario("my-test") + .willSetStateTo("go-to-fail")); // first reload call fails stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")) - .inScenario("my-test").whenScenarioStateIs("go-to-fail").willSetStateTo("go-to-ok")); + .inScenario("my-test") + .whenScenarioStateIs("go-to-fail") + .willSetStateTo("go-to-ok")); V1Secret secretTwo = secret(SECRET_NAME, Map.of("a", "b")); V1SecretList listTwo = new V1SecretList().addItemsItem(secretTwo); stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo))) - .inScenario("my-test").whenScenarioStateIs("go-to-ok")); + .inScenario("my-test") + .whenScenarioStateIs("go-to-ok")); } @@ -139,7 +135,7 @@ static void after() { */ @Test void test(CapturedOutput output) { - // we fail while reading 'configMapOne' + // we fail while reading 'secretOne' Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> { boolean one = output.getOut().contains("failure in reading named sources"); boolean two = output.getOut() @@ -150,7 +146,7 @@ void test(CapturedOutput output) { return one && two && three && updateStrategyNotCalled; }); - // it passes while reading 'configMapTwo' + // it passes while reading 'secretTwo' Awaitility.await() .atMost(Duration.ofSeconds(10)) .pollInterval(Duration.ofSeconds(1)) @@ -159,12 +155,11 @@ void test(CapturedOutput output) { private static V1Secret secret(String name, Map data) { - Map encoded = data.entrySet().stream().collect( - Collectors.toMap(e -> e.getKey(), e -> Base64.getEncoder().encode(e.getValue().getBytes())) - ); + Map encoded = data.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Base64.getEncoder().encode(e.getValue().getBytes()))); - return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata() - .withData(encoded).build(); + return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build(); } @TestConfiguration @@ -178,7 +173,8 @@ PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment en ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.initialize(); return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, - KubernetesClientSecretsPropertySource.class, kubernetesClientSecretsPropertySourceLocator, scheduler); + KubernetesClientSecretsPropertySource.class, kubernetesClientSecretsPropertySourceLocator, + scheduler); } @Bean @@ -187,14 +183,15 @@ AbstractEnvironment environment() { MockEnvironment mockEnvironment = new MockEnvironment(); mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); - // simulate that environment already has a Fabric8ConfigMapPropertySource, + // simulate that environment already has a + // KubernetesClientSecretPropertySource, // otherwise we can't properly test reload functionality - ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), + List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); - PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, - configMapConfigProperties, namespaceProvider) + PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, + namespaceProvider, secretsConfigProperties) .locate(mockEnvironment); mockEnvironment.getPropertySources().addFirst(propertySource); @@ -204,16 +201,16 @@ AbstractEnvironment environment() { @Bean @Primary ConfigReloadProperties configReloadProperties() { - return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH, - ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), - false, Duration.ofSeconds(2)); + return new ConfigReloadProperties(true, false, true, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"), + false, Duration.ofSeconds(2)); } @Bean @Primary - SecretsConfigProperties configMapConfigProperties() { - return new SecretsConfigProperties(true, List.of(), List.of(), Map.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + SecretsConfigProperties secretsConfigProperties() { + return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, + false, true, FAIL_FAST, RetryProperties.DEFAULT); } @Bean @@ -233,9 +230,9 @@ ConfigurationUpdateStrategy configurationUpdateStrategy() { @Bean @Primary KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator( - ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) { - return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties, - namespaceProvider); + SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientSecretsPropertySourceLocator(coreV1Api, namespaceProvider, + secretsConfigProperties); } } From cd715b91ea2f0efa45e05c16a5cb15ad3cfcc97f Mon Sep 17 00:00:00 2001 From: wind57 Date: Thu, 21 Nov 2024 23:23:07 +0200 Subject: [PATCH 11/13] review comments --- .../cloud/kubernetes/commons/config/Constants.java | 6 ++++++ .../cloud/kubernetes/commons/config/LabeledSourceData.java | 6 +++--- .../cloud/kubernetes/commons/config/NamedSourceData.java | 7 ++++--- .../cloud/kubernetes/commons/config/SourceData.java | 6 ------ .../kubernetes/commons/config/reload/ConfigReloadUtil.java | 5 +++-- .../commons/config/reload/ConfigReloadUtilTests.java | 7 +++---- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/Constants.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/Constants.java index e8be954fb8..072770f1dd 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/Constants.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/Constants.java @@ -63,6 +63,12 @@ public final class Constants { */ public static final String RELOAD_MODE = "spring.cloud.kubernetes.reload.mode"; + /** + * property set to true when there was an error reading config maps or secrets, when + * generating a property source. + */ + public static final String ERROR_PROPERTY = "spring.cloud.k8s.error.reading.property.source"; + private Constants() { } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java index 7742c9214f..61178346d9 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java @@ -25,8 +25,8 @@ import org.apache.commons.logging.LogFactory; import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException; +import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY; import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR; -import static org.springframework.cloud.kubernetes.commons.config.SourceData.EMPTY_SOURCE_NAME_ON_ERROR; /** * @author wind57 @@ -41,7 +41,7 @@ public abstract class LabeledSourceData { public final SourceData compute(Map labels, ConfigUtils.Prefix prefix, String target, boolean profileSources, boolean failFast, String namespace, String[] activeProfiles) { - MultipleSourcesContainer data; + MultipleSourcesContainer data = MultipleSourcesContainer.empty(); try { Set profiles = Set.of(); @@ -81,7 +81,7 @@ public final SourceData compute(Map labels, ConfigUtils.Prefix p catch (Exception e) { LOG.warn("failure in reading labeled sources"); onException(failFast, e); - return SourceData.emptyRecord(EMPTY_SOURCE_NAME_ON_ERROR); + data = new MultipleSourcesContainer(data.names(), Map.of(ERROR_PROPERTY, "true")); } String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR)); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java index f3e9ffa254..acd39b64ba 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java @@ -17,14 +17,15 @@ package org.springframework.cloud.kubernetes.commons.config; import java.util.LinkedHashSet; +import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException; +import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY; import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR; -import static org.springframework.cloud.kubernetes.commons.config.SourceData.EMPTY_SOURCE_NAME_ON_ERROR; /** * @author wind57 @@ -43,7 +44,7 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St // first comes non-profile based source sourceNames.add(sourceName); - MultipleSourcesContainer data; + MultipleSourcesContainer data = MultipleSourcesContainer.empty(); try { if (profileSources) { @@ -72,7 +73,7 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St catch (Exception e) { LOG.warn("failure in reading named sources"); onException(failFast, e); - return SourceData.emptyRecord(EMPTY_SOURCE_NAME_ON_ERROR); + data = new MultipleSourcesContainer(data.names(), Map.of(ERROR_PROPERTY, "true")); } String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR)); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java index 11b6788698..b9eb46c0b2 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java @@ -26,12 +26,6 @@ */ public record SourceData(String sourceName, Map sourceData) { - /** - * source name that is generated when there is an error reading the underlying - * configmap(s) or secret(s). - */ - public static final String EMPTY_SOURCE_NAME_ON_ERROR = "source-generate-on-error"; - public static SourceData emptyRecord(String sourceName) { return new SourceData(sourceName, Map.of()); } 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 d1914971cb..02de15f2c1 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,13 +27,14 @@ 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.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.log.LogAccessor; +import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY; + /** * @author wind57 */ @@ -164,7 +165,7 @@ else if (propertySource instanceof CompositePropertySource source) { static boolean changed(List k8sSources, List appSources) { - if (k8sSources.stream().anyMatch(source -> source.getName().equals(SourceData.EMPTY_SOURCE_NAME_ON_ERROR))) { + if (k8sSources.stream().anyMatch(source -> "true".equals(source.getProperty(ERROR_PROPERTY)))) { LOG.info(() -> "there was an error while reading config maps/secrets, no reload will happen"); return false; } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java index ccda13c6ff..df1f54b2b2 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java @@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test; import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; +import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; -import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MapPropertySource; @@ -154,9 +154,8 @@ public Object getProperty(String name) { @Test void testEmptySourceNameOnError() { Object value = new Object(); - Map rightMap = new HashMap<>(); - rightMap.put("key", value); - MapPropertySource left = new MapPropertySource(SourceData.EMPTY_SOURCE_NAME_ON_ERROR, Map.of()); + Map rightMap = Map.of("key", value); + MapPropertySource left = new MapPropertySource("on-error", Map.of(Constants.ERROR_PROPERTY, "true")); MapPropertySource right = new MapPropertySource("right", rightMap); boolean changed = ConfigReloadUtil.changed(List.of(left), List.of(right)); assertThat(changed).isFalse(); From 1560bb5d5b707b2a765206c46e039571360429dd Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 22 Nov 2024 10:38:40 +0200 Subject: [PATCH 12/13] fix fabric8 tests --- ...ic8ConfigMapErrorOnReadingSourceTests.java | 22 +++++++++++-------- ...abric8SecretErrorOnReadingSourceTests.java | 21 +++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java index 5f90eaeb93..549cb4dd5f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java @@ -33,8 +33,8 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; -import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; @@ -85,7 +85,7 @@ void namedSingleConfigMapFails() { .findAny() .orElseThrow(); - assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(mapPropertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -123,8 +123,9 @@ void namedTwoConfigMapsOneFails() { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - // two sources are present, one being empty - assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + // two sources are present + assertThat(names).containsExactly("configmap.two.default", "configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -157,7 +158,8 @@ void namedTwoConfigMapsBothFail() { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -188,7 +190,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(sourceNames).containsExactly("configmap..spring-k8s"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); } @@ -234,7 +237,8 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // two sources are present, one being empty - assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap.two.default", "configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); @@ -272,8 +276,8 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - // all 3 sources ('application' named source, and two labeled sources) - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java index cd5cf86d4d..4c203861f3 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java @@ -32,9 +32,9 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; -import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; @@ -85,7 +85,8 @@ void namedSingleSecretFails(CapturedOutput output) { .findAny() .orElseThrow(); - assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(mapPropertySource.getName()).isEqualTo("secret..spring-k8s"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading named sources"); } @@ -121,7 +122,8 @@ void namedTwoSecretsOneFails() { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // two sources are present, one being empty - assertThat(names).containsExactly("secret.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("secret.two.default", "secret..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -153,7 +155,8 @@ void namedTwoSecretsBothFail() { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("secret..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -184,7 +187,8 @@ void labeledSingleSecretFails(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(sourceNames).containsExactly("secret..spring-k8s"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); } @@ -230,7 +234,8 @@ void labeledTwoSecretsOneFails(CapturedOutput output) { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // two sources are present, one being empty - assertThat(names).containsExactly("secret.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("secret.two.default", "secret..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); @@ -268,8 +273,8 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - // all 3 sources ('application' named source, and two labeled sources) - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("secret..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); From d05ce2dd663c4871638498aed10720ebbc44ae21 Mon Sep 17 00:00:00 2001 From: wind57 Date: Fri, 22 Nov 2024 10:42:41 +0200 Subject: [PATCH 13/13] fix tests --- ...entConfigMapErrorOnReadingSourceTests.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java index 91041c8352..2c79b63c80 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java @@ -39,8 +39,8 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; -import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; @@ -125,7 +125,8 @@ void namedSingleConfigMapFails() { .findAny() .orElseThrow(); - assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(mapPropertySource.getName()).isEqualTo("configmap..spring-k8s"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); } @@ -168,7 +169,8 @@ void namedTwoConfigMapsOneFails(CapturedOutput output) { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // two sources are present, one being empty - assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap.two.default", "configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output.getOut()) .doesNotContain("sourceName : two was requested, but not found in namespace : default"); @@ -212,7 +214,8 @@ void namedTwoConfigMapsBothFail(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output.getOut()) .doesNotContain("sourceName : one was requested, but not found in namespace : default"); assertThat(output.getOut()) @@ -255,7 +258,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment()); List sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); - assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(sourceNames).containsExactly("configmap..spring-k8s"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); } @@ -268,8 +272,6 @@ void labeledSingleConfigMapFails(CapturedOutput output) { */ @Test void labeledTwoConfigMapsOneFails(CapturedOutput output) { - String configMapNameOne = "one"; - String configMapNameTwo = "two"; Map configMapOneLabels = Map.of("one", "1"); Map configMapTwoLabels = Map.of("two", "2"); @@ -310,7 +312,8 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // two sources are present, one being empty - assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap.two.default", "configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources"); @@ -364,7 +367,8 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { List names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList(); // all 3 sources ('application' named source, and two labeled sources) - assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR); + assertThat(names).containsExactly("configmap..default"); + assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true"); assertThat(output).contains("failure in reading labeled sources"); assertThat(output).contains("failure in reading named sources");