From dae4cafb96e4917aa5458366e407ea90179d3f94 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Thu, 17 Sep 2020 18:08:42 -0400 Subject: [PATCH] Updates to use Bootstrapper to customize ConfigData. Removes AbstractConfigData* classes. Creates ConfigServerBootstrapper.java to allow simplified bootstrapping for users. --- .../client/AbstractConfigDataLoader.java | 282 ------------------ .../client/AbstractConfigDataLocation.java | 85 ------ .../AbstractConfigDataLocationResolver.java | 132 -------- .../client/ConfigServerBootstrapper.java | 114 +++++++ .../client/ConfigServerConfigDataLoader.java | 269 ++++++++++++++++- .../ConfigServerConfigDataLocation.java | 57 +++- ...onfigServerConfigDataLocationResolver.java | 113 ++++++- .../ConfigServiceBootstrapConfiguration.java | 1 + ...nfigDataCustomizationIntegrationTests.java | 118 ++++++++ .../TestConfigServerConfigDataLoader.java | 69 ----- .../TestConfigServerConfigDataLocation.java | 29 -- ...onfigServerConfigDataLocationResolver.java | 57 ---- .../test/resources/META-INF/spring.factories | 8 - .../sample/ConfigDataIntegrationTests.java | 87 ++++++ 14 files changed, 746 insertions(+), 675 deletions(-) delete mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLoader.java delete mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocation.java delete mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocationResolver.java create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java delete mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLoader.java delete mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocation.java delete mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocationResolver.java delete mode 100644 spring-cloud-config-client/src/test/resources/META-INF/spring.factories create mode 100644 spring-cloud-config-sample/src/test/java/sample/ConfigDataIntegrationTests.java diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLoader.java deleted file mode 100644 index 7525ed01..00000000 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLoader.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.logging.Log; - -import org.springframework.boot.context.config.ConfigData; -import org.springframework.boot.context.config.ConfigDataLoader; -import org.springframework.boot.context.config.ConfigDataLoaderContext; -import org.springframework.boot.env.OriginTrackedMapPropertySource; -import org.springframework.boot.origin.Origin; -import org.springframework.boot.origin.OriginTrackedValue; -import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.config.environment.PropertySource; -import org.springframework.core.Ordered; -import org.springframework.core.env.MapPropertySource; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestTemplate; - -import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; -import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; -import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; -import static org.springframework.cloud.config.environment.EnvironmentMediaType.V2_JSON; - -public abstract class AbstractConfigDataLoader - implements ConfigDataLoader, Ordered { - - protected final Log logger; - - public AbstractConfigDataLoader(Log logger) { - this.logger = logger; - } - - @Override - public int getOrder() { - return -1; - } - - @Override - // TODO: retry - public ConfigData load(ConfigDataLoaderContext context, L location) throws IOException { - ConfigClientProperties properties = location.getProperties(); - // ConfigClientProperties properties = - // this.defaultProperties.override(environment); - List> composite = new ArrayList<>(); - Exception error = null; - String errorBody = null; - try { - String[] labels = new String[] { "" }; - if (StringUtils.hasText(properties.getLabel())) { - labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel()); - } - String state = ConfigClientStateHolder.getState(); - // Try all the labels until one works - for (String label : labels) { - Environment result = getRemoteEnvironment(location, label.trim(), state); - if (result != null) { - log(result); - - // result.getPropertySources() can be null if using xml - if (result.getPropertySources() != null) { - for (PropertySource source : result.getPropertySources()) { - @SuppressWarnings("unchecked") - Map map = translateOrigins(source.getName(), - (Map) source.getSource()); - composite.add(0, - new OriginTrackedMapPropertySource("configserver:" + source.getName(), map)); - } - } - - HashMap map = new HashMap<>(); - if (StringUtils.hasText(result.getState())) { - putValue(map, "config.client.state", result.getState()); - } - if (StringUtils.hasText(result.getVersion())) { - putValue(map, "config.client.version", result.getVersion()); - } - // the existence of this property source confirms a successful - // response from config server - composite.add(0, new MapPropertySource("configClient", map)); - return new ConfigData(composite); - } - } - errorBody = String.format("None of labels %s found", Arrays.toString(labels)); - } - catch (HttpServerErrorException e) { - error = e; - if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders().getContentType())) { - errorBody = e.getResponseBodyAsString(); - } - } - catch (Exception e) { - error = e; - } - if (properties.isFailFast() || !location.isOptional()) { - String reason; - if (properties.isFailFast()) { - reason = "the fail fast property is set"; - } - else { - reason = "the location is not optional"; - } - throw new IllegalStateException("Could not locate PropertySource and " + reason + ", failing" - + (errorBody == null ? "" : ": " + errorBody), error); - } - logger.warn("Could not locate PropertySource: " + (error != null ? error.getMessage() : errorBody)); - return null; - - } - - protected void log(Environment result) { - if (logger.isInfoEnabled()) { - logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", - result.getName(), result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()), - result.getLabel(), result.getVersion(), result.getState())); - } - if (logger.isDebugEnabled()) { - List propertySourceList = result.getPropertySources(); - if (propertySourceList != null) { - int propertyCount = 0; - for (PropertySource propertySource : propertySourceList) { - propertyCount += propertySource.getSource().size(); - } - logger.debug(String.format("Environment %s has %d property sources with %d properties.", - result.getName(), result.getPropertySources().size(), propertyCount)); - } - - } - } - - protected Map translateOrigins(String name, Map source) { - Map withOrigins = new LinkedHashMap<>(); - for (Map.Entry entry : source.entrySet()) { - boolean hasOrigin = false; - - if (entry.getValue() instanceof Map) { - @SuppressWarnings("unchecked") - Map value = (Map) entry.getValue(); - if (value.size() == 2 && value.containsKey("origin") && value.containsKey("value")) { - Origin origin = new ConfigServicePropertySourceLocator.ConfigServiceOrigin(name, - value.get("origin")); - OriginTrackedValue trackedValue = OriginTrackedValue.of(value.get("value"), origin); - withOrigins.put(entry.getKey(), trackedValue); - hasOrigin = true; - } - } - - if (!hasOrigin) { - withOrigins.put(entry.getKey(), entry.getValue()); - } - } - return withOrigins; - } - - protected void putValue(HashMap map, String key, String value) { - if (StringUtils.hasText(value)) { - map.put(key, value); - } - } - - protected Environment getRemoteEnvironment(L location, String label, String state) { - ConfigClientProperties properties = location.getProperties(); - RestTemplate restTemplate = location.getRestTemplate(); - - String path = "/{name}/{profile}"; - String name = properties.getName(); - String profile = StringUtils.collectionToCommaDelimitedString(location.getProfiles().getAccepted()); - String token = properties.getToken(); - int noOfUrls = properties.getUri().length; - if (noOfUrls > 1) { - logger.info("Multiple Config Server Urls found listed."); - } - - Object[] args = new String[] { name, profile }; - if (StringUtils.hasText(label)) { - // workaround for Spring MVC matching / in paths - label = Environment.denormalize(label); - args = new String[] { name, profile, label }; - path = path + "/{label}"; - } - ResponseEntity response = null; - - for (int i = 0; i < noOfUrls; i++) { - ConfigClientProperties.Credentials credentials = properties.getCredentials(i); - String uri = credentials.getUri(); - String username = credentials.getUsername(); - String password = credentials.getPassword(); - - logger.info("Fetching config from server at : " + uri); - - try { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.parseMediaType(V2_JSON))); - addAuthorizationToken(properties, headers, username, password); - if (StringUtils.hasText(token)) { - headers.add(TOKEN_HEADER, token); - } - if (StringUtils.hasText(state) && properties.isSendState()) { - headers.add(STATE_HEADER, state); - } - - final HttpEntity entity = new HttpEntity<>((Void) null, headers); - response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args); - } - catch (HttpClientErrorException e) { - if (e.getStatusCode() != HttpStatus.NOT_FOUND) { - throw e; - } - } - catch (ResourceAccessException e) { - logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available"); - if (i == noOfUrls - 1) { - throw e; - } - else { - continue; - } - } - - if (response == null || response.getStatusCode() != HttpStatus.OK) { - return null; - } - - Environment result = response.getBody(); - return result; - } - - return null; - } - - protected void addAuthorizationToken(ConfigClientProperties configClientProperties, HttpHeaders httpHeaders, - String username, String password) { - String authorization = configClientProperties.getHeaders().get(AUTHORIZATION); - - if (password != null && authorization != null) { - throw new IllegalStateException("You must set either 'password' or 'authorization'"); - } - - if (password != null) { - byte[] token = Base64Utils.encode((username + ":" + password).getBytes()); - httpHeaders.add("Authorization", "Basic " + new String(token)); - } - else if (authorization != null) { - httpHeaders.add("Authorization", authorization); - } - - } - -} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocation.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocation.java deleted file mode 100644 index e5c85a7a..00000000 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocation.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import java.util.Objects; - -import org.springframework.boot.context.config.ConfigDataLocation; -import org.springframework.boot.context.config.Profiles; -import org.springframework.core.style.ToStringCreator; -import org.springframework.web.client.RestTemplate; - -public abstract class AbstractConfigDataLocation extends ConfigDataLocation { - - private final RestTemplate restTemplate; - - private final ConfigClientProperties properties; - - private final boolean optional; - - private final Profiles profiles; - - public AbstractConfigDataLocation(RestTemplate restTemplate, ConfigClientProperties properties, boolean optional, - Profiles profiles) { - this.restTemplate = restTemplate; - this.properties = properties; - this.optional = optional; - this.profiles = profiles; - } - - public RestTemplate getRestTemplate() { - return this.restTemplate; - } - - public ConfigClientProperties getProperties() { - return this.properties; - } - - public boolean isOptional() { - return this.optional; - } - - public Profiles getProfiles() { - return this.profiles; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AbstractConfigDataLocation that = (AbstractConfigDataLocation) o; - return Objects.equals(this.restTemplate, that.restTemplate) && Objects.equals(this.properties, that.properties) - && Objects.equals(this.optional, that.optional) && Objects.equals(this.profiles, that.profiles); - } - - @Override - public int hashCode() { - return Objects.hash(this.restTemplate, this.properties, this.optional, this.profiles); - } - - @Override - public String toString() { - return new ToStringCreator(this).append("uris", properties.getUri()).append("optional", optional) - .append("profiles", profiles.getAccepted()).toString(); - - } - -} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocationResolver.java deleted file mode 100644 index ef80b983..00000000 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/AbstractConfigDataLocationResolver.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.logging.Log; - -import org.springframework.boot.context.config.ConfigDataLocationResolver; -import org.springframework.boot.context.config.ConfigDataLocationResolverContext; -import org.springframework.boot.context.config.Profiles; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.core.Ordered; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; - -import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; - -public abstract class AbstractConfigDataLocationResolver - implements ConfigDataLocationResolver, Ordered { - - /** - * Prefix for Config Server imports. - */ - public static final String PREFIX = "configserver:"; - - protected final Log log; - - public AbstractConfigDataLocationResolver(Log log) { - this.log = log; - } - - @Override - public int getOrder() { - return -1; - } - - protected ConfigClientProperties loadProperties(Binder binder) { - ConfigClientProperties configClientProperties = binder - .bind(ConfigClientProperties.PREFIX, Bindable.of(ConfigClientProperties.class)) - .orElse(new ConfigClientProperties()); - String applicationName = binder.bind("spring.application.name", String.class).orElse("application"); - configClientProperties.setName(applicationName); - return configClientProperties; - } - - protected RestTemplate createRestTemplate(ConfigClientProperties properties) { - SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); - if (properties.getRequestReadTimeout() < 0) { - throw new IllegalStateException("Invalid Value for Read Timeout set."); - } - if (properties.getRequestConnectTimeout() < 0) { - throw new IllegalStateException("Invalid Value for Connect Timeout set."); - } - requestFactory.setReadTimeout(properties.getRequestReadTimeout()); - requestFactory.setConnectTimeout(properties.getRequestConnectTimeout()); - RestTemplate template = new RestTemplate(requestFactory); - Map headers = new HashMap<>(properties.getHeaders()); - if (headers.containsKey(AUTHORIZATION)) { - headers.remove(AUTHORIZATION); // To avoid redundant addition of header - } - if (!headers.isEmpty()) { - template.setInterceptors(Collections - .singletonList(new ConfigServicePropertySourceLocator.GenericRequestHeaderInterceptor(headers))); - } - - return template; - } - - protected Log getLog() { - return this.log; - } - - public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { - if (!location.startsWith(getPrefix())) { - return false; - } - return context.getBinder().bind(ConfigClientProperties.PREFIX + ".enabled", Boolean.class).orElse(true); - } - - protected String getPrefix() { - return PREFIX; - } - - public List resolve(ConfigDataLocationResolverContext context, String location, boolean optional) { - return Collections.emptyList(); - } - - public List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional, - Profiles profiles) { - - ConfigClientProperties properties = loadProperties(context.getBinder()); - - String uris = (location.startsWith(getPrefix())) ? location.substring(getPrefix().length()) : location; - - if (StringUtils.hasText(uris)) { - String[] uri = StringUtils.commaDelimitedListToStringArray(uris); - properties.setUri(uri); - } - - RestTemplate restTemplate = createRestTemplate(properties); - - List locations = new ArrayList<>(); - locations.add(createConfigDataLocation(optional, profiles, properties, restTemplate)); - - return locations; - } - - protected abstract L createConfigDataLocation(boolean optional, Profiles profiles, - ConfigClientProperties properties, RestTemplate restTemplate); - -} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java new file mode 100644 index 00000000..3f917923 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2020 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.config.client; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.BootstrapRegistry.InstanceSupplier; +import org.springframework.boot.Bootstrapper; +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.util.Assert; +import org.springframework.web.client.RestTemplate; + +public class ConfigServerBootstrapper implements Bootstrapper { + + private Function restTemplateFactory; + + private LoaderInterceptor loaderInterceptor; + + static ConfigServerBootstrapper create() { + return new ConfigServerBootstrapper(); + } + + // TODO: document there will be a ConfigClientProperties in BootstrapContext + public ConfigServerBootstrapper withRestTemplateFactory( + Function restTemplateFactory) { + this.restTemplateFactory = restTemplateFactory; + return this; + } + + public ConfigServerBootstrapper withLoaderInterceptor(LoaderInterceptor loaderInterceptor) { + this.loaderInterceptor = loaderInterceptor; + return this; + } + + @Override + public void intitialize(BootstrapRegistry registry) { + if (restTemplateFactory != null) { + registry.register(RestTemplate.class, restTemplateFactory::apply); + } + if (loaderInterceptor != null) { + registry.register(LoaderInterceptor.class, InstanceSupplier.of(loaderInterceptor)); + } + } + + public interface LoaderInterceptor extends Function { + + } + + @FunctionalInterface + public interface LoaderInvocation + extends BiFunction { + + } + + public static class LoadContext { + + private final ConfigDataLoaderContext loaderContext; + + private final ConfigServerConfigDataLocation location; + + private final Binder binder; + + private final LoaderInvocation invocation; + + LoadContext(ConfigDataLoaderContext loaderContext, ConfigServerConfigDataLocation location, Binder binder, + LoaderInvocation invocation) { + Assert.notNull(loaderContext, "loaderContext may not be null"); + Assert.notNull(location, "location may not be null"); + Assert.notNull(binder, "binder may not be null"); + Assert.notNull(invocation, "invocation may not be null"); + this.loaderContext = loaderContext; + this.location = location; + this.binder = binder; + this.invocation = invocation; + } + + public ConfigDataLoaderContext getLoaderContext() { + return this.loaderContext; + } + + public ConfigServerConfigDataLocation getLocation() { + return this.location; + } + + public Binder getBinder() { + return this.binder; + } + + public LoaderInvocation getInvocation() { + return this.invocation; + } + + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index 143b8b32..c520edaa 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -16,12 +16,277 @@ package org.springframework.cloud.config.client; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import org.apache.commons.logging.Log; -public class ConfigServerConfigDataLoader extends AbstractConfigDataLoader { +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataLoader; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.env.OriginTrackedMapPropertySource; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoadContext; +import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoaderInterceptor; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.core.Ordered; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; +import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; +import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; +import static org.springframework.cloud.config.environment.EnvironmentMediaType.V2_JSON; + +public class ConfigServerConfigDataLoader implements ConfigDataLoader, Ordered { + + protected final Log logger; public ConfigServerConfigDataLoader(Log logger) { - super(logger); + this.logger = logger; + } + + @Override + public int getOrder() { + return -1; + } + + @Override + // TODO: implement retry LoaderInterceptor + public ConfigData load(ConfigDataLoaderContext context, ConfigServerConfigDataLocation location) { + if (context.getBootstrapContext().isRegistered(LoaderInterceptor.class)) { + LoaderInterceptor interceptor = context.getBootstrapContext().get(LoaderInterceptor.class); + Binder binder = context.getBootstrapContext().get(Binder.class); + return interceptor.apply(new LoadContext(context, location, binder, this::doLoad)); + } + return doLoad(context, location); + } + + public ConfigData doLoad(ConfigDataLoaderContext context, ConfigServerConfigDataLocation location) { + ConfigClientProperties properties = location.getProperties(); + List> composite = new ArrayList<>(); + Exception error = null; + String errorBody = null; + try { + String[] labels = new String[] { "" }; + if (StringUtils.hasText(properties.getLabel())) { + labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel()); + } + String state = ConfigClientStateHolder.getState(); + // Try all the labels until one works + for (String label : labels) { + Environment result = getRemoteEnvironment(context, location, label.trim(), state); + if (result != null) { + log(result); + + // result.getPropertySources() can be null if using xml + if (result.getPropertySources() != null) { + for (org.springframework.cloud.config.environment.PropertySource source : result + .getPropertySources()) { + @SuppressWarnings("unchecked") + Map map = translateOrigins(source.getName(), + (Map) source.getSource()); + composite.add(0, + new OriginTrackedMapPropertySource("configserver:" + source.getName(), map)); + } + } + + HashMap map = new HashMap<>(); + if (StringUtils.hasText(result.getState())) { + putValue(map, "config.client.state", result.getState()); + } + if (StringUtils.hasText(result.getVersion())) { + putValue(map, "config.client.version", result.getVersion()); + } + // the existence of this property source confirms a successful + // response from config server + composite.add(0, new MapPropertySource("configClient", map)); + return new ConfigData(composite); + } + } + errorBody = String.format("None of labels %s found", Arrays.toString(labels)); + } + catch (HttpServerErrorException e) { + error = e; + if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders().getContentType())) { + errorBody = e.getResponseBodyAsString(); + } + } + catch (Exception e) { + error = e; + } + if (properties.isFailFast() || !location.isOptional()) { + String reason; + if (properties.isFailFast()) { + reason = "the fail fast property is set"; + } + else { + reason = "the location is not optional"; + } + throw new IllegalStateException("Could not locate PropertySource and " + reason + ", failing" + + (errorBody == null ? "" : ": " + errorBody), error); + } + logger.warn("Could not locate PropertySource: " + (error != null ? error.getMessage() : errorBody)); + return null; + } + + protected void log(Environment result) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", + result.getName(), result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()), + result.getLabel(), result.getVersion(), result.getState())); + } + if (logger.isDebugEnabled()) { + List propertySourceList = result + .getPropertySources(); + if (propertySourceList != null) { + int propertyCount = 0; + for (org.springframework.cloud.config.environment.PropertySource propertySource : propertySourceList) { + propertyCount += propertySource.getSource().size(); + } + logger.debug(String.format("Environment %s has %d property sources with %d properties.", + result.getName(), result.getPropertySources().size(), propertyCount)); + } + + } + } + + protected Map translateOrigins(String name, Map source) { + Map withOrigins = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + boolean hasOrigin = false; + + if (entry.getValue() instanceof Map) { + @SuppressWarnings("unchecked") + Map value = (Map) entry.getValue(); + if (value.size() == 2 && value.containsKey("origin") && value.containsKey("value")) { + Origin origin = new ConfigServicePropertySourceLocator.ConfigServiceOrigin(name, + value.get("origin")); + OriginTrackedValue trackedValue = OriginTrackedValue.of(value.get("value"), origin); + withOrigins.put(entry.getKey(), trackedValue); + hasOrigin = true; + } + } + + if (!hasOrigin) { + withOrigins.put(entry.getKey(), entry.getValue()); + } + } + return withOrigins; + } + + protected void putValue(HashMap map, String key, String value) { + if (StringUtils.hasText(value)) { + map.put(key, value); + } + } + + protected Environment getRemoteEnvironment(ConfigDataLoaderContext context, ConfigServerConfigDataLocation location, + String label, String state) { + ConfigClientProperties properties = location.getProperties(); + RestTemplate restTemplate = context.getBootstrapContext().get(RestTemplate.class); + + String path = "/{name}/{profile}"; + String name = properties.getName(); + String profile = StringUtils.collectionToCommaDelimitedString(location.getProfiles().getAccepted()); + String token = properties.getToken(); + int noOfUrls = properties.getUri().length; + if (noOfUrls > 1) { + logger.info("Multiple Config Server Urls found listed."); + } + + Object[] args = new String[] { name, profile }; + if (StringUtils.hasText(label)) { + // workaround for Spring MVC matching / in paths + label = Environment.denormalize(label); + args = new String[] { name, profile, label }; + path = path + "/{label}"; + } + ResponseEntity response = null; + + for (int i = 0; i < noOfUrls; i++) { + ConfigClientProperties.Credentials credentials = properties.getCredentials(i); + String uri = credentials.getUri(); + String username = credentials.getUsername(); + String password = credentials.getPassword(); + + logger.info("Fetching config from server at : " + uri); + + try { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.parseMediaType(V2_JSON))); + addAuthorizationToken(properties, headers, username, password); + if (StringUtils.hasText(token)) { + headers.add(TOKEN_HEADER, token); + } + if (StringUtils.hasText(state) && properties.isSendState()) { + headers.add(STATE_HEADER, state); + } + + final HttpEntity entity = new HttpEntity<>((Void) null, headers); + response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args); + } + catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { + throw e; + } + } + catch (ResourceAccessException e) { + logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available"); + if (i == noOfUrls - 1) { + throw e; + } + else { + continue; + } + } + + if (response == null || response.getStatusCode() != HttpStatus.OK) { + return null; + } + + Environment result = response.getBody(); + return result; + } + + return null; + } + + protected void addAuthorizationToken(ConfigClientProperties configClientProperties, HttpHeaders httpHeaders, + String username, String password) { + String authorization = configClientProperties.getHeaders().get(AUTHORIZATION); + + if (password != null && authorization != null) { + throw new IllegalStateException("You must set either 'password' or 'authorization'"); + } + + if (password != null) { + byte[] token = Base64Utils.encode((username + ":" + password).getBytes()); + httpHeaders.add("Authorization", "Basic " + new String(token)); + } + else if (authorization != null) { + httpHeaders.add("Authorization", authorization); + } + } } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocation.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocation.java index b1dad009..4abfac8b 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocation.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocation.java @@ -16,14 +16,61 @@ package org.springframework.cloud.config.client; +import java.util.Objects; + +import org.springframework.boot.context.config.ConfigDataLocation; import org.springframework.boot.context.config.Profiles; -import org.springframework.web.client.RestTemplate; +import org.springframework.core.style.ToStringCreator; -public class ConfigServerConfigDataLocation extends AbstractConfigDataLocation { +public class ConfigServerConfigDataLocation extends ConfigDataLocation { + + private final ConfigClientProperties properties; + + private final boolean optional; + + private final Profiles profiles; + + public ConfigServerConfigDataLocation(ConfigClientProperties properties, boolean optional, Profiles profiles) { + this.properties = properties; + this.optional = optional; + this.profiles = profiles; + } + + public ConfigClientProperties getProperties() { + return this.properties; + } + + public boolean isOptional() { + return this.optional; + } + + public Profiles getProfiles() { + return this.profiles; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigServerConfigDataLocation that = (ConfigServerConfigDataLocation) o; + return Objects.equals(this.properties, that.properties) && Objects.equals(this.optional, that.optional) + && Objects.equals(this.profiles, that.profiles); + } + + @Override + public int hashCode() { + return Objects.hash(this.properties, this.optional, this.profiles); + } + + @Override + public String toString() { + return new ToStringCreator(this).append("uris", properties.getUri()).append("optional", optional) + .append("profiles", profiles.getAccepted()).toString(); - public ConfigServerConfigDataLocation(RestTemplate restTemplate, ConfigClientProperties properties, - boolean optional, Profiles profiles) { - super(restTemplate, properties, optional, profiles); } } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 7daf53e4..93700ad1 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -16,24 +16,125 @@ package org.springframework.cloud.config.client; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.commons.logging.Log; +import org.springframework.boot.BootstrapRegistry.InstanceSupplier; +import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.Ordered; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; +import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; + public class ConfigServerConfigDataLocationResolver - extends AbstractConfigDataLocationResolver - implements ConfigDataLocationResolver { + implements ConfigDataLocationResolver, Ordered { + + /** + * Prefix for Config Server imports. + */ + public static final String PREFIX = "configserver:"; + + private final Log log; public ConfigServerConfigDataLocationResolver(Log log) { - super(log); + this.log = log; } @Override - protected ConfigServerConfigDataLocation createConfigDataLocation(boolean optional, Profiles profiles, - ConfigClientProperties properties, RestTemplate restTemplate) { - return new ConfigServerConfigDataLocation(restTemplate, properties, optional, profiles); + public int getOrder() { + return -1; + } + + protected ConfigClientProperties loadProperties(Binder binder) { + ConfigClientProperties configClientProperties = binder + .bind(ConfigClientProperties.PREFIX, Bindable.of(ConfigClientProperties.class)) + .orElse(new ConfigClientProperties()); + String applicationName = binder.bind("spring.application.name", String.class).orElse("application"); + configClientProperties.setName(applicationName); + return configClientProperties; + } + + protected RestTemplate createRestTemplate(ConfigClientProperties properties) { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + if (properties.getRequestReadTimeout() < 0) { + throw new IllegalStateException("Invalid Value for Read Timeout set."); + } + if (properties.getRequestConnectTimeout() < 0) { + throw new IllegalStateException("Invalid Value for Connect Timeout set."); + } + requestFactory.setReadTimeout(properties.getRequestReadTimeout()); + requestFactory.setConnectTimeout(properties.getRequestConnectTimeout()); + RestTemplate template = new RestTemplate(requestFactory); + Map headers = new HashMap<>(properties.getHeaders()); + if (headers.containsKey(AUTHORIZATION)) { + headers.remove(AUTHORIZATION); // To avoid redundant addition of header + } + if (!headers.isEmpty()) { + template.setInterceptors(Collections + .singletonList(new ConfigServicePropertySourceLocator.GenericRequestHeaderInterceptor(headers))); + } + + return template; + } + + protected Log getLog() { + return this.log; + } + + public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { + if (!location.startsWith(getPrefix())) { + return false; + } + return context.getBinder().bind(ConfigClientProperties.PREFIX + ".enabled", Boolean.class).orElse(true); + } + + protected String getPrefix() { + return PREFIX; + } + + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) { + return Collections.emptyList(); + } + + public List resolveProfileSpecific(ConfigDataLocationResolverContext context, + String location, boolean optional, Profiles profiles) { + + ConfigClientProperties properties = loadProperties(context.getBinder()); + + String uris = (location.startsWith(getPrefix())) ? location.substring(getPrefix().length()) : location; + + if (StringUtils.hasText(uris)) { + String[] uri = StringUtils.commaDelimitedListToStringArray(uris); + properties.setUri(uri); + } + + ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext(); + bootstrapContext.registerIfAbsent(ConfigClientProperties.class, InstanceSupplier.of(properties)); + bootstrapContext.addCloseListener(event -> event.getApplicationContext().getBeanFactory().registerSingleton( + "configDataConfigClientProperties", event.getBootstrapContext().get(ConfigClientProperties.class))); + + bootstrapContext.registerIfAbsent(RestTemplate.class, InstanceSupplier.from(() -> { + ConfigClientProperties props = bootstrapContext.get(ConfigClientProperties.class); + return createRestTemplate(props); + })); + + List locations = new ArrayList<>(); + locations.add(new ConfigServerConfigDataLocation(properties, optional, profiles)); + + return locations; } } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java index 190243c8..d6100fbd 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java @@ -46,6 +46,7 @@ public class ConfigServiceBootstrapConfiguration { private ConfigurableEnvironment environment; @Bean + @ConditionalOnMissingBean public ConfigClientProperties configClientProperties() { ConfigClientProperties client = new ConfigClientProperties(this.environment); return client; diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java new file mode 100644 index 00000000..cbaff955 --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2020 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.config.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoaderInterceptor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConfigServerConfigDataCustomizationIntegrationTests { + + @Test + void customizableRestTemplate() { + ConfigurableApplicationContext context = null; + try { + context = new SpringApplicationBuilder(TestConfig.class) + .addBootstrapper(ConfigServerBootstrapper.create().withLoaderInterceptor(new Interceptor()) + .withRestTemplateFactory(this::restTemplate)) + .addBootstrapper(registry -> registry.addCloseListener(event -> { + BootstrapContext bootstrapContext = event.getBootstrapContext(); + ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory(); + + RestTemplate restTemplate = bootstrapContext.get(RestTemplate.class); + beanFactory.registerSingleton("holder", new RestTemplateHolder(restTemplate)); + beanFactory.registerSingleton("interceptor", bootstrapContext.get(LoaderInterceptor.class)); + })).run("--spring.config.import=optional:configserver:", "--custom.prop=customval"); + + RestTemplateHolder holder = context.getBean(RestTemplateHolder.class); + assertThat(holder).isNotNull(); + assertThat(holder.restTemplate).isInstanceOf(CustomRestTemplate.class); + CustomRestTemplate custom = (CustomRestTemplate) holder.restTemplate; + assertThat(custom.customProp).isEqualTo("customval"); + + LoaderInterceptor loaderInterceptor = context.getBean(LoaderInterceptor.class); + assertThat(loaderInterceptor).isNotNull().isInstanceOf(Interceptor.class); + Interceptor interceptor = (Interceptor) loaderInterceptor; + assertThat(interceptor.applied).isTrue(); + assertThat(interceptor.hasBinder).isTrue(); + } + finally { + if (context != null) { + context.close(); + } + } + } + + CustomRestTemplate restTemplate(BootstrapContext context) { + ConfigClientProperties properties = context.get(ConfigClientProperties.class); + String custom = context.get(Binder.class).bind("custom.prop", String.class).orElse("default-custom-prop"); + return new CustomRestTemplate(custom); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + + static class Interceptor implements LoaderInterceptor { + + boolean applied; + + boolean hasBinder; + + @Override + public ConfigData apply(ConfigServerBootstrapper.LoadContext context) { + applied = true; + hasBinder = context.getBinder() != null; + return context.getInvocation().apply(context.getLoaderContext(), context.getLocation()); + } + + } + + static class RestTemplateHolder { + + final RestTemplate restTemplate; + + RestTemplateHolder(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + } + + static class CustomRestTemplate extends RestTemplate { + + private final String customProp; + + CustomRestTemplate(String customProp) { + this.customProp = customProp; + } + + } + +} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLoader.java deleted file mode 100644 index af5e4974..00000000 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLoader.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import java.io.IOException; -import java.util.function.Function; - -import org.apache.commons.logging.Log; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.config.ConfigData; -import org.springframework.boot.context.config.ConfigDataLoaderContext; -import org.springframework.util.ReflectionUtils; - -public class TestConfigServerConfigDataLoader extends AbstractConfigDataLoader { - - public TestConfigServerConfigDataLoader(Log logger) { - super(logger); - } - - @Override - public int getOrder() { - return super.getOrder() - 1; - } - - @Override - public ConfigData load(ConfigDataLoaderContext context, TestConfigServerConfigDataLocation location) - throws IOException { - // This could be a RetryTemplate - Function fn = dataLocation -> { - try { - return super.load(context, dataLocation); - } - catch (IOException e) { - ReflectionUtils.rethrowRuntimeException(e); - } - return null; // will never happen - }; - return fn.apply(location); - } - - public static void main(String[] args) { - new SpringApplicationBuilder(TestConfig.class).properties("spring.config.testconfigdata.enabled=true", - "spring.application.name=foo", "spring.config.import=configserver:").run(args); - } - - @SpringBootConfiguration - @EnableAutoConfiguration - protected static class TestConfig { - - } - -} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocation.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocation.java deleted file mode 100644 index a2a92971..00000000 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocation.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import org.springframework.boot.context.config.Profiles; -import org.springframework.web.client.RestTemplate; - -public class TestConfigServerConfigDataLocation extends AbstractConfigDataLocation { - - public TestConfigServerConfigDataLocation(RestTemplate restTemplate, ConfigClientProperties properties, - boolean optional, Profiles profiles) { - super(restTemplate, properties, optional, profiles); - } - -} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocationResolver.java deleted file mode 100644 index 8250b6a4..00000000 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/TestConfigServerConfigDataLocationResolver.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2013-2020 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.config.client; - -import org.apache.commons.logging.Log; - -import org.springframework.boot.context.config.ConfigDataLocationResolverContext; -import org.springframework.boot.context.config.Profiles; -import org.springframework.web.client.RestTemplate; - -public class TestConfigServerConfigDataLocationResolver - extends AbstractConfigDataLocationResolver { - - public TestConfigServerConfigDataLocationResolver(Log log) { - super(log); - } - - @Override - public int getOrder() { - return super.getOrder() - 1; - } - - protected RestTemplate createRestTemplate(ConfigClientProperties properties) { - // do something custom here - return super.createRestTemplate(properties); - } - - @Override - public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { - if (!location.startsWith(getPrefix())) { - return false; - } - Boolean enabled = context.getBinder().bind("spring.config.testconfigdata.enabled", Boolean.class).orElse(false); - return enabled; - } - - @Override - protected TestConfigServerConfigDataLocation createConfigDataLocation(boolean optional, Profiles profiles, - ConfigClientProperties properties, RestTemplate restTemplate) { - return new TestConfigServerConfigDataLocation(restTemplate, properties, optional, profiles); - } - -} diff --git a/spring-cloud-config-client/src/test/resources/META-INF/spring.factories b/spring-cloud-config-client/src/test/resources/META-INF/spring.factories deleted file mode 100644 index 6a6f9ef3..00000000 --- a/spring-cloud-config-client/src/test/resources/META-INF/spring.factories +++ /dev/null @@ -1,8 +0,0 @@ - -# ConfigData Location Resolvers -org.springframework.boot.context.config.ConfigDataLocationResolver=\ -org.springframework.cloud.config.client.TestConfigServerConfigDataLocationResolver - -# ConfigData Loaders -org.springframework.boot.context.config.ConfigDataLoader=\ -org.springframework.cloud.config.client.TestConfigServerConfigDataLoader diff --git a/spring-cloud-config-sample/src/test/java/sample/ConfigDataIntegrationTests.java b/spring-cloud-config-sample/src/test/java/sample/ConfigDataIntegrationTests.java new file mode 100644 index 00000000..5e1d4963 --- /dev/null +++ b/spring-cloud-config-sample/src/test/java/sample/ConfigDataIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018-2019 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 sample; + +import java.io.IOException; +import java.util.Map; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.config.server.test.ConfigServerTestUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class, + // Normally spring.cloud.config.enabled:true is the default but since we have the + // config server on the classpath we need to set it explicitly + properties = { "spring.cloud.config.enabled=true", "spring.config.import=configserver:", + "management.security.enabled=false", "management.endpoints.web.exposure.include=*" }, + webEnvironment = RANDOM_PORT) +public class ConfigDataIntegrationTests { + + private static final String BASE_PATH = new WebEndpointProperties().getBasePath(); + + private static int configPort = SocketUtils.findAvailableTcpPort(); + + private static ConfigurableApplicationContext server; + + @LocalServerPort + private int port; + + @BeforeClass + public static void startConfigServer() throws IOException { + String baseDir = ConfigServerTestUtils.getBaseDirectory("spring-cloud-config-sample"); + String repo = ConfigServerTestUtils.prepareLocalRepo(baseDir, "target/repos", "config-repo", "target/config"); + server = SpringApplication.run(org.springframework.cloud.config.server.ConfigServerApplication.class, + "--server.port=" + configPort, "--spring.config.name=server", + "--spring.cloud.config.server.git.uri=" + repo); + + System.setProperty("spring.cloud.config.uri", "http://localhost:" + configPort); + } + + @AfterClass + public static void close() { + System.clearProperty("spring.cloud.config.uri"); + if (server != null) { + server.close(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void contextLoads() { + Map res = new TestRestTemplate().getForObject("http://localhost:" + this.port + BASE_PATH + "/env/info.foo", + Map.class); + assertThat(res).containsKey("propertySources"); + Map property = (Map) res.get("property"); + assertThat(property).containsEntry("value", "bar"); + } + +}