ConfigServicePropertySourceLocator.java
/*
* Copyright 2013-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 org.springframework.cloud.config.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.bootstrap.support.OriginTrackedCompositePropertySource;
import org.springframework.cloud.config.client.ConfigClientProperties.Credentials;
import org.springframework.cloud.config.client.ConfigClientProperties.MultipleUriStrategy;
import org.springframework.cloud.config.client.validation.InvalidApplicationNameException;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
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.retry.annotation.Retryable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
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.NAME_PLACEHOLDER;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
/**
* @author Dave Syer
* @author Mathieu Ouellet
* @author Marnee DeRider
*/
@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
private static Log logger = LogFactory.getLog(ConfigServicePropertySourceLocator.class);
private RestTemplate restTemplate;
private ConfigClientProperties defaultProperties;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
/**
* Combine properties from the config client properties and the active profiles from
* the environment.
* @param properties config client properties,
* @param environment application environment.
* @return A list of combined profiles.
*/
private List<String> combineProfiles(ConfigClientProperties properties,
org.springframework.core.env.Environment environment) {
List<String> combinedProfiles = new ArrayList<>();
if (!ObjectUtils.isEmpty(properties.getProfile())) {
combinedProfiles = Stream.of(properties.getProfile().split(",")).map(String::trim).filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
if (environment.getActiveProfiles().length > 0) {
List<String> finalCombinedProfiles = combinedProfiles;
List<String> filteredActiveProfiles = Stream.of(environment.getActiveProfiles())
.filter(s -> !finalCombinedProfiles.contains(s)).collect(Collectors.toList());
combinedProfiles.addAll(filteredActiveProfiles);
}
else if (environment.getDefaultProfiles().length > 0 && combinedProfiles.isEmpty()) {
combinedProfiles = Arrays.asList(environment.getDefaultProfiles());
}
return combinedProfiles;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
properties.setProfile(String.join(",", combineProfiles(properties, environment)));
if (StringUtils.startsWithIgnoreCase(properties.getName(), "application-")) {
InvalidApplicationNameException exception = new InvalidApplicationNameException(properties.getName());
if (properties.isFailFast()) {
throw exception;
}
else {
logger.warn(NAME_PLACEHOLDER + " resolved to " + properties.getName()
+ ", not going to load remote properties. Ensure application name doesn't start with 'application-'");
return null;
}
}
CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService");
ConfigClientRequestTemplateFactory requestTemplateFactory = new ConfigClientRequestTemplateFactory(logger,
properties);
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(requestTemplateFactory, 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.addPropertySource(new OriginTrackedMapPropertySource(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.addFirstPropertySource(new MapPropertySource("configClient", map));
return 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()) {
throw new IllegalStateException("Could not locate PropertySource and the fail fast property is set, failing"
+ (errorBody == null ? "" : ": " + errorBody), error);
}
logger.warn("Could not locate PropertySource: " + (error != null ? error.getMessage() : errorBody));
return null;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public Collection<org.springframework.core.env.PropertySource<?>> locateCollection(
org.springframework.core.env.Environment environment) {
return PropertySourceLocator.locateCollection(this, environment);
}
private 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));
}
}
}
private 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 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;
}
private void putValue(HashMap<String, Object> map, String key, String value) {
if (StringUtils.hasText(value)) {
map.put(key, value);
}
}
private Environment getRemoteEnvironment(ConfigClientRequestTemplateFactory requestTemplateFactory, String label,
String state) {
RestTemplate restTemplate = this.restTemplate == null ? requestTemplateFactory.create() : this.restTemplate;
ConfigClientProperties properties = requestTemplateFactory.getProperties();
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
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;
List<MediaType> acceptHeader = Collections.singletonList(MediaType.parseMediaType(properties.getMediaType()));
for (int i = 0; i < noOfUrls; i++) {
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(acceptHeader);
requestTemplateFactory.addAuthorizationToken(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 | HttpServerErrorException e) {
if (i < noOfUrls - 1 && defaultProperties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) {
logger.info("Failed to fetch configs from server at : " + uri
+ ". Will try the next url if available. Error : " + e.getMessage());
continue;
}
if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
throw e;
}
}
catch (ResourceAccessException e) {
logger.info("Exception on Url - " + uri + ":" + e + ". 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;
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
/**
* Adds the provided headers to the request.
*/
@Deprecated
public static class GenericRequestHeaderInterceptor
extends ConfigClientRequestTemplateFactory.GenericRequestHeaderInterceptor {
public GenericRequestHeaderInterceptor(Map<String, String> headers) {
super(headers);
}
}
static class ConfigServiceOrigin implements Origin {
private final String remotePropertySource;
private final Object origin;
ConfigServiceOrigin(String remotePropertySource, Object origin) {
this.remotePropertySource = remotePropertySource;
Assert.notNull(origin, "origin may not be null");
this.origin = origin;
}
@Override
public String toString() {
return "Config Server " + this.remotePropertySource + ":" + this.origin.toString();
}
}
}