Updates to use Bootstrapper to customize ConfigData.
Removes AbstractConfigData* classes. Creates ConfigServerBootstrapper.java to allow simplified bootstrapping for users.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class ConfigServiceBootstrapConfiguration {
|
||||
private ConfigurableEnvironment environment;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ConfigClientProperties configClientProperties() {
|
||||
ConfigClientProperties client = new ConfigClientProperties(this.environment);
|
||||
return client;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user