This commit is contained in:
erabii
2022-06-16 22:09:22 +03:00
committed by GitHub
parent c019cfa451
commit c7e104bed7
26 changed files with 1257 additions and 14 deletions

View File

@@ -65,8 +65,38 @@ of the application is resolved.
Any matching `ConfigMap` that is found is processed as follows:
* Apply individual configuration properties.
* Apply as `yaml` the content of any property named `application.yaml`.
* Apply as a properties file the content of any property named `application.properties`.
* Apply as `yaml` (or `properties`) the content of any property that is named by the value of `spring.application.name`
(if it's not present, by `application.yaml/properties`)
* Apply as a properties file the content of the above name + each active profile.
An example should make a lot more sense. Let's suppose that `spring.application.name=my-app` and that
we have a single active profile called `k8s`. For a configuration as below:
====
[source]
----
kind: ConfigMap
apiVersion: v1
metadata:
name: my-app
data:
my-app.yaml: |-
...
my-app-k8s.yaml: |-
..
my-app-dev.yaml: |-
..
someProp: someValue
----
====
These is what we will end-up loading:
- `my-app.yaml` treated as a file
- `my-app-k8s.yaml` treated as a file
- `my-app-dev.yaml` _ignored_, since `dev` is _not_ an active profile
- `someProp: someValue` plain property
The single exception to the aforementioned flow is when the `ConfigMap` contains a *single* key that indicates
the file is a YAML or properties file. In that case, the name of the key does NOT have to be `application.yaml` or

View File

@@ -0,0 +1,35 @@
/*
* 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.applications.single_source_multiple_files;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Color;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Name;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Shape;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Type;
@SpringBootApplication
@EnableConfigurationProperties({ Name.class, Shape.class, Color.class, Type.class })
public class SingleSourceMultipleFilesApp {
public static void main(String[] args) {
SpringApplication.run(SingleSourceMultipleFilesApp.class, args);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.applications.single_source_multiple_files;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/**
* @author wind57
*/
@ActiveProfiles("color")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SingleSourceMultipleFilesApp.class,
properties = { "spring.cloud.bootstrap.name=single-source-multiple-files",
"spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true",
"spring.cloud.kubernetes.client.namespace=spring-k8s", "single.source.multiple.files.stub=true" })
class SingleSourceMultipleFilesBootstrapTests extends SingleSourceMultipleFilesTests {
}

View File

