From 2b9709b4a0ae26e962604198e711c2eeb86bd4cb Mon Sep 17 00:00:00 2001 From: Arjav <54466460+arjavdongaonkar@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:26:18 +0530 Subject: [PATCH] Kubernetes as a Composite config source (#1873) * initial commit, crearting factory Signed-off-by: Arjav * testing default Signed-off-by: Arjav * remove checkstyle Signed-off-by: Arjav * for try Signed-off-by: Arjav * for try Signed-off-by: Arjav * trying existing bean Signed-off-by: Arjav * reverting pom.xml Signed-off-by: Arjav * ObjectProvider for missing bean Signed-off-by: Arjav * Update default order value in KubernetesConfigServerProperties Signed-off-by: Arjav * code style fixes Signed-off-by: Arjav * code style fixes Signed-off-by: Arjav * code style fixes Signed-off-by: Arjav * code style Signed-off-by: Arjav * test cases Signed-off-by: Arjav * Composite Integration Tests where kubernetes is one of the type in the composite setup KubernetesEnvironmentRepository as condition for KubernetesEnvironmentRepositoryFactory bean creation Signed-off-by: Arjav * Composite Integration Tests where kubernetes is one of the type in the composite setup KubernetesEnvironmentRepository as condition for KubernetesEnvironmentRepositoryFactory bean creation Signed-off-by: Arjav * Add KubernetesConfigServerProperties support to KubernetesEnvironmentRepository and related tests Signed-off-by: Arjav * mark old implementation as deprecated and revert test cases Signed-off-by: Arjav * test case for new constructor Signed-off-by: Arjav * linting Signed-off-by: Arjav * remove ObjectProvider Signed-off-by: Arjav --------- Signed-off-by: Arjav --- ...bernetesConfigServerAutoConfiguration.java | 19 +- .../KubernetesConfigServerProperties.java | 13 +- .../KubernetesEnvironmentRepository.java | 24 +- ...ubernetesEnvironmentRepositoryFactory.java | 40 ++ ...ithGitAndKubernetesConfigSourcesTests.java | 53 +++ ...hMultipleKubernetesConfigSourcesTests.java | 52 +++ ...leWithOnlyKubernetesConfigSourceTests.java | 49 +++ ...oConfigurationKubernetesDisabledTests.java | 1 + ...ProfileIncludedConfigApiDisabledTests.java | 1 + ...etesEnvironmentRepositoryFactoryTests.java | 66 ++++ .../KubernetesEnvironmentRepositoryTests.java | 42 +++ .../CompositeKubernetesIntegrationTests.java | 356 ++++++++++++++++++ 12 files changed, 710 insertions(+), 6 deletions(-) create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactory.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithGitAndKubernetesConfigSourcesTests.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithMultipleKubernetesConfigSourcesTests.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithOnlyKubernetesConfigSourceTests.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactoryTests.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java index 1028151d..88905606 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java @@ -23,12 +23,13 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration; -import org.springframework.cloud.config.server.environment.EnvironmentRepository; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigContext; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; @@ -59,11 +60,21 @@ public class KubernetesConfigServerAutoConfiguration { @Bean @Profile("kubernetes") - public EnvironmentRepository kubernetesEnvironmentRepository(CoreV1Api coreV1Api, + @ConditionalOnMissingBean + public KubernetesEnvironmentRepository kubernetesEnvironmentRepository(CoreV1Api coreV1Api, List kubernetesPropertySourceSuppliers, - KubernetesNamespaceProvider kubernetesNamespaceProvider) { + KubernetesNamespaceProvider kubernetesNamespaceProvider, + KubernetesConfigServerProperties kubernetesConfigServerProperties) { return new KubernetesEnvironmentRepository(coreV1Api, kubernetesPropertySourceSuppliers, - kubernetesNamespaceProvider.getNamespace()); + kubernetesNamespaceProvider.getNamespace(), kubernetesConfigServerProperties); + } + + @Bean + @ConditionalOnBean(KubernetesEnvironmentRepository.class) + @ConditionalOnMissingBean + public KubernetesEnvironmentRepositoryFactory kubernetesEnvironmentRepositoryFactory( + KubernetesEnvironmentRepository kubernetesEnvironmentRepository) { + return new KubernetesEnvironmentRepositoryFactory(kubernetesEnvironmentRepository); } @Bean diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java index 1e753994..ab8eb42b 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java @@ -17,13 +17,16 @@ package org.springframework.cloud.kubernetes.configserver; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.config.server.support.EnvironmentRepositoryProperties; /** * @author Ryan Baxter */ @ConfigurationProperties("spring.cloud.kubernetes.configserver") -public class KubernetesConfigServerProperties { +public class KubernetesConfigServerProperties implements EnvironmentRepositoryProperties { + + private int order = DEFAULT_ORDER; private String configMapNamespaces = ""; @@ -45,4 +48,12 @@ public class KubernetesConfigServerProperties { this.secretsNamespaces = secretsNamespaces; } + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java index aa377751..30fba4c5 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.core.Ordered; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.StandardEnvironment; @@ -36,7 +37,7 @@ import org.springframework.util.StringUtils; /** * @author Ryan Baxter */ -public class KubernetesEnvironmentRepository implements EnvironmentRepository { +public class KubernetesEnvironmentRepository implements EnvironmentRepository, Ordered { private static final Log LOG = LogFactory.getLog(KubernetesEnvironmentRepository.class); @@ -46,6 +47,9 @@ public class KubernetesEnvironmentRepository implements EnvironmentRepository { private final String namespace; + private int order = KubernetesConfigServerProperties.DEFAULT_ORDER; + + @Deprecated public KubernetesEnvironmentRepository(CoreV1Api coreApi, List kubernetesPropertySourceSuppliers, String namespace) { this.coreApi = coreApi; @@ -53,6 +57,15 @@ public class KubernetesEnvironmentRepository implements EnvironmentRepository { this.namespace = namespace; } + public KubernetesEnvironmentRepository(CoreV1Api coreApi, + List kubernetesPropertySourceSuppliers, String namespace, + KubernetesConfigServerProperties properties) { + this.coreApi = coreApi; + this.kubernetesPropertySourceSuppliers = kubernetesPropertySourceSuppliers; + this.namespace = namespace; + this.order = properties.getOrder(); + } + @Override public Environment findOne(String application, String profile, String label) { return findOne(application, profile, label, true); @@ -118,4 +131,13 @@ public class KubernetesEnvironmentRepository implements EnvironmentRepository { }); } + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactory.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactory.java new file mode 100644 index 00000000..8a7de0f3 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.configserver; + +import org.springframework.cloud.config.server.environment.EnvironmentRepositoryFactory; + +/** + * Factory class for creating instances of {@link KubernetesEnvironmentRepository}. + * + * @author Arjav Dongaonkar + */ +public class KubernetesEnvironmentRepositoryFactory + implements EnvironmentRepositoryFactory { + + private final KubernetesEnvironmentRepository kubernetesEnvironmentRepository; + + public KubernetesEnvironmentRepositoryFactory(KubernetesEnvironmentRepository kubernetesEnvironmentRepository) { + this.kubernetesEnvironmentRepository = kubernetesEnvironmentRepository; + } + + @Override + public KubernetesEnvironmentRepository build(KubernetesConfigServerProperties environmentProperties) { + return kubernetesEnvironmentRepository; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithGitAndKubernetesConfigSourcesTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithGitAndKubernetesConfigSourcesTests.java new file mode 100644 index 00000000..7797a7f9 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithGitAndKubernetesConfigSourcesTests.java @@ -0,0 +1,53 @@ +/* + * 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.configserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.server.environment.JGitEnvironmentRepository; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjav Dongaonkar + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.profiles.include=kubernetes", + "spring.cloud.kubernetes.client.namespace=default", "spring.profiles.active=composite", + "spring.cloud.config.server.composite[0].type=git", + "spring.cloud.config.server.composite[0].uri=https://github.com/spring-cloud-samples/config-repo", + "spring.cloud.config.server.composite[1].type=kubernetes", + "spring.cloud.config.server.composite[1].config-map-namespace=default", + "spring.cloud.config.server.composite[1].secrets-namespace=default" }) +class CompositeProfileWithGitAndKubernetesConfigSourcesTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(2); + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepositoryFactory.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).isNotEmpty(); + assertThat(context.getBeanNamesForType(JGitEnvironmentRepository.class)).hasSize(1); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithMultipleKubernetesConfigSourcesTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithMultipleKubernetesConfigSourcesTests.java new file mode 100644 index 00000000..1c083237 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithMultipleKubernetesConfigSourcesTests.java @@ -0,0 +1,52 @@ +/* + * 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.configserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjav Dongaonkar + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.profiles.include=kubernetes", + "spring.cloud.kubernetes.client.namespace=default", "spring.profiles.active=composite", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.config.server.composite[0].config-map-namespace=default", + "spring.cloud.config.server.composite[0].secrets-namespace=default", + "spring.cloud.config.server.composite[1].type=kubernetes", + "spring.cloud.config.server.composite[1].config-map-namespace=another-namespace", + "spring.cloud.config.server.composite[1].secrets-namespace=another-namespace" }) +class CompositeProfileWithMultipleKubernetesConfigSourcesTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(3); + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepositoryFactory.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).isNotEmpty(); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithOnlyKubernetesConfigSourceTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithOnlyKubernetesConfigSourceTests.java new file mode 100644 index 00000000..fe1a5e6a --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/CompositeProfileWithOnlyKubernetesConfigSourceTests.java @@ -0,0 +1,49 @@ +/* + * 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.configserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjav Dongaonkar + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.profiles.include=kubernetes", + "spring.cloud.kubernetes.client.namespace=default", "spring.profiles.active=composite", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.config.server.composite[0].config-map-namespace=default", + "spring.cloud.config.server.composite[0].secrets-namespace=default" }) +class CompositeProfileWithOnlyKubernetesConfigSourceTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(2); + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepositoryFactory.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).isNotEmpty(); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesDisabledTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesDisabledTests.java index 84d69211..1864cdbd 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesDisabledTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesDisabledTests.java @@ -38,6 +38,7 @@ class ConfigServerAutoConfigurationKubernetesDisabledTests { @Test void runTest() { assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(0); + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepositoryFactory.class)).hasSize(0); } } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesEnabledProfileIncludedConfigApiDisabledTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesEnabledProfileIncludedConfigApiDisabledTests.java index d800e068..e82baedb 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesEnabledProfileIncludedConfigApiDisabledTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerAutoConfigurationKubernetesEnabledProfileIncludedConfigApiDisabledTests.java @@ -40,6 +40,7 @@ class ConfigServerAutoConfigurationKubernetesEnabledProfileIncludedConfigApiDisa void runTest() { assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(0); + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepositoryFactory.class)).hasSize(1); } } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactoryTests.java new file mode 100644 index 00000000..e99b83ad --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryFactoryTests.java @@ -0,0 +1,66 @@ +/* + * 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.configserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SpringJUnitConfig +@SpringBootTest +class KubernetesEnvironmentRepositoryFactoryTests { + + @MockBean + private KubernetesEnvironmentRepository mockRepository; + + @Test + void testBuild() { + KubernetesEnvironmentRepositoryFactory factory = new KubernetesEnvironmentRepositoryFactory(mockRepository); + KubernetesConfigServerProperties properties = new KubernetesConfigServerProperties(); + + EnvironmentRepository repository = factory.build(properties); + + assertThat(repository).isNotNull(); + assertThat(repository).isInstanceOf(KubernetesEnvironmentRepository.class); + assertThat(repository).isSameAs(mockRepository); + } + + @Configuration + static class TestConfig { + + @Bean + public KubernetesEnvironmentRepository kubernetesEnvironmentRepository() { + return mock(KubernetesEnvironmentRepository.class); + } + + @Bean + public KubernetesEnvironmentRepositoryFactory kubernetesEnvironmentRepositoryFactory( + KubernetesEnvironmentRepository kubernetesEnvironmentRepository) { + return new KubernetesEnvironmentRepositoryFactory(kubernetesEnvironmentRepository); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java index f34aed50..eaa87623 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -190,6 +190,48 @@ class KubernetesEnvironmentRepositoryTests { }); } + @Test + public void testApplicationCaseWithNewConstructor() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + KubernetesConfigServerProperties properties = mock(KubernetesConfigServerProperties.class); + when(properties.getOrder()).thenReturn(0); + + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEV_LIST); + + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default", properties); + + Environment environment = environmentRepository.findOne("application", "", ""); + + assertThat(environment.getPropertySources().size()).isEqualTo(2); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secret.application.default")) + .isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + + assertThat(environmentRepository.getOrder()).isEqualTo(0); + } + @Test public void testStoresCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java new file mode 100644 index 00000000..00c07893 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java @@ -0,0 +1,356 @@ +/* + * 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.configserver.it; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +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.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigContext; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; +import org.springframework.cloud.kubernetes.commons.config.Constants; +import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerApplication; +import org.springframework.cloud.kubernetes.configserver.KubernetesPropertySourceSupplier; +import org.springframework.core.env.MapPropertySource; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * @author Arjav Dongaonkar + */ +public class CompositeKubernetesIntegrationTests { + + private static final List KUBERNETES_PROPERTY_SOURCE_SUPPLIER = new ArrayList<>(); + + private static V1ConfigMap buildConfigMap(String name, String namespace) { + return new V1ConfigMapBuilder() + .withMetadata( + new V1ObjectMetaBuilder().withName(name).withNamespace(namespace).withResourceVersion("1").build()) + .addToData(Constants.APPLICATION_YAML, "dummy:\n property:\n string: \"" + name + "\"\n") + .build(); + } + + private static V1Secret buildSecret(String name, String namespace) { + return new V1SecretBuilder() + .withMetadata( + new V1ObjectMetaBuilder().withName(name).withResourceVersion("0").withNamespace(namespace).build()) + .addToData("password", "p455w0rd".getBytes()) + .addToData("username", "user".getBytes()) + .build(); + } + + private static final V1ConfigMapList CONFIGMAP_DEFAULT_LIST = new V1ConfigMapList() + .addItemsItem(buildConfigMap("gateway", "default")); + + private static final V1SecretList SECRET_DEFAULT_LIST = new V1SecretListBuilder() + .addToItems(buildSecret("gateway", "default")) + .build(); + + @BeforeAll + public static void before() { + KUBERNETES_PROPERTY_SOURCE_SUPPLIER.add((coreApi, applicationName, namespace, springEnv) -> { + List propertySources = new ArrayList<>(); + + NormalizedSource defaultSource = new NamedConfigMapNormalizedSource(applicationName, "default", false, + true); + KubernetesClientConfigContext defaultContext = new KubernetesClientConfigContext(coreApi, defaultSource, + "default", springEnv); + propertySources.add(new KubernetesClientConfigMapPropertySource(defaultContext)); + return propertySources; + }); + } + + @AfterEach + public void after() { + new KubernetesClientConfigMapsCache().discardAll(); + } + + @Nested + @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", + "spring.config.name=compositeconfigserver", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.kubernetes.secrets.enableApi=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "composite", "kubernetes" }) + class KubernetesCompositeConfigServerTest { + + @LocalServerPort + private int port; + + @MockBean + private CoreV1Api coreV1Api; + + @Test + public void contextLoads() throws ApiException { + when(coreV1Api.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreV1Api.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_DEFAULT_LIST); + + ResponseEntity response = new RestTemplate().exchange( + "http://localhost:" + this.port + "/gateway/default", HttpMethod.GET, null, Environment.class); + + Environment environment = response.getBody(); + assertThat(environment).isNotNull(); + assertThat(environment.getPropertySources()).hasSize(4); + assertThat(environment.getPropertySources().get(0).getName()) + .isEqualTo("configmap.gateway.default.default"); + assertThat(environment.getPropertySources().get(1).getName()).contains("secret.gateway.default.default"); + } + + } + + @Nested + @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.config.server.composite[1].type=native", + "spring.cloud.config.server.composite[1].location=file:./native-config", + "spring.cloud.kubernetes.secrets.enableApi=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "composite", "kubernetes", "native" }) + class KubernetesSecretsEnabledCompositeConfigServerTest { + + @LocalServerPort + private int port; + + @MockBean + private CoreV1Api coreV1Api; + + @SpyBean + private NativeEnvironmentRepository nativeEnvironmentRepository; + + @Test + public void contextLoads() throws Exception { + when(coreV1Api.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreV1Api.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_DEFAULT_LIST); + + Environment mockNativeEnvironment = new Environment("gateway", "default"); + mockNativeEnvironment.add(new PropertySource("nativeProperties", Map.of("key1", "value1"))); + + when(nativeEnvironmentRepository.findOne(anyString(), anyString(), eq(null), anyBoolean())) + .thenReturn(mockNativeEnvironment); + + ResponseEntity response = new RestTemplate().exchange( + "http://localhost:" + this.port + "/gateway/default", HttpMethod.GET, null, Environment.class); + + Environment environment = response.getBody(); + assertThat(environment).isNotNull(); + assertThat(environment.getPropertySources()).hasSizeGreaterThanOrEqualTo(5); + + assertThat(environment.getPropertySources().get(0).getName()) + .isEqualTo("configmap.gateway.default.default"); + assertThat(environment.getPropertySources().get(1).getName()).contains("secret.gateway.default.default"); + + assertThat(environment.getPropertySources()).anyMatch(ps -> ps.getName().contains("native")); + } + + } + + @Nested + @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.config.server.composite[1].type=native", + "spring.cloud.config.server.composite[1].location=file:./native-config", + "spring.cloud.kubernetes.config.enableApi=false", + "spring.cloud.kubernetes.secrets.enableApi=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "composite", "kubernetes", "native" }) + class KubernetesConfigMapDisabledCompositeConfigServerTest { + + @LocalServerPort + private int port; + + @MockBean + private CoreV1Api coreV1Api; + + @SpyBean + private NativeEnvironmentRepository nativeEnvironmentRepository; + + @Test + public void contextLoads() throws Exception { + when(coreV1Api.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreV1Api.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_DEFAULT_LIST); + + Environment mockNativeEnvironment = new Environment("gateway", "default"); + mockNativeEnvironment.add(new PropertySource("nativeProperties", Map.of("key1", "value1"))); + + when(nativeEnvironmentRepository.findOne(anyString(), anyString(), eq(null), anyBoolean())) + .thenReturn(mockNativeEnvironment); + + ResponseEntity response = new RestTemplate().exchange( + "http://localhost:" + this.port + "/gateway/default", HttpMethod.GET, null, Environment.class); + + Environment environment = response.getBody(); + assertThat(environment).isNotNull(); + assertThat(environment.getPropertySources()).hasSizeGreaterThanOrEqualTo(3); + + assertThat(environment.getPropertySources().get(0).getName()).isEqualTo("secret.gateway.default.default"); + assertThat(environment.getPropertySources().get(1).getName()).contains("nativeProperties"); + + assertThat(environment.getPropertySources()).anyMatch(ps -> ps.getName().contains("native")); + } + + } + + @Nested + @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.config.server.composite[0].type=kubernetes", + "spring.cloud.config.server.composite[1].type=native", + "spring.cloud.config.server.composite[1].location=file:./native-config" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "composite", "kubernetes", "native" }) + class KubernetesSecretsDisabledCompositeConfigServerTest { + + @LocalServerPort + private int port; + + @MockBean + private CoreV1Api coreV1Api; + + @SpyBean + private NativeEnvironmentRepository nativeEnvironmentRepository; + + @Test + public void contextLoads() throws Exception { + when(coreV1Api.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreV1Api.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_DEFAULT_LIST); + + Environment mockNativeEnvironment = new Environment("gateway", "default"); + mockNativeEnvironment.add(new PropertySource("nativeProperties", Map.of("key1", "value1"))); + + when(nativeEnvironmentRepository.findOne(anyString(), anyString(), eq(null), anyBoolean())) + .thenReturn(mockNativeEnvironment); + + ResponseEntity response = new RestTemplate().exchange( + "http://localhost:" + this.port + "/gateway/default", HttpMethod.GET, null, Environment.class); + + Environment environment = response.getBody(); + assertThat(environment).isNotNull(); + assertThat(environment.getPropertySources()).hasSizeGreaterThanOrEqualTo(3); + + assertThat(environment.getPropertySources().get(0).getName()) + .isEqualTo("configmap.gateway.default.default"); + assertThat(environment.getPropertySources().get(1).getName()).contains("nativeProperties"); + + assertThat(environment.getPropertySources()).anyMatch(ps -> ps.getName().contains("native")); + } + + } + + @Nested + @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.config.name:compositeconfigserver", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.config.server.native.search-locations=file:./native-config", + "spring.cloud.config.server.native.order=1", "spring.cloud.kubernetes.configserver.order=2", + "spring.cloud.kubernetes.secrets.enableApi=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "native", "kubernetes" }) + class NativeAndKubernetesConfigServerTest { + + @LocalServerPort + private int port; + + @MockBean + private CoreV1Api coreV1Api; + + @SpyBean + private NativeEnvironmentRepository nativeEnvironmentRepository; + + @Test + public void contextLoads() throws Exception { + when(coreV1Api.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreV1Api.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SECRET_DEFAULT_LIST); + + Environment mockNativeEnvironment = new Environment("gateway", "default"); + mockNativeEnvironment.add(new PropertySource("nativeProperties", Map.of("key1", "value1"))); + + when(nativeEnvironmentRepository.findOne(anyString(), anyString(), eq(null), anyBoolean())) + .thenReturn(mockNativeEnvironment); + + ResponseEntity response = new RestTemplate().exchange( + "http://localhost:" + this.port + "/gateway/default", HttpMethod.GET, null, Environment.class); + + Environment environment = response.getBody(); + + assert environment != null; + assertThat(3).isEqualTo(environment.getPropertySources().size()); + assertThat("nativeProperties").isEqualTo(environment.getPropertySources().get(0).getName()); + assertThat(environment.getPropertySources().get(1).getName().contains("configmap.gateway.default.default") + && !environment.getPropertySources().get(1).getName().contains("nativeProperties")) + .isTrue(); + assertThat(environment.getPropertySources().get(2).getName()).contains("secret.gateway.default.default"); + } + + } + +}