diff --git a/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc b/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc index d656135def..6053e6a4fa 100644 --- a/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc +++ b/docs/modules/ROOT/pages/property-source-config/propertysource-reload.adoc @@ -2,8 +2,8 @@ = `PropertySource` Reload WARNING: This functionality has been deprecated in the 2020.0 release. Please see -the xref:spring-cloud-kubernetes-configuration-watcher.adoc#spring-cloud-kubernetes-configuration-watcher[null] controller for an alternative way -to achieve the same functionality. +the xref:spring-cloud-kubernetes-configuration-watcher.adoc#spring-cloud-kubernetes-configuration-watcher[Spring Cloud Kubernetes Configuration Watcher] +controller for an alternative way to achieve the same functionality. Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or diff --git a/docs/package.json b/docs/package.json index 37c3765900..1eebd05f53 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,10 +1,10 @@ { "dependencies": { - "antora": "3.2.0-alpha.4", + "antora": "3.2.0-alpha.6", "@antora/atlas-extension": "1.0.0-alpha.2", - "@antora/collector-extension": "1.0.0-beta.3", + "@antora/collector-extension": "1.0.0-rc.1", "@asciidoctor/tabs": "1.0.0-beta.6", - "@springio/antora-extensions": "1.11.1", + "@springio/antora-extensions": "1.14.2", "@springio/asciidoctor-extensions": "1.0.0-alpha.14" } } diff --git a/docs/pom.xml b/docs/pom.xml index 5d35ce25c1..ddf676f71b 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT .. jar diff --git a/pom.xml b/pom.xml index 4e8652882b..ce08764849 100644 --- a/pom.xml +++ b/pom.xml @@ -25,12 +25,12 @@ org.springframework.cloud spring-cloud-build - 4.1.4-SNAPSHOT + 4.1.5-SNAPSHOT spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT pom Spring Cloud Kubernetes @@ -69,10 +69,10 @@ surefire-reports failsafe-reports - 4.1.5-SNAPSHOT - 4.1.4-SNAPSHOT - 4.1.3-SNAPSHOT - 4.1.5-SNAPSHOT + 4.1.6-SNAPSHOT + 4.1.5-SNAPSHOT + 4.1.4-SNAPSHOT + 4.1.6-SNAPSHOT true true diff --git a/spring-cloud-kubernetes-client-autoconfig/pom.xml b/spring-cloud-kubernetes-client-autoconfig/pom.xml index ec678eaf2e..6d133fcaa7 100644 --- a/spring-cloud-kubernetes-client-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-client-autoconfig/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-config/pom.xml b/spring-cloud-kubernetes-client-config/pom.xml index 3fd3f818d6..5d6dd917c0 100644 --- a/spring-cloud-kubernetes-client-config/pom.xml +++ b/spring-cloud-kubernetes-client-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 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/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java index 21bc92b9ee..f4be4ef88e 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java @@ -147,7 +147,7 @@ private static List strippedConfigMaps(CoreV1Api coreV1 private static List strippedSecrets(CoreV1Api coreV1Api, String namespace) { List strippedSecrets = KubernetesClientSecretsCache.byNamespace(coreV1Api, namespace); if (strippedSecrets.isEmpty()) { - LOG.debug("No configmaps in namespace '" + namespace + "'"); + LOG.debug("No secrets in namespace '" + namespace + "'"); } return strippedSecrets; } 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..13174e7cc3 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java @@ -0,0 +1,367 @@ +/* + * 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.core.env.CompositePropertySource; +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.
+	 * 
+ */ + @Test + void namedSingleConfigMapFails(CapturedOutput output) { + 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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[my-config]'"); + + } + + /** + *
+	 *     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(); + + // one property source is present + assertThat(names).containsExactly("configmap.two.default"); + assertThat(output.getOut()) + .doesNotContain("sourceName : two was requested, but not found in namespace : default"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[one]'"); + + } + + /** + *
+	 *     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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + 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"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[one]'"); + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail.
+	 * 
+ */ + @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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{a=b}'"); + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void labeledTwoConfigMapsOneFails(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 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(); + + // one source is present + assertThat(names).containsExactly("configmap.two.default"); + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{one=1}'"); + + } + + /** + *
+	 *     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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output).contains("Failure in reading labeled sources"); + assertThat(output).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{one=1}'"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{two=2}'"); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index 1266b00f70..04bbefb870 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.client.config; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,12 +35,16 @@ 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.Constants; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; @@ -55,6 +60,7 @@ * @author Ryan Baxter * @author Isik Erhan */ +@ExtendWith(OutputCaptureExtension.class) class KubernetesClientConfigMapPropertySourceLocatorTests { private static final V1ConfigMapList PROPERTIES_CONFIGMAP_LIST = new V1ConfigMapList() @@ -185,7 +191,7 @@ public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { } @Test - public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { + public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput output) { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); @@ -196,7 +202,17 @@ public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); - assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment())); + List> result = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> { + PropertySource source = locator.locate(new MockEnvironment()); + result.add(source); + }); + + assertThat(result.get(0)).isInstanceOf(CompositePropertySource.class); + CompositePropertySource composite = (CompositePropertySource) result.get(0); + assertThat(composite.getPropertySources()).hasSize(0); + assertThat(output.getOut()).contains("Failed to load source:"); + } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java index f0bfc7fafb..a78b344d2b 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java @@ -174,7 +174,8 @@ void kubernetesReloadEnabledButSecretAndConfigDisabled() { */ @Test void reloadEventEnabledMonitoringConfigMapsEnabledByDefault() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.cloud-platform=KUBERNETES"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); Assertions.assertEquals(map.size(), 1); Assertions.assertTrue(map.values() @@ -197,8 +198,8 @@ void reloadEventEnabledMonitoringConfigMapsEnabledByDefault() { */ @Test void reloadEventEnabledMonitoringConfigMapsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.cloud.kubernetes.reload.mode=event", - "spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.cloud.kubernetes.reload.mode=event", "spring.main.cloud-platform=KUBERNETES"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); Assertions.assertEquals(map.size(), 1); Assertions.assertTrue(map.values() @@ -259,8 +260,8 @@ void reloadPollingEnabledMonitoringConfigMapsDisabledMonitoringSecretsDisabled() */ @Test void reloadPollingEnabledMonitoringConfigMapsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.cloud.kubernetes.reload.mode=polling", - "spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.cloud.kubernetes.reload.mode=polling", "spring.main.cloud-platform=KUBERNETES"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); Assertions.assertEquals(map.size(), 1); Assertions.assertTrue( @@ -281,8 +282,8 @@ void reloadPollingEnabledMonitoringConfigMapsEnabled() { */ @Test void reloadEventEnabledMonitoringConfigMapsDisabledMonitoringSecretsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.main.cloud-platform=KUBERNETES", - "spring.cloud.kubernetes.reload.monitoring-secrets=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-secrets=true", "spring.cloud.kubernetes.reload.monitoring-configMaps=false", "spring.cloud.kubernetes.reload.mode=event"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); @@ -308,8 +309,8 @@ void reloadEventEnabledMonitoringConfigMapsDisabledMonitoringSecretsEnabled() { */ @Test void reloadPollingEnabledMonitoringConfigMapsDisabledMonitoringSecretsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.main.cloud-platform=KUBERNETES", - "spring.cloud.kubernetes.reload.monitoring-secrets=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-secrets=true", "spring.cloud.kubernetes.reload.monitoring-configMaps=false", "spring.cloud.kubernetes.reload.mode=polling"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); @@ -332,8 +333,8 @@ void reloadPollingEnabledMonitoringConfigMapsDisabledMonitoringSecretsEnabled() */ @Test void reloadEventEnabledMonitoringConfigMapsEnabledMonitoringSecretsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.main.cloud-platform=KUBERNETES", - "spring.cloud.kubernetes.reload.monitoring-secrets=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-secrets=true", "spring.cloud.kubernetes.reload.monitoring-configMaps=true", "spring.cloud.kubernetes.reload.mode=event"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); @@ -360,8 +361,8 @@ void reloadEventEnabledMonitoringConfigMapsEnabledMonitoringSecretsEnabled() { */ @Test void reloadPollingEnabledMonitoringConfigMapsEnabledMonitoringSecretsEnabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.main.cloud-platform=KUBERNETES", - "spring.cloud.kubernetes.reload.monitoring-secrets=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-secrets=true", "spring.cloud.kubernetes.reload.monitoring-configMaps=true", "spring.cloud.kubernetes.reload.mode=polling"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); @@ -430,8 +431,9 @@ void reloadPollingEnabledMonitorConfigMapsDisabledMonitoringSecretsDisabled() { */ @Test void reloadEventEnabledMonitoringConfigMapsEnabledMonitoringSecretsDisabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.cloud.kubernetes.reload.mode=event", - "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-configMaps=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.cloud.kubernetes.reload.mode=event", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.reload.monitoring-configMaps=true", "spring.cloud.kubernetes.reload.monitoring-secrets=false"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); Assertions.assertEquals(map.size(), 1); @@ -456,8 +458,9 @@ void reloadEventEnabledMonitoringConfigMapsEnabledMonitoringSecretsDisabled() { */ @Test void reloadPollingEnabledMonitoringConfigMapsEnabledMonitoringSecretsDisabled() { - setup("spring.cloud.kubernetes.reload.enabled=true", "spring.cloud.kubernetes.reload.mode=polling", - "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.reload.monitoring-configMaps=true", + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.reload.enabled=true", + "spring.cloud.kubernetes.reload.mode=polling", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.reload.monitoring-configMaps=true", "spring.cloud.kubernetes.reload.monitoring-secrets=false"); Map map = context.getBeansOfType(ConfigurationChangeDetector.class); Assertions.assertEquals(map.size(), 1); @@ -472,21 +475,23 @@ void reloadPollingEnabledMonitoringConfigMapsEnabledMonitoringSecretsDisabled() @Test void kubernetesConfigAndSecretEnabledByDefault() { - setup("spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.main.cloud-platform=KUBERNETES"); assertThat(context.containsBean("configMapPropertySourceLocator")).isTrue(); assertThat(context.containsBean("secretsPropertySourceLocator")).isTrue(); } @Test void kubernetesConfigEnabledButSecretDisabled() { - setup("spring.cloud.kubernetes.secrets.enabled=false", "spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.secrets.enabled=false", + "spring.main.cloud-platform=KUBERNETES"); assertThat(context.containsBean("configMapPropertySourceLocator")).isTrue(); assertThat(context.containsBean("secretsPropertySourceLocator")).isFalse(); } @Test void kubernetesSecretsEnabledButConfigDisabled() { - setup("spring.cloud.kubernetes.config.enabled=false", "spring.main.cloud-platform=KUBERNETES"); + setup("spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.config.enabled=false", + "spring.main.cloud-platform=KUBERNETES"); assertThat(context.containsBean("configMapPropertySourceLocator")).isFalse(); assertThat(context.containsBean("secretsPropertySourceLocator")).isTrue(); } 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..030c62b968 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 @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.client.config; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -30,11 +31,15 @@ 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.NamespaceResolutionFailedException; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; @@ -50,32 +55,59 @@ * @author Ryan Baxter * @author Isik Erhan */ +@ExtendWith(OutputCaptureExtension.class) 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; @@ -174,7 +206,7 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { } @Test - void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { + void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput output) { CoreV1Api api = new CoreV1Api(); stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); @@ -184,7 +216,16 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties); - assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment())); + List> result = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> { + PropertySource source = locator.locate(new MockEnvironment()); + result.add(source); + }); + + assertThat(result.get(0)).isInstanceOf(CompositePropertySource.class); + CompositePropertySource composite = (CompositePropertySource) result.get(0); + assertThat(composite.getPropertySources()).hasSize(0); + assertThat(output.getOut()).contains("Failed to load source:"); } } 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..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 @@ -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/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..cfdbfb9f2d --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java @@ -0,0 +1,254 @@ +/* + * 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("Failed to load source"); + 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..86e3b09ed0 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java @@ -0,0 +1,259 @@ +/* + * 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("Failed to load source"); + 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 new file mode 100644 index 0000000000..2fd0b3f659 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -0,0 +1,233 @@ +/* + * 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; + +/** + * @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 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")); + + // 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))) + .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("Failed to load source"); + 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 + // 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); + 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/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..a9b3b9b40c --- /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.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.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.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; +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.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 = { PollingReloadSecretTest.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 '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("Failed to load source"); + 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 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 + PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy, + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy, + KubernetesClientSecretsPropertySource.class, kubernetesClientSecretsPropertySourceLocator, + scheduler); + } + + @Bean + @Primary + AbstractEnvironment environment() { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE); + + // simulate that environment already has a + // KubernetesClientSecretPropertySource, + // 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, false, 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, false, 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 + KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator( + SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) { + return new KubernetesClientSecretsPropertySourceLocator(coreV1Api, namespaceProvider, + secretsConfigProperties); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml index 1e92ea594d..1cde90f756 100644 --- a/spring-cloud-kubernetes-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-client-discovery/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-client-loadbalancer/pom.xml index a253bc6d82..97dd186bde 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/pom.xml +++ b/spring-cloud-kubernetes-client-loadbalancer/pom.xml @@ -6,7 +6,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index eaead86a84..4915053c5b 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java index 7c2688472b..4fe25ff676 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java @@ -82,7 +82,16 @@ public PropertySource locate(Environment environment) { if (this.properties.enableApi()) { Set sources = new LinkedHashSet<>(this.properties.determineSources(environment)); LOG.debug("Config Map normalized sources : " + sources); - sources.forEach(s -> composite.addFirstPropertySource(getMapPropertySource(s, env))); + sources.forEach(s -> { + MapPropertySource propertySource = getMapPropertySource(s, env); + if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { + LOG.warn("Failed to load source: " + s); + } + else { + LOG.debug("Adding config map property source " + propertySource.getName()); + composite.addFirstPropertySource(propertySource); + } + }); } addPropertySourcesFromPaths(environment, composite); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java index 5f40858eab..886e9e7b2f 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java @@ -36,6 +36,7 @@ import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.context.ApplicationListener; import org.springframework.core.env.Environment; import org.springframework.core.style.ToStringCreator; import org.springframework.util.CollectionUtils; @@ -60,6 +61,9 @@ public final class ConfigUtils { || sourceName.endsWith("-" + activeProfile + ".yaml") || sourceName.endsWith("-" + activeProfile + ".properties"); + private static final ApplicationListener NO_OP = (e) -> { + }; + private ConfigUtils() { } @@ -206,7 +210,7 @@ public static MultipleSourcesContainer processNamedData(List { StrippedSourceContainer stripped = hashByName.get(sourceName); if (stripped != null) { - LOG.debug("Found source with name : '" + sourceName + " in namespace: '" + namespace + "'"); + LOG.debug("Found source with name : '" + sourceName + "' in namespace: '" + namespace + "'"); foundSourceNames.add(sourceName); // see if data is a single yaml/properties file and if it needs decoding Map rawData = stripped.data(); @@ -225,6 +229,9 @@ public static MultipleSourcesContainer processNamedData(List decodeData(Map data) { } public static void registerSingle(ConfigurableBootstrapContext bootstrapContext, Class cls, T instance, - String name) { + String name, ApplicationListener listener) { bootstrapContext.registerIfAbsent(cls, BootstrapRegistry.InstanceSupplier.of(instance)); bootstrapContext.addCloseListener(event -> { if (event.getApplicationContext().getBeanFactory().getSingleton(name) == null) { event.getApplicationContext() .getBeanFactory() .registerSingleton(name, event.getBootstrapContext().get(cls)); + event.getApplicationContext().addApplicationListener(listener); } }); } + public static void registerSingle(ConfigurableBootstrapContext bootstrapContext, Class cls, T instance, + String name) { + registerSingle(bootstrapContext, cls, instance, name, NO_OP); + } + /** * append prefix to the keys and return a new Map with the new values. */ 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 df4e6aab1f..428949d427 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,7 +21,11 @@ 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.ERROR_PROPERTY; import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR; /** @@ -32,6 +36,8 @@ */ 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) { @@ -45,8 +51,7 @@ public final SourceData compute(Map labels, ConfigUtils.Prefix p data = dataSupplier(labels, profiles); // need this check because when there is no data, the name of the property - // source - // is using provided labels, + // source is using provided labels, // unlike when the data is present: when we use secret names if (data.names().isEmpty()) { String names = labels.keySet() @@ -74,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); + 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 f98464650c..34da831a86 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 @@ -20,7 +20,11 @@ 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; /** @@ -31,6 +35,8 @@ */ public abstract class NamedSourceData { + private static final Log LOG = LogFactory.getLog(NamedSourceData.class); + public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, String target, boolean profileSources, boolean failFast, String namespace, String[] activeProfiles) { @@ -51,7 +57,9 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St data = dataSupplier(sourceNames); if (data.names().isEmpty()) { - return new SourceData(ConfigUtils.sourceName(target, sourceName, namespace), Map.of()); + String emptySourceName = ConfigUtils.sourceName(target, sourceName, namespace); + LOG.debug("Will return empty source with name : " + emptySourceName); + return SourceData.emptyRecord(emptySourceName); } if (prefix != ConfigUtils.Prefix.DEFAULT) { @@ -63,7 +71,9 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St } catch (Exception e) { + LOG.warn("Failure in reading named sources"); onException(failFast, e); + 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/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index ca954a6ef0..07365b3fe5 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -42,6 +42,7 @@ import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; /** @@ -87,8 +88,17 @@ public PropertySource locate(Environment environment) { putPathConfig(composite); if (this.properties.enableApi()) { - uniqueSources - .forEach(s -> composite.addPropertySource(getSecretsPropertySourceForSingleSecret(env, s))); + uniqueSources.forEach(s -> { + MapPropertySource propertySource = getSecretsPropertySourceForSingleSecret(env, s); + + if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { + LOG.warn("Failed to load source: " + s); + } + else { + LOG.debug("Adding secret property source " + propertySource.getName()); + composite.addFirstPropertySource(propertySource); + } + }); } cache.discardAll(); 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 29b4280e22..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 @@ -16,7 +16,6 @@ package org.springframework.cloud.kubernetes.commons.config; -import java.util.Collections; import java.util.Map; /** @@ -25,10 +24,10 @@ * * @author wind57 */ -public final record SourceData(String sourceName, Map sourceData) { +public record SourceData(String sourceName, Map sourceData) { public static SourceData emptyRecord(String sourceName) { - return new SourceData(sourceName, Collections.emptyMap()); + return new SourceData(sourceName, Map.of()); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java index 7e313b70d1..5f51e9700d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java @@ -48,7 +48,7 @@ public class SourceDataEntriesProcessor extends MapPropertySource { private static final Log LOG = LogFactory.getLog(SourceDataEntriesProcessor.class); - private static Predicate ENDS_IN_EXTENSION = x -> x.endsWith(".yml") || x.endsWith(".yaml") + private static final Predicate ENDS_IN_EXTENSION = x -> x.endsWith(".yml") || x.endsWith(".yaml") || x.endsWith(".properties"); public SourceDataEntriesProcessor(SourceData sourceData) { 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 9044815080..3986f6c825 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 @@ -44,20 +44,38 @@ private ConfigReloadUtil() { private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(ConfigReloadUtil.class)); - public static boolean reload(String target, String eventSourceType, PropertySourceLocator locator, + /** + * used for the event based reloading. + */ + public static boolean reload(String target, String sourceAsString, PropertySourceLocator locator, ConfigurableEnvironment environment, Class existingSourcesType) { - LOG.debug(() -> "onEvent " + target + ": " + eventSourceType); + LOG.debug(() -> "onEvent " + target + ": " + sourceAsString); + + return reload(locator, environment, existingSourcesType); + } + + /** + * used for the poll based reloading. + */ + public static boolean reload(PropertySourceLocator locator, ConfigurableEnvironment environment, + Class existingSourcesType) { - List sourceFromK8s = locateMapPropertySources(locator, environment); List existingSources = findPropertySources(existingSourcesType, environment); + if (existingSources.isEmpty()) { + LOG.debug(() -> "no existingSources found, reload will not happen"); + return false; + } + + List sourceFromK8s = locateMapPropertySources(locator, environment); + 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; @@ -67,7 +85,9 @@ public static boolean reload(String target, String eventSourceType, PropertySour * @param property source type * @param sourceClass class for which property sources will be found * @return finds all registered property sources of the given type + * @deprecated this method will not be public in the next major release. */ + @Deprecated(forRemoval = false) public static > List findPropertySources(Class sourceClass, ConfigurableEnvironment environment) { List managedSources = new ArrayList<>(); @@ -141,25 +161,26 @@ else if (propertySource instanceof CompositePropertySource source) { return result; } - static boolean changed(List left, List right) { - if (left.size() != right.size()) { + static boolean changed(List k8sSources, List appSources) { + + if (k8sSources.size() != appSources.size()) { if (LOG.isDebugEnabled()) { - LOG.debug("left size: " + left.size()); - left.forEach(item -> LOG.debug(item.toString())); + LOG.debug("k8s property sources size: " + k8sSources.size()); + k8sSources.forEach(item -> LOG.debug(item.toString())); - LOG.debug("right size: " + right.size()); - right.forEach(item -> LOG.debug(item.toString())); + LOG.debug("app property sources size size: " + appSources.size()); + appSources.forEach(item -> LOG.debug(item.toString())); } - LOG.warn(() -> "The current number of ConfigMap PropertySources does not match " + LOG.warn(() -> "The current number of PropertySources does not match " + "the ones loaded from Kubernetes - No reload will take place"); return false; } - for (int i = 0; i < left.size(); i++) { - MapPropertySource leftPropertySource = left.get(i); - MapPropertySource rightPropertySource = right.get(i); - if (changed(leftPropertySource, rightPropertySource)) { - LOG.debug(() -> "found change in : " + leftPropertySource); + for (int i = 0; i < k8sSources.size(); i++) { + MapPropertySource k8sSource = k8sSources.get(i); + MapPropertySource appSource = appSources.get(i); + if (changed(k8sSource, appSource)) { + LOG.debug(() -> "found change in : " + k8sSource); return true; } } @@ -169,20 +190,20 @@ static boolean changed(List left, List leftMap = left.getSource(); - Map rightMap = right.getSource(); - return !Objects.equals(leftMap, rightMap); + Map k8sMap = k8sSource.getSource(); + Map appMap = appSource.getSource(); + return !Objects.equals(k8sMap, appMap); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java index bcd568b178..e26aa85673 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingConfigMapChangeDetector.java @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.commons.config.reload; import java.time.Duration; -import java.util.List; import jakarta.annotation.PostConstruct; import org.apache.commons.logging.Log; @@ -29,10 +28,6 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.changed; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.findPropertySources; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.locateMapPropertySources; - /** * A change detector that periodically retrieves configmaps and fire a reload when * something changes. @@ -75,23 +70,13 @@ private void init() { } private void executeCycle() { - - boolean changedConfigMap = false; if (monitorConfigMaps) { - log.debug("Polling for changes in config maps"); - List currentConfigMapSources = findPropertySources(propertySourceClass, - environment); - - if (!currentConfigMapSources.isEmpty()) { - changedConfigMap = changed(locateMapPropertySources(this.propertySourceLocator, this.environment), - currentConfigMapSources); + boolean changedConfigMap = ConfigReloadUtil.reload(propertySourceLocator, environment, propertySourceClass); + if (changedConfigMap) { + log.info("Detected change in config maps"); + reloadProperties(); } } - - if (changedConfigMap) { - log.info("Detected change in config maps"); - reloadProperties(); - } } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java index b70975a5c7..da0422bb68 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/PollingSecretsChangeDetector.java @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.commons.config.reload; import java.time.Duration; -import java.util.List; import jakarta.annotation.PostConstruct; import org.apache.commons.logging.Log; @@ -29,10 +28,6 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.changed; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.findPropertySources; -import static org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadUtil.locateMapPropertySources; - /** * A change detector that periodically retrieves secrets and fires a reload when something * changes. @@ -75,23 +70,13 @@ private void init() { } private void executeCycle() { - - boolean changedSecrets = false; if (monitorSecrets) { - log.debug("Polling for changes in secrets"); - List currentSecretSources = locateMapPropertySources(this.propertySourceLocator, - this.environment); - if (!currentSecretSources.isEmpty()) { - List propertySources = findPropertySources(propertySourceClass, - environment); - changedSecrets = changed(currentSecretSources, propertySources); + boolean changedSecrets = ConfigReloadUtil.reload(propertySourceLocator, environment, propertySourceClass); + if (changedSecrets) { + log.info("Detected change in secrets"); + reloadProperties(); } } - - if (changedSecrets) { - log.info("Detected change in secrets"); - reloadProperties(); - } } } diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml index 3b7deb9ab8..3e782cc248 100644 --- a/spring-cloud-kubernetes-controllers/pom.xml +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 pom diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml index 5f27f256be..8ae8a6bcbd 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-controllers org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 @@ -60,7 +60,7 @@ com.spotify dockerfile-maven-plugin - 1.4.12 + 1.4.13 ${docker.registry.organization}/${artifactId} ${project.version} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml index aad5c6bcb0..0c4cba7069 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-controllers - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 @@ -79,7 +79,7 @@ com.spotify dockerfile-maven-plugin - 1.4.12 + 1.4.13 ${docker.registry.organization}/${artifactId} ${project.version} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml index 90bc70d489..c91269d5a3 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-controllers org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 @@ -59,7 +59,7 @@ com.spotify dockerfile-maven-plugin - 1.4.12 + 1.4.13 ${docker.registry.organization}/${artifactId} ${project.version} diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index 4dcc18c8ec..9a3fc41e03 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -23,11 +23,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.1.4-SNAPSHOT + 4.1.5-SNAPSHOT spring-cloud-kubernetes-dependencies - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT pom Spring Cloud Kubernetes :: Dependencies Spring Cloud Kubernetes Dependencies diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index 0ff563a970..c469b08d29 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index fbfb0cb6cd..07f25f0591 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes-examples - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT kubernetes-leader-election-example diff --git a/spring-cloud-kubernetes-examples/pom.xml b/spring-cloud-kubernetes-examples/pom.xml index f0d55c8ef6..742a56b606 100644 --- a/spring-cloud-kubernetes-examples/pom.xml +++ b/spring-cloud-kubernetes-examples/pom.xml @@ -23,7 +23,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT spring-cloud-kubernetes-examples diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml index 8d466974c2..da5893cab6 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml @@ -23,7 +23,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8AutoConfiguration.java b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8AutoConfiguration.java index 509be0d32b..fb52604755 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8AutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8AutoConfiguration.java @@ -18,11 +18,13 @@ import java.time.Duration; +import io.fabric8.kubernetes.client.Client; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -31,6 +33,8 @@ import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; /** * Auto configuration for Kubernetes. @@ -117,4 +121,13 @@ public Fabric8PodUtils kubernetesPodUtils(KubernetesClient client) { return new Fabric8PodUtils(client); } + @EventListener + void onContextClosed(ContextClosedEvent event) { + // Clean up any open connections from the KubernetesClient when the context is + // closed + BeanFactoryUtils.beansOfTypeIncludingAncestors(event.getApplicationContext(), KubernetesClient.class) + .values() + .forEach(Client::close); + } + } diff --git a/spring-cloud-kubernetes-fabric8-config/pom.xml b/spring-cloud-kubernetes-fabric8-config/pom.xml index ee1819e288..9a6c4c250a 100644 --- a/spring-cloud-kubernetes-fabric8-config/pom.xml +++ b/spring-cloud-kubernetes-fabric8-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java index 08c9c71f5b..278541ea66 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java @@ -34,6 +34,8 @@ import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator; import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.env.Environment; import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.registerSingle; @@ -90,7 +92,8 @@ private KubernetesClient registerConfigAndClient(ConfigurableBootstrapContext bo registerSingle(bootstrapContext, Config.class, config, "fabric8Config"); KubernetesClient kubernetesClient = new Fabric8AutoConfiguration().kubernetesClient(config); - registerSingle(bootstrapContext, KubernetesClient.class, kubernetesClient, "configKubernetesClient"); + registerSingle(bootstrapContext, KubernetesClient.class, kubernetesClient, "configKubernetesClient", + (ApplicationListener) event -> kubernetesClient.close()); return kubernetesClient; } 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..9d6be13557 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java @@ -0,0 +1,277 @@ +/* + * 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.core.env.CompositePropertySource; +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 the composite property source is empty.
+	 * 
+ */ + @Test + void namedSingleConfigMapFails(CapturedOutput output) { + 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()); + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[my-config]'"); + } + + /** + *
+	 *     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"; + + 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(); + + // one source is present + assertThat(names).containsExactly("configmap.two.default"); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[one]'"); + } + + /** + *
+	 *     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"; + + 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(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[one]'"); + assertThat(output.getOut()).contains("Failed to load source: { config-map name : 'Optional[two]'"); + } + + /** + *
+	 *     we try to read all config maps in a namespace and fail.
+	 * 
+ */ + @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(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{a=b}'"); + } + + /** + *
+	 *     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(); + + // one property source is present + assertThat(names).containsExactly("configmap.two.default"); + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{one=1}'"); + + } + + /** + *
+	 *     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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{two=2}'"); + assertThat(output.getOut()).contains("Failed to load source: { config map labels : '{one=1}'"); + } + + 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/Fabric8ConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java index e289bbc43e..9b26bf7f1b 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 @@ -16,19 +16,27 @@ package org.springframework.cloud.kubernetes.fabric8.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; 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.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -36,11 +44,17 @@ * @author Isik Erhan */ @EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) 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() { @@ -61,7 +75,7 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { } @Test - void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { + void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput output) { String name = "my-config"; String namespace = "default"; String path = "/api/v1/namespaces/default/configmaps"; @@ -74,7 +88,16 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); - assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment())); + List> result = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> { + PropertySource source = locator.locate(new MockEnvironment()); + result.add(source); + }); + + assertThat(result.get(0)).isInstanceOf(CompositePropertySource.class); + CompositePropertySource composite = (CompositePropertySource) result.get(0); + assertThat(composite.getPropertySources()).hasSize(0); + assertThat(output.getOut()).contains("Failed to load source:"); } } 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/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..00bee4b51b --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java @@ -0,0 +1,279 @@ +/* + * 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.core.env.CompositePropertySource; +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.
+	 * 
+ */ + @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()); + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { secret name : 'Optional[my-secret]'"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     one fails and one passes.
+	 * 
+ */ + @Test + void namedTwoSecretsOneFails(CapturedOutput output) { + 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(); + + // one property source is present + assertThat(names).containsExactly("secret.two.default"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { secret name : 'Optional[one]'"); + + } + + /** + *
+	 *     there are two sources and we try to read them.
+	 *     both fail.
+	 * 
+ */ + @Test + void namedTwoSecretsBothFail(CapturedOutput output) { + 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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failed to load source: { secret name : 'Optional[two]'"); + + } + + /** + *
+	 *     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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { secret labels : '{a=b}'"); + } + + /** + *
+	 *     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(); + + // one property source is present + assertThat(names).containsExactly("secret.two.default"); + + assertThat(output.getOut()).contains("Failure in reading labeled sources"); + assertThat(output.getOut()).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { secret labels : '{one=1}'"); + + } + + /** + *
+	 *     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()); + + assertThat(propertySource.getPropertySources()).isEmpty(); + + assertThat(output).contains("Failure in reading labeled sources"); + assertThat(output).contains("Failure in reading named sources"); + assertThat(output.getOut()).contains("Failed to load source: { secret labels : '{one=1}'"); + assertThat(output.getOut()).contains("Failed to load source: { secret labels : '{two=2}'"); + + } + + private Secret secret(String name, Map labels) { + return new SecretBuilder().withNewMetadata().withName(name).withLabels(labels).endMetadata().build(); + } + +} 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..e8ac439dc3 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 @@ -16,19 +16,27 @@ package org.springframework.cloud.kubernetes.fabric8.config; +import java.util.ArrayList; import java.util.List; import java.util.Map; 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.core.env.CompositePropertySource; +import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -36,11 +44,17 @@ * @author Isik Erhan */ @EnableKubernetesMockClient +@ExtendWith(OutputCaptureExtension.class) 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() { @@ -61,7 +75,7 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { } @Test - void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { + void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput output) { String name = "my-secret"; String namespace = "default"; String path = "/api/v1/namespaces/default/secrets/my-secret"; @@ -74,7 +88,17 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); - assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment())); + List> result = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> { + PropertySource source = locator.locate(new MockEnvironment()); + result.add(source); + }); + + assertThat(result.get(0)).isInstanceOf(CompositePropertySource.class); + CompositePropertySource composite = (CompositePropertySource) result.get(0); + assertThat(composite.getPropertySources()).hasSize(0); + assertThat(output.getOut()).contains("Failed to load source:"); + } } 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"; 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..7dfdfa566f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/VisibleFabric8ConfigMapPropertySourceLocator.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.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) { + 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..0f4c7c3db9 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java @@ -0,0 +1,214 @@ +/* + * 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; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "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("Failed to load source"); + 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..57a699bc5c --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java @@ -0,0 +1,221 @@ +/* + * 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; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "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("Failed to load source"); + 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..0f980577f0 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java @@ -0,0 +1,204 @@ +/* + * 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; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "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("Failed to load source"); + 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..4c100084ee --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java @@ -0,0 +1,211 @@ +/* + * 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; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.main.allow-bean-definition-overriding=true", + "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("Failed to load source"); + 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-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml index 73b4241991..5b527b30d9 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml +++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-istio/pom.xml b/spring-cloud-kubernetes-fabric8-istio/pom.xml index ddb29e7aca..2abde636e6 100644 --- a/spring-cloud-kubernetes-fabric8-istio/pom.xml +++ b/spring-cloud-kubernetes-fabric8-istio/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-fabric8-leader/pom.xml b/spring-cloud-kubernetes-fabric8-leader/pom.xml index 084f8efa4b..79e9211bc9 100644 --- a/spring-cloud-kubernetes-fabric8-leader/pom.xml +++ b/spring-cloud-kubernetes-fabric8-leader/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT spring-cloud-kubernetes-fabric8-leader diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml index 39d7feb6d3..04cefbf442 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index c17152c451..48eca0ba29 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT spring-cloud-kubernetes-integration-tests diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml index 33be033dd6..943b54ab1d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java index a3062c0981..fba6fd0712 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java @@ -25,7 +25,7 @@ */ @SpringBootApplication @EnableScheduling -public class Application { +class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java index c4a3e86d43..38b12ad248 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java @@ -28,7 +28,7 @@ * @author wind57 */ @Service -public class EndpointNameAndNamespaceService { +class EndpointNameAndNamespaceService { private List result; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java index 926c7b7f44..6ea65aae4f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java @@ -31,11 +31,11 @@ * @author wind57 */ @Component -public class HeartBeatListener implements ApplicationListener { +class HeartBeatListener implements ApplicationListener { private final EndpointNameAndNamespaceService service; - public HeartBeatListener(EndpointNameAndNamespaceService service) { + HeartBeatListener(EndpointNameAndNamespaceService service) { this.service = service; } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java index f2889faacc..75870a17cf 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java @@ -23,16 +23,16 @@ import org.springframework.web.bind.annotation.RestController; @RestController -public class HeartbeatController { +class HeartbeatController { private final EndpointNameAndNamespaceService service; - public HeartbeatController(EndpointNameAndNamespaceService service) { + HeartbeatController(EndpointNameAndNamespaceService service) { this.service = service; } @GetMapping("/result") - public List result() { + List result() { return service.result(); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml index b3f1ff57e6..ed7a0a63de 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml index 7cb749a2f5..7f6b6739f4 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-istio/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml index a2f04ddabb..0817e6dc29 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-reload/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml index 1f5896c84c..a88f0cae08 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml index ed2ac36246..1db81335ee 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-configuration-watcher/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml index 81fbd0b0c7..5a060e027f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery-server/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml index cdf6aacb3d..1d50ce8076 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml index e689f2f971..1f4a6931d7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-a/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 jar diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml index 4efc9f1d66..9bdc2d3fb8 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-app-b/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT ../../spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml index 78df763d41..b2166c1eb4 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/kafka-configmap-test-app/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 jar diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml index 450171e4f3..0f08bbeb6b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml index 47832fd7a0..33ecf23929 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-integration-tests org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml index 6e9b6299b0..30e83e17fb 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-a/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 jar diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml index c2d260596c..d059aa5b02 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-app-b/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 jar diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml index f54882a5cc..c28c9aff86 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps/rabbitmq-secret-test-app/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 jar diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml index b8e3037c3d..582020397e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT spring-cloud-kubernetes-k8s-client-reload diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml index fb84489801..2208fa1ad3 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-reload/src/main/resources/application-with-secret.yaml @@ -1,6 +1,9 @@ logging: level: - root: DEBUG + org: + springframework: + cloud: + kubernetes: debug spring: application: @@ -18,3 +21,6 @@ spring: secrets: enabled: true enable-api: true + + config: + enabled: false diff --git a/spring-cloud-kubernetes-test-support/pom.xml b/spring-cloud-kubernetes-test-support/pom.xml index 355b479811..00b4c0ae4f 100644 --- a/spring-cloud-kubernetes-test-support/pom.xml +++ b/spring-cloud-kubernetes-test-support/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-kubernetes - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java index 9d279aee3b..c6221e855f 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java @@ -276,6 +276,11 @@ private static void loadImageFromPath(String tarName, K3sContainer container) { } private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) { + + if (tarName == null) { + return false; + } + try { boolean present = container.execInContainer("sh", "-c", "ctr images list | grep " + tarName) .getStdout() diff --git a/spring-cloud-starter-kubernetes-client-all/pom.xml b/spring-cloud-starter-kubernetes-client-all/pom.xml index b9fed3e242..a6c0aad5f1 100644 --- a/spring-cloud-starter-kubernetes-client-all/pom.xml +++ b/spring-cloud-starter-kubernetes-client-all/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client-config/pom.xml b/spring-cloud-starter-kubernetes-client-config/pom.xml index 74133557dd..db509c42f1 100644 --- a/spring-cloud-starter-kubernetes-client-config/pom.xml +++ b/spring-cloud-starter-kubernetes-client-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml index 706a0a45e9..5da98b7b44 100644 --- a/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml +++ b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-client/pom.xml b/spring-cloud-starter-kubernetes-client/pom.xml index 5e35220f40..3981c1d364 100644 --- a/spring-cloud-starter-kubernetes-client/pom.xml +++ b/spring-cloud-starter-kubernetes-client/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-discoveryclient/pom.xml b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml index 1231553442..555900f83d 100644 --- a/spring-cloud-starter-kubernetes-discoveryclient/pom.xml +++ b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-all/pom.xml b/spring-cloud-starter-kubernetes-fabric8-all/pom.xml index cbf16c33f9..731bc7e7b0 100644 --- a/spring-cloud-starter-kubernetes-fabric8-all/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-all/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-config/pom.xml b/spring-cloud-starter-kubernetes-fabric8-config/pom.xml index 5be3dff932..b7a44172e4 100644 --- a/spring-cloud-starter-kubernetes-fabric8-config/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml index fadfd3ba86..5909797203 100644 --- a/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8-loadbalancer/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-fabric8/pom.xml b/spring-cloud-starter-kubernetes-fabric8/pom.xml index 11c140547c..945496dc2e 100644 --- a/spring-cloud-starter-kubernetes-fabric8/pom.xml +++ b/spring-cloud-starter-kubernetes-fabric8/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 3.1.4-SNAPSHOT + 3.1.5-SNAPSHOT 4.0.0