@@ -0,0 +1,63 @@
/*
* 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.applications.single_source_multiple_files;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.mockito.MockedStatic;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
import org.springframework.test.context.ActiveProfiles;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.mockito.Mockito.mockStatic;
import static org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub.stubData;
/**
* @author wind57
*/
@ActiveProfiles("color")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SingleSourceMultipleFilesApp.class,
properties = { "spring.main.cloud-platform=KUBERNETES",
"spring.config.import=kubernetes:,classpath:./single-source-multiple-files.yaml",
"spring.cloud.kubernetes.client.namespace=spring-k8s" })
class SingleSourceMultipleFilesConfigDataTests extends SingleSourceMultipleFilesTests {
private static MockedStatic<KubernetesClientUtils> clientUtilsMock;
@BeforeAll
static void wireMock() {
WireMockServer server = new WireMockServer(options().dynamicPort());
server.start();
WireMock.configureFor("localhost", server.port());
clientUtilsMock = mockStatic(KubernetesClientUtils.class);
clientUtilsMock.when(KubernetesClientUtils::kubernetesApiClient)
.thenReturn(new ClientBuilder().setBasePath(server.baseUrl()).build());
stubData();
}
@AfterAll
static void teardown() {
clientUtilsMock.close();
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.applications.single_source_multiple_files;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author wind57
*
* Stub for this test is here :
* {@link org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub}
*
* issue: https://github.com/spring-cloud/spring-cloud-kubernetes/issues/640
*
*/
abstract class SingleSourceMultipleFilesTests {
@Autowired
private WebTestClient webClient;
@AfterEach
void afterEach() {
WireMock.reset();
}
@AfterAll
static void afterAll() {
WireMock.shutdownServer();
}
/**
* <pre>
* "fruit-color.properties" is taken since "spring.application.name=fruit" and
* "color" is an active profile
* </pre>
*/
@Test
void color() {
this.webClient.get().uri("/single_source-multiple-files/color").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("raw:green###ripe:yellow"));
}
/**
* <pre>
* "fruit.properties" is read, since it matches "spring.application.name"
* </pre>
*/
@Test
void name() {
this.webClient.get().uri("/single_source-multiple-files/name").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("banana"));
}
/**
* <pre>
* shape profile is not active, thus property "fruit-shape.properties" is skipped
* and as such, a null comes here.
* </pre>
*/
@Test
void shape() {
this.webClient.get().uri("/single_source-multiple-files/shape").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.nullValue());
}
/**
* <pre>
* this is a non-file property in the configmap
* </pre>
*/
@Test
void type() {
this.webClient.get().uri("/single_source-multiple-files/type").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("yummy"));
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.client.config.applications.single_source_multiple_files.controller;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Color;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Name;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Shape;
import org.springframework.cloud.kubernetes.client.config.applications.single_source_multiple_files.properties.Type;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SingleSourceMultipleFilesController {
private final Name name;
private final Shape shape;
private final Color color;
private final Type type;
public SingleSourceMultipleFilesController(Name name, Shape shape, Color color, Type type) {
this.name = name;
this.shape = shape;
this.color = color;
this.type = type;
}
@GetMapping("/single_source-multiple-files/type")
public String type() {
return type.getType();
}
@GetMapping("/single_source-multiple-files/shape")
public String shape() {
return shape.getRaw();
}
@GetMapping("/single_source-multiple-files/color")
public String color() {
return "raw:" + color.getRaw() + "###" + "ripe:" + color.getRipe();
}
@GetMapping("/single_source-multiple-files/name")
public String name() {
return name.getName();
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.client.config.applications.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "color.when")
public class Color {
private String raw;
private String ripe;
public String getRaw() {
return raw;
}
public void setRaw(String raw) {
this.raw = raw;
}
public String getRipe() {
return ripe;
}
public void setRipe(String ripe) {
this.ripe = ripe;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.applications.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("cool")
public class Name {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.applications.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("shape.when")
public class Shape {
private String raw;
private String ripe;
public String getRaw() {
return raw;
}
public void setRaw(String raw) {
this.raw = raw;
}
public String getRipe() {
return ripe;
}
public void setRipe(String ripe) {
this.ripe = ripe;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.applications.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("fruit")
public class Type {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.boostrap.stubs;
import java.util.HashMap;
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.JSON;
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.util.ClientBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
/**
* A test bootstrap that takes care to initialize ApiClient _before_ our main bootstrap
* context; with some stub data already present.
*
* @author wind57
*/
@Order(0)
@Configuration
@ConditionalOnProperty("single.source.multiple.files.stub")
public class SingleSourceMultipleFilesConfigurationStub {
@Bean
public WireMockServer wireMock() {
WireMockServer server = new WireMockServer(options().dynamicPort());
server.start();
WireMock.configureFor("localhost", server.port());
return server;
}
@Bean
public ApiClient apiClient(WireMockServer wireMockServer) {
ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
io.kubernetes.client.openapi.Configuration.setDefaultApiClient(apiClient);
stubData();
return apiClient;
}
public static void stubData() {
Map<String, String> one = new HashMap<>();
one.put("fruit.type", "yummy");
one.put("fruit.properties", "cool.name=banana");
one.put("fruit-color.properties", "color.when.raw=green\ncolor.when.ripe=yellow");
// this is not taken, since "shape" is not an active profile
one.put("fruit-shape.properties", "shape.when.raw=small-sphere\nshape.when.ripe=bigger-sphere");
V1ConfigMap configMap = new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("my-configmap").withNamespace("spring-k8s").build())
.addToData(one).build();
V1ConfigMapList allConfigMaps = new V1ConfigMapList();
allConfigMaps.addItemsItem(configMap);
// the actual stub for CoreV1Api calls
WireMock.stubFor(WireMock.get("/api/v1/namespaces/spring-k8s/configmaps")
.willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(allConfigMaps))));
}
}

View File

@@ -1,10 +1,11 @@
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithProfileConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedConfigMapWithPrefixConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithPrefixConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithPrefixConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.NamedSecretWithProfileConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithPrefixConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledSecretWithProfileConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithPrefixConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.LabeledConfigMapWithProfileConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.boostrap.stubs.SingleSourceMultipleFilesConfigurationStub, \
org.springframework.cloud.kubernetes.client.config.EnableRetryBootstrapConfiguration

View File

@@ -0,0 +1,9 @@
spring:
application:
name: fruit
cloud:
kubernetes:
config:
namespace: spring-k8s
sources:
- name: my-configmap

View File

@@ -16,11 +16,13 @@
package org.springframework.cloud.kubernetes.commons.config;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -28,9 +30,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_PROPERTIES;
import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML;
import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YML;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.PROPERTIES_TO_MAP;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.throwingMerger;
@@ -76,21 +75,45 @@ public class SourceDataEntriesProcessor extends MapPropertySource {
private static Map<String, Object> defaultProcessAllEntries(Map<String, String> input, Environment environment) {
return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), environment))
// we pass empty Strings on purpose, the logic here is either the value of
// "spring.application.name"
// or literal "application".
String applicationName = ConfigUtils.getApplicationName(environment, "", "");
String[] activeProfiles = environment.getActiveProfiles();
Set<String> fileNames = Stream
.concat(Stream.of(applicationName),
Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile))
.collect(Collectors.toSet());
return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), fileNames, environment))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, throwingMerger(), HashMap::new));
}
private static Map<String, Object> extractProperties(String resourceName, String content, Environment environment) {
private static Map<String, Object> extractProperties(String resourceName, String content, Set<String> fileNames,
Environment environment) {
if (resourceName.equals(APPLICATION_YAML) || resourceName.equals(APPLICATION_YML)) {
return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content);
}
else if (resourceName.equals(APPLICATION_PROPERTIES)) {
return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content);
if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) {
if (fileNames.contains(resourceName.split("\\.", 2)[0])) {
if (resourceName.endsWith(".properties")) {
LOG.debug("entry : " + resourceName + " will be treated as a single properties file");
return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content);
}
else {
LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file");
return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content);
}
}
else {
LOG.warn("entry : " + resourceName + " will be skipped");
return Collections.emptyMap();
}
}
return Collections.singletonMap(resourceName, content);
}
}

