Config Server Controller Should Return All PropertySources (#1600)

This commit is contained in:
Ryan Baxter
2024-03-28 09:59:29 -04:00
committed by GitHub
parent 52f410648a
commit cb3d7ca781
23 changed files with 443 additions and 103 deletions

View File

@@ -36,6 +36,7 @@ import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecret
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesConfigEnabled;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesSecretsEnabled;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigUtils;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
@@ -77,9 +78,9 @@ public class KubernetesConfigServerAutoConfiguration {
namespaces.forEach(space -> {
NamedConfigMapNormalizedSource source = new NamedConfigMapNormalizedSource(applicationName, space,
false, true);
false, ConfigUtils.Prefix.DEFAULT, true, true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space,
springEnv);
springEnv, false);
propertySources.add(new KubernetesClientConfigMapPropertySource(context));
});
@@ -96,9 +97,10 @@ public class KubernetesConfigServerAutoConfiguration {
List<MapPropertySource> propertySources = new ArrayList<>();
namespaces.forEach(space -> {
NormalizedSource source = new NamedSecretNormalizedSource(applicationName, space, false, false);
NormalizedSource source = new NamedSecretNormalizedSource(applicationName, space, false,
ConfigUtils.Prefix.DEFAULT, true, true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space,
springEnv);
springEnv, false);
propertySources.add(new KubernetesClientSecretsPropertySource(context));
});

View File

@@ -0,0 +1,31 @@
/*
* 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.configserver;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.StandardEnvironment;
/**
* @author Ryan Baxter
*/
public class KubernetesConfigServerEnvironment extends StandardEnvironment {
KubernetesConfigServerEnvironment(MutablePropertySources mutablePropertySources) {
super(mutablePropertySources);
}
}

View File

