Allow label to be a comman-separated list on config client
The client will search through a list of labels until it finds one that succeeds, otherwise fail. Fixes gh-153
This commit is contained in:
@@ -189,7 +189,7 @@ Config Server comes with a Health Indicator that checks if the configured
|
||||
for an application named `app`, the `default` profile and the default
|
||||
label provided by the `EnvironmentRepository` implementation.
|
||||
|
||||
You can configure the Health Indicator to check more appliations
|
||||
You can configure the Health Indicator to check more applications
|
||||
along with custom profiles and custom labels, e.g.
|
||||
|
||||
----
|
||||
@@ -419,8 +419,7 @@ an Exception.
|
||||
|
||||
=== Locating Remote Configuration Resources
|
||||
|
||||
The Config Service serves property sources from `/{name}/{env}/{label}`, where the default bindings in the
|
||||
client app are
|
||||
The Config Service serves property sources from `/{name}/{env}/{label}`, where the default bindings in the client app are
|
||||
|
||||
* "name" = `${spring.application.name}`
|
||||
* "env" = `${spring.profiles.active}` (actually `Environment.getActiveProfiles()`)
|
||||
@@ -430,7 +429,11 @@ All of them can be overridden by setting `spring.cloud.config.\*`
|
||||
(where `*` is "name", "env" or "label"). The "label" is useful for
|
||||
rolling back to previous versions of configuration; with the default
|
||||
Config Server implementation it can be a git label, branch name or
|
||||
commit id.
|
||||
commit id. Label can also be provided as a comma-separated list, in
|
||||
which case the items in the list are tried on-by-one until one succeeds.
|
||||
This can be useful when working on a feature branch, for instance,
|
||||
when you might want to align the config label with your branch, but
|
||||
make it optional (e.g. `spring.cloud.config.label=myfeature,develop`).
|
||||
|
||||
=== Security
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
@@ -58,7 +60,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(interceptor="configServerRetryInterceptor")
|
||||
@Retryable(interceptor = "configServerRetryInterceptor")
|
||||
public org.springframework.core.env.PropertySource<?> locate(
|
||||
org.springframework.core.env.Environment environment) {
|
||||
ConfigClientProperties client = defaults.override(environment);
|
||||
@@ -68,22 +70,24 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
Exception error = null;
|
||||
String errorBody = null;
|
||||
try {
|
||||
Object[] args = new String[] { client.getName(), client.getProfile() };
|
||||
String path = "/{name}/{profile}";
|
||||
String[] labels = new String[]{""};
|
||||
if (StringUtils.hasText(client.getLabel())) {
|
||||
args = new String[] { client.getName(), client.getProfile(),
|
||||
client.getLabel() };
|
||||
path = path + "/{label}";
|
||||
labels = StringUtils.commaDelimitedListToStringArray(client.getLabel());
|
||||
}
|
||||
Environment result = restTemplate.exchange(client.getRawUri() + path,
|
||||
HttpMethod.GET, new HttpEntity<Void>((Void) null), Environment.class,
|
||||
args).getBody();
|
||||
for (PropertySource source : result.getPropertySources()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) source.getSource();
|
||||
composite.addPropertySource(new MapPropertySource(source.getName(), map));
|
||||
// Try all the labels until one works
|
||||
for (String label : labels) {
|
||||
Environment result = getRemoteEnvironment(restTemplate, client.getRawUri(), client.getName(), client.getProfile(), label.trim());
|
||||
if (result != null) {
|
||||
for (PropertySource source : result.getPropertySources()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) source
|
||||
.getSource();
|
||||
composite.addPropertySource(new MapPropertySource(source
|
||||
.getName(), map));
|
||||
}
|
||||
return composite;
|
||||
}
|
||||
}
|
||||
return composite;
|
||||
}
|
||||
catch (HttpServerErrorException e) {
|
||||
error = e;
|
||||
@@ -101,11 +105,28 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
error);
|
||||
}
|
||||
logger.error("Could not locate PropertySource: "
|
||||
+ (errorBody == null ? error.getMessage() : errorBody));
|
||||
+ (errorBody == null ? error==null ? "label not found" : error.getMessage() : errorBody));
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private Environment getRemoteEnvironment(RestTemplate restTemplate, String uri, String name, String profile, String label) {
|
||||
String path = "/{name}/{profile}";
|
||||
Object[] args = new String[] { name, profile };
|
||||
if (StringUtils.hasText(label)) {
|
||||
args = new String[] { name, profile, label };
|
||||
path = path + "/{label}";
|
||||
}
|
||||
ResponseEntity<Environment> response = restTemplate.exchange(uri + path,
|
||||
HttpMethod.GET, new HttpEntity<Void>((Void) null),
|
||||
Environment.class, args);
|
||||
if (response==null || response.getStatusCode()!=HttpStatus.OK) {
|
||||
return null;
|
||||
}
|
||||
Environment result = response.getBody();
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setRestTemplate(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Matchers;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.test.EnvironmentTestUtils;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
@@ -27,6 +25,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@@ -54,13 +53,22 @@ public class ConfigServicePropertySourceLocatorTests {
|
||||
@Test
|
||||
public void sunnyDayWithLabel() {
|
||||
Environment body = new Environment("app", "master");
|
||||
mockRequestResponseWithLabel(new ResponseEntity<Environment>(body,
|
||||
HttpStatus.OK), "v1.0.0");
|
||||
mockRequestResponseWithLabel(
|
||||
new ResponseEntity<Environment>(body, HttpStatus.OK), "v1.0.0");
|
||||
locator.setRestTemplate(restTemplate);
|
||||
EnvironmentTestUtils.addEnvironment(environment, "spring.cloud.config.label:v1.0.0");
|
||||
EnvironmentTestUtils.addEnvironment(environment,
|
||||
"spring.cloud.config.label:v1.0.0");
|
||||
assertNotNull(locator.locate(environment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sunnyDayWithNoSuchLabel() {
|
||||
mockRequestResponseWithLabel(new ResponseEntity<Void>((Void) null,
|
||||
HttpStatus.NOT_FOUND), "nosuchlabel");
|
||||
locator.setRestTemplate(restTemplate);
|
||||
assertNull(locator.locate(environment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failsQuietly() {
|
||||
mockRequestResponseWithoutLabel(new ResponseEntity<String>("Wah!",
|
||||
@@ -98,6 +106,35 @@ public class ConfigServicePropertySourceLocatorTests {
|
||||
assertNull(locator.locate(environment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failFastWhenNotFound() throws Exception {
|
||||
ClientHttpRequestFactory requestFactory = Mockito
|
||||
.mock(ClientHttpRequestFactory.class);
|
||||
ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class);
|
||||
ClientHttpResponse response = Mockito.mock(ClientHttpResponse.class);
|
||||
Mockito.when(
|
||||
requestFactory.createRequest(Mockito.any(URI.class),
|
||||
Mockito.any(HttpMethod.class))).thenReturn(request);
|
||||
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
||||
ConfigClientProperties defaults = new ConfigClientProperties(environment);
|
||||
defaults.setFailFast(true);
|
||||
locator = new ConfigServicePropertySourceLocator(defaults);
|
||||
Mockito.when(request.getHeaders()).thenReturn(new HttpHeaders());
|
||||
Mockito.when(request.execute()).thenReturn(response);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
Mockito.when(response.getHeaders()).thenReturn(headers);
|
||||
Mockito.when(response.getStatusCode()).thenReturn(
|
||||
HttpStatus.NOT_FOUND);
|
||||
Mockito.when(response.getBody()).thenReturn(
|
||||
new ByteArrayInputStream("".getBytes()));
|
||||
locator.setRestTemplate(restTemplate);
|
||||
expected.expectCause(IsInstanceOf
|
||||
.<Throwable> instanceOf(HttpClientErrorException.class));
|
||||
expected.expectMessage("fail fast property is set");
|
||||
assertNull(locator.locate(environment));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void mockRequestResponseWithLabel(ResponseEntity<?> response, String label) {
|
||||
Mockito.when(
|
||||
|
||||
Reference in New Issue
Block a user