Rework profile-based app.yaml parsing to support yaml merging

With the current implementation, any default properties (i.e. part of yaml docs that did not specify a "spring.profile" entry) would not be read. This caused unexpected behavior where "default" keys were not being read, and if NO document specified a "spring.profile" entry, the configmap would be completely ignored.

This new logic instead performs proper yaml merges between any yaml doc that has no "spring.profile" entry, as well as docs with the specific "profile" we have active

Co-authored-by: Vivian Li <vivian@autonomic.ai>
This commit is contained in:
Rafay Khan
2018-06-20 17:28:47 -04:00
committed by Ioannis Canellos
parent 01fe68eb82
commit 50ac65024b
9 changed files with 142 additions and 29 deletions

View File

@@ -18,6 +18,7 @@
package org.springframework.cloud.kubernetes.config;
import static java.util.Arrays.asList;
import static org.springframework.beans.factory.config.YamlProcessor.MatchStatus.ABSTAIN;
import static org.springframework.beans.factory.config.YamlProcessor.MatchStatus.FOUND;
import static org.springframework.beans.factory.config.YamlProcessor.MatchStatus.NOT_FOUND;
@@ -122,13 +123,14 @@ public class ConfigMapPropertySource extends KubernetesPropertySource {
private static Function<String, Properties> yamlParserGenerator(final String[] profiles) {
return s -> {
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
if ((profiles != null) && (profiles.length > 0)){
yamlFactory.setDocumentMatchers(
(DocumentMatcher) properties ->
(asList(profiles).contains(properties.getProperty("spring.profiles")) ?
FOUND : NOT_FOUND)
);
}
yamlFactory.setDocumentMatchers(properties -> {
String profileProperty = properties.getProperty("spring.profiles");
if (profileProperty != null && profileProperty.length() > 0) {
return asList(profiles).contains(profileProperty) ? FOUND : NOT_FOUND;
} else {
return ABSTAIN;
}
});
yamlFactory.setResources(new ByteArrayResource(s.getBytes()));
return yamlFactory.getObject();
};

View File

@@ -76,7 +76,7 @@ public class ConfigMapsSpringBootTest {
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
HashMap<String,String> data = new HashMap<>();
data.put("bean.message","Hello ConfigMap, %s!");
data.put("bean.greeting","Hello ConfigMap, %s!");
server.expect().withPath("/api/v1/namespaces/test/configmaps/" + APPLICATION_NAME).andReturn(200, new ConfigMapBuilder()
.withNewMetadata().withName(APPLICATION_NAME).endMetadata()
.addToData(data)
@@ -108,7 +108,7 @@ public class ConfigMapsSpringBootTest {
public void testConfigMap() {
ConfigMap configmap = mockClient.configMaps().inNamespace("test").withName(APPLICATION_NAME).get();
HashMap<String,String> keys = (HashMap<String, String>) configmap.getData();
assertEquals(keys.get("bean.message"),"Hello ConfigMap, %s!");
assertEquals(keys.get("bean.greeting"),"Hello ConfigMap, %s!");
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.config.example.App;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
/**
@@ -82,17 +83,22 @@ public class ConfigMapsWithProfilesNoActiveProfileSpringBootTest {
.always();
}
@Before
public void setUp() {
RestAssured.baseURI = String.format("http://localhost:%d/api/greeting", port);
}
@Test
public void testGreetingEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/greeting", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Hello ConfigMap prod, World!"));
.body("content", is("Hello ConfigMap default, World!"));
}
@Test
public void testFarewellEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/farewell", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Goodbye ConfigMap default, World!"));
}
}

View File

@@ -87,17 +87,21 @@ public class ConfigMapsWithProfilesSpringBootTest {
.always();
}
@Before
public void setUp() {
RestAssured.baseURI = String.format("http://localhost:%d/api/greeting", port);
}
@Test
public void testGreetingEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/greeting", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Hello ConfigMap dev, World!"));
}
@Test
public void testFarewellEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/farewell", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Goodbye ConfigMap default, World!"));
}
}

View File

@@ -0,0 +1,83 @@
package org.springframework.cloud.kubernetes.config;
import static io.restassured.RestAssured.when;
import static org.hamcrest.core.Is.is;
import static org.springframework.cloud.kubernetes.config.ConfigMapTestUtil.readResourceFile;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.restassured.RestAssured;
import java.util.HashMap;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.config.example.App;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = App.class,
properties = { "spring.application.name=configmap-without-profile-example",
"spring.cloud.kubernetes.reload.enabled=false"}
)
@ActiveProfiles("development")
public class ConfigMapsWithoutProfilesSpringBootTest {
@ClassRule
public static KubernetesServer server = new KubernetesServer();
private static KubernetesClient mockClient;
@Autowired(required = false)
Config config;
private static final String APPLICATION_NAME = "configmap-without-profile-example";
@Value("${local.server.port}")
private int port;
@BeforeClass
public static void setUpBeforeClass() {
mockClient = server.getClient();
//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");
HashMap<String,String> data = new HashMap<>();
data.put("application.yml", readResourceFile("application-without-profiles.yaml"));
server.expect().withPath("/api/v1/namespaces/test/configmaps/" + APPLICATION_NAME).andReturn(200, new ConfigMapBuilder()
.withNewMetadata().withName(APPLICATION_NAME).endMetadata()
.addToData(data)
.build())
.always();
}
@Test
public void testGreetingEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/greeting", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Hello ConfigMap, World!"));
}
@Test
public void testFarewellEndpoint() {
RestAssured.baseURI = String.format("http://localhost:%d/api/farewell", port);
when().get()
.then()
.statusCode(200)
.body("content", is("Goodbye ConfigMap, World!"));
}
}

View File

@@ -37,7 +37,13 @@ public class GreetingController {
@RequestMapping("/api/greeting")
public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) {
String message = String.format(properties.getMessage(), name);
String message = String.format(properties.getGreeting(), name);
return new Greeting(message);
}
@RequestMapping("/api/farewell")
public Greeting farewell(@RequestParam(value="name", defaultValue="World") String name) {
String message = String.format(properties.getFarewell(), name);
return new Greeting(message);
}
}

View File

@@ -24,14 +24,22 @@ import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "bean")
public class GreetingProperties {
private String message = "Hello, %s!";
private String greeting = "Hello, %s!";
private String farewell = "Goodbye, %s!";
public String getMessage() {
return message;
public String getGreeting() {
return greeting;
}
public void setMessage(String message) {
this.message = message;
public void setGreeting(String greeting) {
this.greeting = greeting;
}
public String getFarewell() {
return farewell;
}
public void setFarewell(String farewell) {
this.farewell = farewell;
}
}

View File

@@ -1,12 +1,13 @@
bean:
message: "Hello ConfigMap default, %s!"
greeting: "Hello ConfigMap default, %s!"
farewell: "Goodbye ConfigMap default, %s!"
---
spring:
profiles: development
bean:
message: "Hello ConfigMap dev, %s!"
greeting: "Hello ConfigMap dev, %s!"
---
spring:
profiles: production
bean:
message: "Hello ConfigMap prod, %s!"
greeting: "Hello ConfigMap prod, %s!"

View File

@@ -0,0 +1,3 @@
bean:
greeting: "Hello ConfigMap, %s!"
farewell: "Goodbye ConfigMap, %s!"