From ce3a20d2627391d693aaaf24e6b9d6f10d63bf40 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 19 May 2015 12:51:10 +0100 Subject: [PATCH] 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 --- .../main/asciidoc/spring-cloud-config.adoc | 11 ++-- .../ConfigServicePropertySourceLocator.java | 51 +++++++++++++------ ...nfigServicePropertySourceLocatorTests.java | 47 +++++++++++++++-- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 274fc7cf..f7554df4 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -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 diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index 87a42a4d..1d4dcb0a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -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) null), Environment.class, - args).getBody(); - for (PropertySource source : result.getPropertySources()) { - @SuppressWarnings("unchecked") - Map map = (Map) 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 map = (Map) 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 response = restTemplate.exchange(uri + path, + HttpMethod.GET, new HttpEntity((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; } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java index 8f7b56fb..920186e9 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java @@ -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(body, - HttpStatus.OK), "v1.0.0"); + mockRequestResponseWithLabel( + new ResponseEntity(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) null, + HttpStatus.NOT_FOUND), "nosuchlabel"); + locator.setRestTemplate(restTemplate); + assertNull(locator.locate(environment)); + } + @Test public void failsQuietly() { mockRequestResponseWithoutLabel(new ResponseEntity("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 + . 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(