View File

@@ -0,0 +1,174 @@
/*
* 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.commons.config;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
/**
* @author wind57
*/
class SourceDataEntriesProcessorTests {
@Test
void testSingleYml() {
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(Map.of("one.yml", "key: \n value"),
new MockEnvironment());
Assertions.assertEquals(1, result.size());
Assertions.assertEquals("value", result.get("key"));
}
@Test
void testSingleYaml() {
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(Map.of("one.yaml", "key: \n value"),
new MockEnvironment());
Assertions.assertEquals(1, result.size());
Assertions.assertEquals("value", result.get("key"));
}
@Test
void testSingleProperties() {
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(Map.of("one.properties", "key=value"),
new MockEnvironment());
Assertions.assertEquals(1, result.size());
Assertions.assertEquals("value", result.get("key"));
}
/**
* <pre>
* two properties present, none are file treated
* </pre>
*/
@Test
void twoEntriesNoneFileTreated() {
Map.Entry<String, String> one = Map.entry("one", "1");
Map.Entry<String, String> two = Map.entry("two", "2");
Map<String, String> map = Map.ofEntries(one, two);
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, new MockEnvironment());
Assertions.assertEquals(2, result.size());
Assertions.assertEquals("1", result.get("one"));
Assertions.assertEquals("2", result.get("two"));
}
/**
* <pre>
* - two properties present, none are file treated.
* - even if there is a application.yaml, it is not taken since it is != spring.application.name
* </pre>
*/
@Test
void twoEntriesOneIsYamlButNotTaken() {
Map.Entry<String, String> one = Map.entry("one", "1");
Map.Entry<String, String> myName = Map.entry("my-name.yaml", "color: \n blue");
Map<String, String> map = Map.ofEntries(one, myName);
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, new MockEnvironment());
Assertions.assertEquals(1, result.size());
Assertions.assertEquals("1", result.get("one"));
}
/**
* <pre>
* - two properties present, both taken.
* - second one is treated as a file, since it's name matches "spring.application.name"
* </pre>
*/
@Test
void twoEntriesBothTaken() {
Map.Entry<String, String> one = Map.entry("one", "1");
Map.Entry<String, String> application = Map.entry("application.yaml", "color: \n blue");
Map<String, String> map = Map.ofEntries(one, application);
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, new MockEnvironment());
Assertions.assertEquals(2, result.size());
Assertions.assertEquals("1", result.get("one"));
Assertions.assertEquals("blue", result.get("color"));
}
/**
* <pre>
* - three properties present, all taken.
* - second one is treated as a file, since it's name matches "spring.application.name"
* - third one is taken since it matches one active profile
* </pre>
*/
@Test
void threeEntriesAllTaken() {
Map.Entry<String, String> one = Map.entry("one", "1");
Map.Entry<String, String> application = Map.entry("application.properties", "color=blue");
Map.Entry<String, String> applicationDev = Map.entry("application-dev.properties", "fit=sport");
Map<String, String> map = Map.ofEntries(one, application, applicationDev);
MockEnvironment env = new MockEnvironment();
env.setActiveProfiles("dev");
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, env);
Assertions.assertEquals(3, result.size());
Assertions.assertEquals("1", result.get("one"));
Assertions.assertEquals("blue", result.get("color"));
Assertions.assertEquals("sport", result.get("fit"));
}
/**
* <pre>
* - five properties present, four are taken
* - second one is treated as a file, since it's name matches "spring.application.name"
* - third one is taken since it matches one active profile
* - fourth one is taken since it matches one active profile
* </pre>
*/
@Test
void fiveEntriesFourTaken() {
Map.Entry<String, String> one = Map.entry("one", "1");
Map.Entry<String, String> jacket = Map.entry("jacket.properties", "name=jacket");
Map.Entry<String, String> jacketFit = Map.entry("jacket-fit.properties", "fit=sport");
Map.Entry<String, String> jacketColor = Map.entry("jacket-color.properties", "color=black");
Map.Entry<String, String> jacketSeason = Map.entry("jacket-season.properties", "season=summer");
Map<String, String> map = Map.ofEntries(one, jacket, jacketFit, jacketColor, jacketSeason);
MockEnvironment env = new MockEnvironment();
env.setProperty("spring.application.name", "jacket");
env.setActiveProfiles("fit", "color");
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, env);
Assertions.assertEquals(4, result.size());
Assertions.assertEquals("1", result.get("one"));
Assertions.assertEquals("jacket", result.get("name"));
Assertions.assertEquals("sport", result.get("fit"));
Assertions.assertEquals("black", result.get("color"));
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.fabric8.config.single_source_multiple_files;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Color;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Name;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Shape;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Type;
@SpringBootApplication
@EnableConfigurationProperties({ Name.class, Shape.class, Color.class, Type.class })
public class SingleSourceMultipleFilesApp {
public static void main(String[] args) {
SpringApplication.run(SingleSourceMultipleFilesApp.class, args);
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.fabric8.config.single_source_multiple_files;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/**
* @author wind57
*/
@ActiveProfiles("color")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SingleSourceMultipleFilesApp.class,
properties = { "spring.cloud.bootstrap.name=single-source-multiple-files",
"spring.main.cloud-platform=KUBERNETES", "spring.cloud.bootstrap.enabled=true" })
@EnableKubernetesMockClient(crud = true, https = false)
class SingleSourceMultipleFilesBootstrapTests extends SingleSourceMultipleFilesTests {
private static KubernetesClient mockClient;
@BeforeAll
static void setUpBeforeClass() {
setUpBeforeClass(mockClient);
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.fabric8.config.single_source_multiple_files;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/**
* @author wind57
*/
@ActiveProfiles("color")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SingleSourceMultipleFilesApp.class,
properties = { "spring.main.cloud-platform=KUBERNETES",
"spring.config.import=kubernetes:,classpath:./single-source-multiple-files.yaml" })
@EnableKubernetesMockClient(crud = true, https = false)
class SingleSourceMultipleFilesConfigDataTests extends SingleSourceMultipleFilesTests {
private static KubernetesClient mockClient;
@BeforeAll
static void setUpBeforeClass() {
setUpBeforeClass(mockClient);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.fabric8.config.single_source_multiple_files;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author wind57
*
* issue: https://github.com/spring-cloud/spring-cloud-kubernetes/issues/640
*/
abstract class SingleSourceMultipleFilesTests {
private static KubernetesClient mockClient;
@Autowired
private WebTestClient webClient;
static void setUpBeforeClass(KubernetesClient mockClient) {
SingleSourceMultipleFilesTests.mockClient = mockClient;
// 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");
Map<String, String> one = new HashMap<>();
one.put("fruit.type", "yummy");
one.put("fruit.properties", "cool.name=banana");
one.put("fruit-color.properties", "color.when.raw=green\ncolor.when.ripe=yellow");
// this is not taken, since "shape" is not an active profile
one.put("fruit-shape.properties", "shape.when.raw=small-sphere\nshape.when.ripe=bigger-sphere");
createConfigmap(one);
}
static void createConfigmap(Map<String, String> data) {
mockClient.configMaps().inNamespace("spring-k8s").create(new ConfigMapBuilder().withNewMetadata()
.withName("my-configmap").endMetadata().addToData(data).build());
}
/**
* <pre>
* "fruit-color.properties" is taken since "spring.application.name=fruit" and
* "color" is an active profile
* </pre>
*/
@Test
void color() {
this.webClient.get().uri("/single_source-multiple-files/color").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("raw:green###ripe:yellow"));
}
/**
* <pre>
* "fruit.properties" is read, since it matches "spring.application.name"
* </pre>
*/
@Test
void name() {
this.webClient.get().uri("/single_source-multiple-files/name").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("banana"));
}
/**
* <pre>
* shape profile is not active, thus property "fruit-shape.properties" is skipped
* and as such, a null comes here.
* </pre>
*/
@Test
void shape() {
this.webClient.get().uri("/single_source-multiple-files/shape").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.nullValue());
}
/**
* <pre>
* this is a non-file property in the configmap
* </pre>
*/
@Test
void type() {
this.webClient.get().uri("/single_source-multiple-files/type").exchange().expectStatus().isOk()
.expectBody(String.class).value(Matchers.equalTo("yummy"));
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.fabric8.config.single_source_multiple_files.controller;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Color;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Name;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Shape;
import org.springframework.cloud.kubernetes.fabric8.config.single_source_multiple_files.properties.Type;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SingleSourceMultipleFilesController {
private final Name name;
private final Shape shape;
private final Color color;
private final Type type;
public SingleSourceMultipleFilesController(Name name, Shape shape, Color color, Type type) {
this.name = name;
this.shape = shape;
this.color = color;
this.type = type;
}
@GetMapping("/single_source-multiple-files/type")
public String type() {
return type.getType();
}
@GetMapping("/single_source-multiple-files/shape")
public String shape() {
return shape.getRaw();
}
@GetMapping("/single_source-multiple-files/color")
public String color() {
return "raw:" + color.getRaw() + "###" + "ripe:" + color.getRipe();
}
@GetMapping("/single_source-multiple-files/name")
public String name() {
return name.getName();
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.fabric8.config.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "color.when")
public class Color {
private String raw;
private String ripe;
public String getRaw() {
return raw;
}
public void setRaw(String raw) {
this.raw = raw;
}
public String getRipe() {
return ripe;
}
public void setRipe(String ripe) {
this.ripe = ripe;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.fabric8.config.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("cool")
public class Name {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.fabric8.config.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("shape.when")
public class Shape {
private String raw;
private String ripe;
public String getRaw() {
return raw;
}
public void setRaw(String raw) {
this.raw = raw;
}
public String getRipe() {
return ripe;
}
public void setRipe(String ripe) {
this.ripe = ripe;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.fabric8.config.single_source_multiple_files.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("fruit")
public class Type {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,9 @@
spring:
application:
name: fruit
cloud:
kubernetes:
config:
namespace: spring-k8s
sources:
- name: my-configmap

View File

@@ -83,7 +83,9 @@ public class LeaderInitiatorTest {
assertThat(this.leaderInitiator.isRunning()).isTrue();
verify(this.mockFabric8LeaderRecordWatcher).start();
verify(this.mockFabric8PodReadinessWatcher).start();
Thread.sleep(10);
// TODO this tests needs to be reviewed not to use sleep
Thread.sleep(1000);
verify(this.mockFabric8LeadershipController, atLeastOnce()).update();
}