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:
Dave Syer
2015-05-19 12:51:10 +01:00
parent 891e928890
commit ce3a20d262
3 changed files with 85 additions and 24 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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(