diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/MountSecretPropertySource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/MountSecretPropertySource.java new file mode 100644 index 0000000000..b07c1f0311 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/MountSecretPropertySource.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-present 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.commons.config; + +/** + * @author wind57 + */ +public final class MountSecretPropertySource extends SecretsPropertySource { + + public MountSecretPropertySource(SourceData sourceData) { + super(sourceData); + } + +} 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 b878d1cf0e..c919c8e291 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 @@ -152,17 +152,17 @@ protected void putPathConfig(CompositePropertySource composite) { * @author wind57 */ private static class SecretsPropertySourceCollector - implements Collector, List> { + implements Collector, List> { @Override - public Supplier> supplier() { + public Supplier> supplier() { return ArrayList::new; } @Override - public BiConsumer, Path> accumulator() { + public BiConsumer, Path> accumulator() { return (list, filePath) -> { - SecretsPropertySource source = property(filePath); + MountSecretPropertySource source = property(filePath); if (source != null) { list.add(source); } @@ -170,7 +170,7 @@ public BiConsumer, Path> accumulator() { } @Override - public BinaryOperator> combiner() { + public BinaryOperator> combiner() { return (left, right) -> { left.addAll(right); return left; @@ -178,7 +178,7 @@ public BinaryOperator> combiner() { } @Override - public Function, List> finisher() { + public Function, List> finisher() { return Function.identity(); } @@ -187,7 +187,7 @@ public Set characteristics() { return EnumSet.of(Characteristics.UNORDERED, Characteristics.IDENTITY_FINISH); } - private SecretsPropertySource property(Path filePath) { + private MountSecretPropertySource property(Path filePath) { String fileName = filePath.getFileName().toString(); @@ -195,7 +195,7 @@ private SecretsPropertySource property(Path filePath) { String content = new String(Files.readAllBytes(filePath)).trim(); String sourceName = fileName.toLowerCase(Locale.ROOT); SourceData sourceData = new SourceData(sourceName, Map.of(fileName, content)); - return new SecretsPropertySource(sourceData); + return new MountSecretPropertySource(sourceData); } catch (IOException e) { LOG.warn("Error reading properties file", e); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java index 1f63362b96..3b4a0b25da 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java @@ -27,7 +27,7 @@ import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; -import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.config.MountSecretPropertySource; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -89,6 +89,7 @@ public static boolean reload(PropertySourceLocator locator, ConfigurableEnvironm * @deprecated this method will not be public in the next major release. */ @Deprecated(forRemoval = false) + @SuppressWarnings("unchecked") public static > List findPropertySources(Class sourceClass, ConfigurableEnvironment environment) { List managedSources = new ArrayList<>(); @@ -111,9 +112,9 @@ else if (source instanceof MountConfigMapPropertySource mountConfigMapPropertySo // we know that the type is correct here managedSources.add((S) mountConfigMapPropertySource); } - else if (source instanceof SecretsPropertySource secretsPropertySource) { + else if (source instanceof MountSecretPropertySource mountSecretPropertySource) { // we know that the type is correct here - managedSources.add((S) secretsPropertySource); + managedSources.add((S) mountSecretPropertySource); } else if (source instanceof BootstrapPropertySource bootstrapPropertySource) { PropertySource propertySource = bootstrapPropertySource.getDelegate(); @@ -125,9 +126,9 @@ else if (propertySource instanceof MountConfigMapPropertySource mountConfigMapPr // we know that the type is correct here managedSources.add((S) mountConfigMapPropertySource); } - else if (propertySource instanceof SecretsPropertySource secretsPropertySource) { + else if (propertySource instanceof MountSecretPropertySource mountSecretPropertySource) { // we know that the type is correct here - managedSources.add((S) secretsPropertySource); + managedSources.add((S) mountSecretPropertySource); } } } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java index cd447851dd..418ebee0f7 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtilTests.java @@ -27,7 +27,7 @@ import org.springframework.cloud.bootstrap.config.BootstrapPropertySource; import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource; -import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.config.MountSecretPropertySource; import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.EnumerablePropertySource; @@ -157,7 +157,7 @@ public Object getProperty(String name) { void testSecretsPropertySource() { MockEnvironment environment = new MockEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); - propertySources.addFirst(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b")))); + propertySources.addFirst(new MountSecretPropertySource(new SourceData("secret", Map.of("a", "b")))); List result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class, environment); @@ -170,7 +170,7 @@ void testBootstrapSecretsPropertySource() { MockEnvironment environment = new MockEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); propertySources - .addFirst(new OneBootstrap<>(new SecretsPropertySource(new SourceData("secret", Map.of("a", "b"))))); + .addFirst(new OneBootstrap<>(new MountSecretPropertySource(new SourceData("secret", Map.of("a", "b"))))); List result = ConfigReloadUtil.findPropertySources(PlainPropertySource.class, environment); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java index 69e7a97131..c5ed535567 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/example/App.java @@ -25,7 +25,7 @@ */ @EnableConfigurationProperties(GreetingProperties.class) @SpringBootApplication -class App { +public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java new file mode 100644 index 0000000000..4f117f2f4a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapAndSecretTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2012-present 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.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +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.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.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.example.App; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Proves that + * this + * issue is fixed. + * + * @author wind57 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.cloud.bootstrap.enabled=true", "spring.cloud.kubernetes.config.enabled=true", + "spring.cloud.bootstrap.name=polling-reload-configmap-and-secret", + "spring.main.cloud-platform=KUBERNETES", "spring.application.name=polling-reload-configmap-and-secret", + "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.kubernetes.client.namespace=spring-k8s", + "logging.level.org.springframework.cloud.kubernetes.commons.config.reload=debug" }, + classes = { PollingReloadConfigMapAndSecretTest.TestConfig.class, App.class }) +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class PollingReloadConfigMapAndSecretTest { + + private static final String NAMESPACE = "spring-k8s"; + + private static final AtomicBoolean STRATEGY_FOR_SECRET_CALLED = new AtomicBoolean(false); + + private static KubernetesClient mockClient; + + @Autowired + private ConfigurableEnvironment environment; + + @BeforeAll + static void beforeAll() { + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + // namespace: spring-k8s, name: secret-a + Map secretA = Collections.singletonMap("one", + Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8))); + createSecret("secret-a", secretA); + + // namespace: spring-k8s, name: secret-b + Map secretB = Collections.singletonMap("two", + Base64.getEncoder().encodeToString("b".getBytes(StandardCharsets.UTF_8))); + createSecret("secret-b", secretB); + + // namespace: spring-k8s, name: configmap-a + Map configMapA = Collections.singletonMap("one", "a"); + createConfigMap("configmap-a", configMapA); + + // namespace: spring-k8s, name: configmap-b + Map configMapB = Collections.singletonMap("two", "b"); + createConfigMap("configmap-b", configMapB); + + } + + @Test + void test(CapturedOutput output) { + + Set sources = environment.getPropertySources() + .stream() + .map(PropertySource::getName) + .collect(Collectors.toSet()); + assertThat(sources).contains("bootstrapProperties-configmap.configmap-b.spring-k8s", + "bootstrapProperties-configmap.configmap-a.spring-k8s", + "bootstrapProperties-secret.secret-b.spring-k8s", "bootstrapProperties-secret.secret-a.spring-k8s"); + + // 1. first, wait for a cycle where we see the configmaps as being the same + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(() -> output.getOut() + .contains("Reloadable condition was not satisfied, reload will not be triggered")); + + // 2. then change a configmap, so the cycle seems them as different and triggers a + // reload + Map configMapA = Collections.singletonMap("one", "aa"); + replaceConfigMap("configmap-a", configMapA); + + // 3. reload is triggered + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofSeconds(1)) + .until(STRATEGY_FOR_SECRET_CALLED::get); + + } + + private static void createSecret(String name, Map data) { + mockClient.secrets() + .inNamespace(NAMESPACE) + .resource(new SecretBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .create(); + } + + private static void createConfigMap(String name, Map data) { + mockClient.configMaps() + .inNamespace(NAMESPACE) + .resource(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .create(); + } + + private static void replaceConfigMap(String name, Map data) { + mockClient.configMaps() + .inNamespace(NAMESPACE) + .resource(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()) + .createOrReplace(); + } + + @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 + ConfigReloadProperties configReloadProperties() { + return new ConfigReloadProperties(true, true, true, ConfigReloadProperties.ReloadStrategy.REFRESH, + ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(200), Set.of(NAMESPACE), + false, Duration.ofSeconds(2)); + } + + @Bean + @Primary + ConfigurationUpdateStrategy secretConfigurationUpdateStrategy() { + return new ConfigurationUpdateStrategy("to-console", () -> STRATEGY_FOR_SECRET_CALLED.set(true)); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml new file mode 100644 index 0000000000..102672e48c --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/polling-reload-configmap-and-secret.yaml @@ -0,0 +1,24 @@ +spring: + application: + name: polling-reload-configmap-and-secret + cloud: + kubernetes: + reload: + enabled: true + monitoring-config-maps: true + monitoring-secrets: true + mode: polling + config: + namespace: spring-k8s + sources: + - name: configmap-a + - name: configmap-b + enable-api: true + include-profile-specific-sources: false + secrets: + namespace: spring-k8s + sources: + - name: secret-a + - name: secret-b + enable-api: true + include-profile-specific-sources: false