Updates to use Bootstrapper to customize ConfigData.

Removes AbstractConfigData* classes.

Creates ConfigServerBootstrapper.java to allow simplified bootstrapping for users.
This commit is contained in:
spencergibb
2020-09-17 18:08:42 -04:00
parent baa1bd4282
commit dae4cafb96
14 changed files with 746 additions and 675 deletions

View File

@@ -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<L extends AbstractConfigDataLocation>
implements ConfigDataLoader<L>, 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<org.springframework.core.env.PropertySource<?>> 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<String, Object> map = translateOrigins(source.getName(),
(Map<String, Object>) source.getSource());
composite.add(0,
new OriginTrackedMapPropertySource("configserver:" + source.getName(), map));
}
}
HashMap<String, Object> 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<PropertySource> 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<String, Object> translateOrigins(String name, Map<String, Object> source) {
Map<String, Object> withOrigins = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
boolean hasOrigin = false;
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> value = (Map<String, Object>) 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<String, Object> 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<Environment> 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<Void> 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);
}
}
}

View File

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

View File

@@ -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<L extends AbstractConfigDataLocation>
implements ConfigDataLocationResolver<L>, 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<String, String> 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<L> resolve(ConfigDataLocationResolverContext context, String location, boolean optional) {
return Collections.emptyList();
}
public List<L> 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<L> locations = new ArrayList<>();
locations.add(createConfigDataLocation(optional, profiles, properties, restTemplate));
return locations;
}
protected abstract L createConfigDataLocation(boolean optional, Profiles profiles,
ConfigClientProperties properties, RestTemplate restTemplate);
}

View File

@@ -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<BootstrapContext, RestTemplate> restTemplateFactory;
private LoaderInterceptor loaderInterceptor;
static ConfigServerBootstrapper create() {
return new ConfigServerBootstrapper();
}
// TODO: document there will be a ConfigClientProperties in BootstrapContext
public ConfigServerBootstrapper withRestTemplateFactory(
Function<BootstrapContext, RestTemplate> 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<LoadContext, ConfigData> {
}
@FunctionalInterface
public interface LoaderInvocation
extends BiFunction<ConfigDataLoaderContext, ConfigServerConfigDataLocation, ConfigData> {
}
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;
}
}
}

View File

@@ -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<ConfigServerConfigDataLocation> {
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<ConfigServerConfigDataLocation>, 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<PropertySource<?>> 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<String, Object> map = translateOrigins(source.getName(),
(Map<String, Object>) source.getSource());
composite.add(0,
new OriginTrackedMapPropertySource("configserver:" + source.getName(), map));
}
}
HashMap<String, Object> 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<org.springframework.cloud.config.environment.PropertySource> 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<String, Object> translateOrigins(String name, Map<String, Object> source) {
Map<String, Object> withOrigins = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
boolean hasOrigin = false;
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> value = (Map<String, Object>) 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<String, Object> 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<Environment> 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<Void> 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);
}
}
}

View File

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

View File

@@ -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<ConfigServerConfigDataLocation>
implements ConfigDataLocationResolver<ConfigServerConfigDataLocation> {
implements ConfigDataLocationResolver<ConfigServerConfigDataLocation>, 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<String, String> 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<ConfigServerConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
return Collections.emptyList();
}
public List<ConfigServerConfigDataLocation> 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<ConfigServerConfigDataLocation> locations = new ArrayList<>();
locations.add(new ConfigServerConfigDataLocation(properties, optional, profiles));
return locations;
}
}

View File

@@ -46,6 +46,7 @@ public class ConfigServiceBootstrapConfiguration {
private ConfigurableEnvironment environment;
@Bean
@ConditionalOnMissingBean
public ConfigClientProperties configClientProperties() {
ConfigClientProperties client = new ConfigClientProperties(this.environment);
return client;

View File

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

View File

@@ -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<TestConfigServerConfigDataLocation> {
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<TestConfigServerConfigDataLocation, ConfigData> 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 {
}
}

View File

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

View File

@@ -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<TestConfigServerConfigDataLocation> {
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);
}
}

View File

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

View File

@@ -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<String, Object> property = (Map<String, Object>) res.get("property");
assertThat(property).containsEntry("value", "bar");
}
}