@@ -16,7 +16,11 @@
package org.springframework.cloud.kubernetes.configserver;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.apache.commons.logging.Log;
@@ -26,6 +30,7 @@ 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.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.util.StringUtils;
@@ -56,26 +61,51 @@ public class KubernetesEnvironmentRepository implements EnvironmentRepository {
@Override
public Environment findOne(String application, String profile, String label, boolean includeOrigin) {
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
if (!StringUtils.hasText(profile)) {
profile = "default";
}
List<String> profiles = new java.util.ArrayList<>(
Arrays.stream(StringUtils.commaDelimitedListToStringArray(profile)).toList());
Collections.reverse(profiles);
if (!profiles.contains("default")) {
profiles.add("default");
}
Environment environment = new Environment(application, profiles.toArray(profiles.toArray(new String[0])), label,
null, null);
LOG.info("Profiles: " + profile);
LOG.info("Application: " + application);
LOG.info("Label: " + label);
Environment environment = new Environment(application, profiles, label, null, null);
try {
StandardEnvironment springEnv = new StandardEnvironment();
springEnv.setActiveProfiles(profiles);
if (!"application".equalsIgnoreCase(application)) {
addApplicationConfiguration(environment, springEnv, application);
for (String activeProfile : profiles) {
try {
// This is needed so that when we get the application name in
// SourceDataProcessor.sorted that it actually
// exists in the Environment
StandardEnvironment springEnv = new KubernetesConfigServerEnvironment(
createPropertySources(application));
springEnv.setActiveProfiles(activeProfile);
if (!"application".equalsIgnoreCase(application)) {
addApplicationConfiguration(environment, springEnv, application);
}
}
catch (Exception e) {
LOG.warn(e);
}
addApplicationConfiguration(environment, springEnv, "application");
return environment;
}
catch (Exception e) {
LOG.warn(e);
}
StandardEnvironment springEnv = new KubernetesConfigServerEnvironment(createPropertySources("application"));
addApplicationConfiguration(environment, springEnv, "application");
return environment;
}
private MutablePropertySources createPropertySources(String application) {
Map<String, Object> applicationProperties = new HashMap<>();
applicationProperties.put("spring.application.name", application);
MapPropertySource propertySource = new MapPropertySource("kubernetes-config-server", applicationProperties);
MutablePropertySources mutablePropertySources = new MutablePropertySources();
mutablePropertySources.addFirst(propertySource);
return mutablePropertySources;
}
private void addApplicationConfiguration(Environment environment, StandardEnvironment springEnv,
String applicationName) {
kubernetesPropertySourceSuppliers.stream().forEach(supplier -> {

View File

@@ -27,6 +27,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
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.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -34,7 +35,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.cloud.config.environment.Environment;
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.client.config.KubernetesClientSecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.ConfigUtils;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
@@ -56,6 +59,19 @@ class KubernetesEnvironmentRepositoryTests {
private static final String DEFAULT_NAMESPACE = "default";
private static final V1ConfigMapList CONFIGMAP_ONE_LIST = new V1ConfigMapList()
.addItemsItem(new V1ConfigMapBuilder()
.withMetadata(
new V1ObjectMetaBuilder().withName("storessingle").withNamespace(DEFAULT_NAMESPACE).build())
.addToData("storessingle.yaml", VALUE)
.addToData("storessingle-dev.yaml",
"dummy:\n property:\n string2: \"dev\"\n int2: 1\n bool2: false\n")
.addToData("storessingle-qa.yaml",
"dummy:\n property:\n string2: \"qa\"\n int2: 2\n bool2: true\n")
.addToData("storessingle-prod.yaml",
"dummy:\n property:\n string2: \"prod\"\n int2: 3\n bool2: true\n")
.build());
private static final V1ConfigMapList CONFIGMAP_DEFAULT_LIST = new V1ConfigMapList()
.addItemsItem(new V1ConfigMapBuilder()
.withMetadata(
@@ -105,13 +121,14 @@ class KubernetesEnvironmentRepositoryTests {
true);
KubernetesClientConfigContext defaultContext = new KubernetesClientConfigContext(coreApi, defaultSource,
"default", springEnv);
NormalizedSource devSource = new NamedConfigMapNormalizedSource(applicationName, "dev", false, true);
KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "dev",
springEnv);
propertySources.add(new KubernetesClientConfigMapPropertySource(defaultContext));
propertySources.add(new KubernetesClientConfigMapPropertySource(devContext));
if ("stores".equals(applicationName) && "dev".equals(namespace)) {
NormalizedSource devSource = new NamedConfigMapNormalizedSource(applicationName, "dev", false, true);
KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "dev",
springEnv);
propertySources.add(new KubernetesClientConfigMapPropertySource(devContext));
}
return propertySources;
});
KUBERNETES_PROPERTY_SOURCE_SUPPLIER.add((coreApi, applicationName, namespace, springEnv) -> {
@@ -126,6 +143,11 @@ class KubernetesEnvironmentRepositoryTests {
});
}
@AfterEach
public void after() {
new KubernetesClientConfigMapsCache().discardAll();
}
@Test
public void testApplicationCase() throws ApiException {
CoreV1Api coreApi = mock(CoreV1Api.class);
@@ -168,12 +190,11 @@ class KubernetesEnvironmentRepositoryTests {
KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi,
KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default");
Environment environment = environmentRepository.findOne("stores", "", "");
assertThat(environment.getPropertySources().size()).isEqualTo(5);
assertThat(environment.getPropertySources().size()).isEqualTo(4);
environment.getPropertySources().forEach(propertySource -> {
assertThat(propertySource.getName().equals("configmap.application.default")
|| propertySource.getName().equals("secret.application.default")
|| propertySource.getName().equals("configmap.stores.default")
|| propertySource.getName().equals("configmap.stores.dev")
|| propertySource.getName().equals("secret.stores.default")).isTrue();
if (propertySource.getName().equals("configmap.application.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(3);
@@ -192,12 +213,6 @@ class KubernetesEnvironmentRepositoryTests {
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a");
}
if (propertySource.getName().equals("configmap.stores.dev")) {
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("dev");
}
if (propertySource.getName().equals("secret.stores.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(2);
assertThat(propertySource.getSource().get("username")).isEqualTo("stores");
@@ -218,13 +233,14 @@ class KubernetesEnvironmentRepositoryTests {
KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi,
KUBERNETES_PROPERTY_SOURCE_SUPPLIER, "default");
Environment environment = environmentRepository.findOne("stores", "dev", "");
assertThat(environment.getPropertySources().size()).isEqualTo(5);
assertThat(environment.getPropertySources().size()).isEqualTo(6);
environment.getPropertySources().forEach(propertySource -> {
assertThat(propertySource.getName().equals("configmap.application.default")
|| propertySource.getName().equals("secret.application.default")
|| propertySource.getName().equals("configmap.stores.stores-dev.default")
|| propertySource.getName().equals("configmap.stores.dev")
|| propertySource.getName().equals("secret.stores.stores-dev.default")).isTrue();
|| propertySource.getName().equals("secret.stores.default")
|| propertySource.getName().equals("secret.stores.stores-dev.default")
|| propertySource.getName().equals("configmap.stores.default")).isTrue();
if (propertySource.getName().equals("configmap.application.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(3);
assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1);
@@ -236,24 +252,29 @@ class KubernetesEnvironmentRepositoryTests {
assertThat(propertySource.getSource().get("username")).isEqualTo("user");
assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd");
}
else if (propertySource.getName().equals("configmap.stores.stores-dev.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(4);
assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(2);
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(false);
assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("b");
assertThat(propertySource.getSource().get("dummy.property.string1")).isEqualTo("a");
}
else if (propertySource.getName().equals("configmap.stores.dev")) {
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("dev");
}
else if (propertySource.getName().equals("secret.stores.stores-dev.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(2);
assertThat(propertySource.getSource().get("username")).isEqualTo("stores-dev");
assertThat(propertySource.getSource().get("password")).isEqualTo("password-from-stores-dev");
}
else if (propertySource.getName().equals("configmap.stores.stores-dev.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(4);
assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(2);
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(false);
assertThat(propertySource.getSource().get("dummy.property.string1")).isEqualTo("a");
assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("b");
}
else if (propertySource.getName().equals("configmap.stores.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");
}
else if (propertySource.getName().equals("secret.stores.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(2);
assertThat(propertySource.getSource().get("username")).isEqualTo("stores");
assertThat(propertySource.getSource().get("password")).isEqualTo("password-from-stores");
}
else {
Assertions.fail("no match in property source names");
}
@@ -292,4 +313,52 @@ class KubernetesEnvironmentRepositoryTests {
});
}
@Test
public void testSingleConfigMapMultipleSources() throws ApiException {
CoreV1Api coreApi = mock(CoreV1Api.class);
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_ONE_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(new V1SecretList());
List<KubernetesPropertySourceSupplier> suppliers = new ArrayList<>();
suppliers.add((coreV1Api, name, namespace, environment) -> {
List<MapPropertySource> propertySources = new ArrayList<>();
NormalizedSource devSource = new NamedConfigMapNormalizedSource(name, namespace, false,
ConfigUtils.Prefix.DEFAULT, true, true);
KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "default",
environment);
propertySources.add(new KubernetesClientConfigMapPropertySource(devContext));
return propertySources;
});
KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, suppliers,
"default");
Environment environment = environmentRepository.findOne("storessingle", "", "");
assertThat(environment.getPropertySources().size()).isEqualTo(1);
assertThat(environment.getPropertySources().get(0).getName())
.isEqualTo("configmap.storessingle.default.default");
environment = environmentRepository.findOne("storessingle", "dev", "");
assertThat(environment.getPropertySources().size()).isEqualTo(2);
assertThat(environment.getPropertySources().get(0).getName()).isEqualTo("configmap.storessingle.default.dev");
assertThat(environment.getPropertySources().get(1).getName())
.isEqualTo("configmap.storessingle.default.default");
environment = environmentRepository.findOne("storessingle", "dev,prod", "");
assertThat(environment.getPropertySources().size()).isEqualTo(3);
assertThat(environment.getPropertySources().get(0).getName()).isEqualTo("configmap.storessingle.default.prod");
assertThat(environment.getPropertySources().get(0).getSource().get("dummy.property.int2")).isEqualTo(3);
assertThat(environment.getPropertySources().get(0).getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(environment.getPropertySources().get(0).getSource().get("dummy.property.string2")).isEqualTo("prod");
assertThat(environment.getPropertySources().get(1).getName()).isEqualTo("configmap.storessingle.default.dev");
assertThat(environment.getPropertySources().get(1).getSource().get("dummy.property.int2")).isEqualTo(1);
assertThat(environment.getPropertySources().get(1).getSource().get("dummy.property.bool2")).isEqualTo(false);
assertThat(environment.getPropertySources().get(1).getSource().get("dummy.property.string2")).isEqualTo("dev");
assertThat(environment.getPropertySources().get(2).getName())
.isEqualTo("configmap.storessingle.default.default");
assertThat(environment.getPropertySources().get(2).getSource().get("dummy.property.int2")).isEqualTo(1);
assertThat(environment.getPropertySources().get(2).getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(environment.getPropertySources().get(2).getSource().get("dummy.property.string2")).isEqualTo("a");
}
}

View File

@@ -49,7 +49,7 @@ class KubernetesPropertySourceSupplierTests {
private static final CoreV1Api coreApi = mock(CoreV1Api.class);
private static final V1ConfigMapList CONFIGMAP_DEFAULT_LIST = new V1ConfigMapList()
.addItemsItem(buildConfigMap("gateway", "tdefault"));
.addItemsItem(buildConfigMap("gateway", "default"));
private static final V1ConfigMapList CONFIGMAP_TEAM_A_LIST = new V1ConfigMapList()
.addItemsItem(buildConfigMap("stores", "team-a"));

View File

@@ -51,8 +51,17 @@ abstract class ConfigServerIntegrationTest {
@BeforeEach
void beforeEach() {
V1ConfigMapList TEST_CONFIGMAP = new V1ConfigMapList().addItemsItem(new V1ConfigMapBuilder().withMetadata(
new V1ObjectMetaBuilder().withName("test-cm").withNamespace("default").withResourceVersion("1").build())
V1ConfigMapList TEST_CONFIGMAP = new V1ConfigMapList().addItemsItem(new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("test-cm").withNamespace("default")
.withResourceVersion("1").build())
.addToData("test-cm-dev.yaml",
"dummy:\n property:\n string2: \"dev\"\n int2: 1\n bool2: false\n")
.addToData("test-cm-qa.yaml",
"dummy:\n property:\n string2: \"qa\"\n int2: 2\n bool2: true\n")
.addToData("test-cm-prod.yaml",
"dummy:\n property:\n string2: \"prod\"\n int2: 3\n bool2: true\n")
.addToData("test-cm.yaml",
"dummy:\n property:\n string2: \"default\"\n int2: 4\n bool2: true\n")
.addToData("app.name", "test").build());
V1SecretList TEST_SECRET = new V1SecretListBuilder()
@@ -74,11 +83,34 @@ abstract class ConfigServerIntegrationTest {
void enabled() {
Environment env = testRestTemplate.getForObject("/test-cm/default", Environment.class);
assertThat(env.getPropertySources().size()).isEqualTo(2);
assertThat(env.getPropertySources().get(0).getName().equals("configmap.test-cm.default")).isTrue();
assertThat(env.getPropertySources().get(0).getName().equals("configmap.test-cm.default.default")).isTrue();
assertThat(env.getPropertySources().get(0).getSource().get("app.name")).isEqualTo("test");
assertThat(env.getPropertySources().get(1).getName().equals("secret.test-cm.default")).isTrue();
assertThat(env.getPropertySources().get(1).getName().equals("secret.test-cm.default.default")).isTrue();
assertThat(env.getPropertySources().get(1).getSource().get("password")).isEqualTo("p455w0rd");
assertThat(env.getPropertySources().get(1).getSource().get("username")).isEqualTo("user");
Environment devprod = testRestTemplate.getForObject("/test-cm/dev,prod", Environment.class);
assertThat(devprod.getPropertySources().size()).isEqualTo(4);
assertThat(devprod.getPropertySources().get(0).getName().equals("configmap.test-cm.default.prod")).isTrue();
assertThat(devprod.getPropertySources().get(0).getSource().size()).isEqualTo(3);
assertThat(devprod.getPropertySources().get(0).getSource().get("dummy.property.int2")).isEqualTo(3);
assertThat(devprod.getPropertySources().get(0).getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(devprod.getPropertySources().get(0).getSource().get("dummy.property.string2")).isEqualTo("prod");
assertThat(devprod.getPropertySources().get(1).getName().equals("configmap.test-cm.default.dev")).isTrue();
assertThat(devprod.getPropertySources().get(1).getSource().size()).isEqualTo(3);
assertThat(devprod.getPropertySources().get(1).getSource().get("dummy.property.int2")).isEqualTo(1);
assertThat(devprod.getPropertySources().get(1).getSource().get("dummy.property.bool2")).isEqualTo(false);
assertThat(devprod.getPropertySources().get(1).getSource().get("dummy.property.string2")).isEqualTo("dev");
assertThat(devprod.getPropertySources().get(2).getName().equals("configmap.test-cm.default.default")).isTrue();
assertThat(devprod.getPropertySources().get(2).getSource().size()).isEqualTo(4);
assertThat(devprod.getPropertySources().get(2).getSource().get("app.name")).isEqualTo("test");
assertThat(devprod.getPropertySources().get(2).getSource().get("dummy.property.int2")).isEqualTo(4);
assertThat(devprod.getPropertySources().get(2).getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(devprod.getPropertySources().get(2).getSource().get("dummy.property.string2")).isEqualTo("default");
assertThat(devprod.getPropertySources().get(3).getName().equals("secret.test-cm.default.default")).isTrue();
assertThat(devprod.getPropertySources().get(3).getSource().size()).isEqualTo(2);
assertThat(devprod.getPropertySources().get(3).getSource().get("password")).isEqualTo("p455w0rd");
assertThat(devprod.getPropertySources().get(3).getSource().get("username")).isEqualTo("user");
}
}