Implement VaultEnvironmentRepository.

This is an alternative to VCS or file based repository implementations.

All properties are stored ecrypted in vault, regardless of whether or
not that actaully need to be encrypted.

Supports properties style (this.is.a.property) or vault nested
properties.

Adds a watch to client and server, if enabled, the client will long poll
the server and wait for changes rather than relying on a message broker
to send change events.

Adds a server side watch for consul keys, this is useful when vault is
backed by consul (and currently the only use case, since there is not a
consul repository implementation).

Closes gh-397
This commit is contained in:
Spencer Gibb
2016-05-04 17:00:31 -06:00
parent 0850de9737
commit 753db0ed22
32 changed files with 742 additions and 692 deletions

View File

@@ -1 +1 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
<name>Spring Cloud Config Docs</name>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Cloud Config</name>
<description>Spring Cloud Config</description>

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -80,4 +81,16 @@ public class ConfigClientAutoConfiguration {
}
}
@Configuration
@ConditionalOnClass(ContextRefresher.class)
@ConditionalOnBean(ContextRefresher.class)
@ConditionalOnProperty(value = "spring.cloud.config.watch.enabled")
protected static class ConfigClientWatchConfiguration {
@Bean
public ConfigClientWatch configClientWatch(ContextRefresher contextRefresher) {
return new ConfigClientWatch(contextRefresher);
}
}
}

View File

@@ -34,6 +34,8 @@ import org.springframework.web.util.UriComponentsBuilder;
public class ConfigClientProperties {
public static final String PREFIX = "spring.cloud.config";
public static final String TOKEN_HEADER = "X-Config-Token";
public static final String STATE_HEADER = "X-Config-State";
/**
* Flag to say that remote configuration is enabled. Default true;
@@ -83,6 +85,11 @@ public class ConfigClientProperties {
*/
private boolean failFast = false;
/**
* Security Token passed thru to underlying environment repository.
*/
private String token;
private ConfigClientProperties() {
}
@@ -170,6 +177,14 @@ public class ConfigClientProperties {
this.failFast = failFast;
}
public String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
private String[] extractCredentials() {
String[] result = new String[3];
String uri = this.uri;
@@ -272,7 +287,7 @@ public class ConfigClientProperties {
+ (this.label == null ? "" : this.label) + ", username=" + this.username
+ ", password=" + this.password + ", uri=" + this.uri
+ ", discovery.enabled=" + this.discovery.enabled + ", failFast="
+ this.failFast + "]";
+ this.failFast + ", token=" + this.token + "]";
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.config.client;
/**
* @author Spencer Gibb
*/
public class ConfigClientStateHolder {
private static ThreadLocal<String> state = new ThreadLocal<>();
public static void resetState() {
state.remove();
}
public static void setState(String newState) {
if (newState == null) {
resetState();
return;
}
state.set(newState);
}
public static String getState() {
return state.get();
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-2016 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
*
* http://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.Closeable;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Scheduled;
import static org.springframework.util.StringUtils.hasText;
/**
* @author Spencer Gibb
*/
public class ConfigClientWatch implements Closeable, EnvironmentAware {
private static Log log = LogFactory
.getLog(ConfigServicePropertySourceLocator.class);
private final AtomicBoolean running = new AtomicBoolean(false);
private final ContextRefresher refresher;
private Environment environment;
public ConfigClientWatch(ContextRefresher refresher) {
this.refresher = refresher;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@PostConstruct
public void start() {
this.running.compareAndSet(false, true);
}
@Scheduled(initialDelayString = "${spring.cloud.config.watch.initialDelay:180000}", fixedDelayString = "${spring.cloud.config.watch.delay:500}")
public void watchConfigServer() {
if (this.running.get()) {
String newState = this.environment.getProperty("config.client.state");
String oldState = ConfigClientStateHolder.getState();
// only refresh if state has changed
if (stateChanged(oldState, newState)) {
ConfigClientStateHolder.setState(newState);
this.refresher.refresh();
}
}
}
/* for testing */ boolean stateChanged(String oldState, String newState) {
return (!hasText(oldState) && hasText(newState))
|| (hasText(oldState) && !oldState.equals(newState));
}
@Override
public void close() {
this.running.compareAndSet(true, false);
}
}

View File

@@ -51,9 +51,9 @@ public class ConfigServiceBootstrapConfiguration {
@Bean
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource() {
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
configClientProperties());
properties);
return locator;
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.config.client;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
@@ -29,6 +30,7 @@ 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.HttpRequest;
import org.springframework.http.HttpStatus;
@@ -37,12 +39,17 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.retry.annotation.Retryable;
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.RestTemplate;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
/**
* @author Dave Syer
*
@@ -54,36 +61,40 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
.getLog(ConfigServicePropertySourceLocator.class);
private RestTemplate restTemplate;
private ConfigClientProperties defaults;
private ConfigClientProperties defaultProperties;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaults) {
this.defaults = defaults;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties client = this.defaults.override(environment);
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(client)
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties)
: this.restTemplate;
Exception error = null;
String errorBody = null;
logger.info("Fetching config from server at: " + client.getRawUri());
logger.info("Fetching config from server at: " + properties.getRawUri());
try {
String[] labels = new String[]{""};
if (StringUtils.hasText(client.getLabel())) {
labels = StringUtils.commaDelimitedListToStringArray(client.getLabel());
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(restTemplate, client.getRawUri(), client.getName(), client.getProfile(), label.trim());
Environment result = getRemoteEnvironment(restTemplate,
properties, label.trim(), state);
if (result != null) {
logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s",
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.getLabel(), result.getVersion(), result.getState()));
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
@@ -92,6 +103,13 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
composite.addPropertySource(new MapPropertySource(source
.getName(), map));
}
if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(new MapPropertySource("configClient", map));
}
return composite;
}
}
@@ -106,7 +124,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
catch (Exception e) {
error = e;
}
if (client != null && client.isFailFast()) {
if (properties.isFailFast()) {
throw new IllegalStateException(
"Could not locate PropertySource and the fail fast property is set, failing",
error);
@@ -117,8 +135,20 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
private Environment getRemoteEnvironment(RestTemplate restTemplate, String uri, String name, String profile, String label) {
private void putValue(HashMap<String, Object> map, String key, String value) {
if (StringUtils.hasText(value)) {
map.put(key, value);
}
}
private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties,
String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
String uri = properties.getRawUri();
Object[] args = new String[] { name, profile };
if (StringUtils.hasText(label)) {
args = new String[] { name, profile, label };
@@ -127,16 +157,24 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
ResponseEntity<Environment> response = null;
try {
response = restTemplate.exchange(uri + path,
HttpMethod.GET, new HttpEntity<Void>((Void) null),
Environment.class, args);
} catch (HttpClientErrorException e) {
if(e.getStatusCode() != HttpStatus.NOT_FOUND ) {
HttpHeaders headers = new HttpHeaders();
if (StringUtils.hasText(token)) {
headers.add(TOKEN_HEADER, token);
}
if (StringUtils.hasText(state)) { //TODO: opt in to sending state?
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;
}
}
if (response==null || response.getStatusCode()!=HttpStatus.OK) {
if (response == null || response.getStatusCode() != HttpStatus.OK) {
return null;
}
Environment result = response.getBody();
@@ -148,7 +186,9 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
RestTemplate template = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout((60 * 1000 * 3) + 5000); //TODO 3m5s, make configurable?
RestTemplate template = new RestTemplate(requestFactory);
String password = client.getPassword();
if (password != null) {
template.setInterceptors(Arrays
@@ -173,8 +213,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
byte[] token = Base64
.encode((this.username + ":" + this.password).getBytes());
byte[] token = Base64Utils.encode((this.username + ":" + this.password).getBytes());
request.getHeaders().add("Authorization", "Basic " + new String(token));
return execution.execute(request, body);
}
@@ -182,634 +221,3 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
}
/**
* Base64 encoder which is a reduced version of Robert Harder's public domain
* implementation (version 2.3.7). See <a
* href="http://iharder.net/base64">http://iharder.net/base64</a> for more information.
* <p>
* For internal use only.
*
* @author Luke Taylor
* @since 3.0
*/
final class Base64 {
/** No options specified. Value is zero. */
public final static int NO_OPTIONS = 0;
/** Specify encoding in first bit. Value is one. */
public final static int ENCODE = 1;
/** Specify decoding in first bit. Value is zero. */
public final static int DECODE = 0;
/** Do break lines when encoding. Value is 8. */
public final static int DO_BREAK_LINES = 8;
/**
* Encode using Base64-like encoding that is URL- and Filename-safe as described in
* Section 4 of RFC3548: <a
* href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs
* .org/rfcs/rfc3548.html</a>. It is important to note that data encoded this way is
* <em>not</em> officially valid Base64, or at the very least should not be called
* Base64 without also specifying that is was encoded using the URL- and Filename-safe
* dialect.
*/
public final static int URL_SAFE = 16;
/**
* Encode using the special "ordered" dialect of Base64 described here: <a
* href="http://www.faqs.org/qa/rfcc-1940.html"
* >http://www.faqs.org/qa/rfcc-1940.html</a>.
*/
public final static int ORDERED = 32;
/** Maximum line length (76) of Base64 output. */
private final static int MAX_LINE_LENGTH = 76;
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte) '=';
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte) '\n';
private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
/** The 64 valid Base64 values. */
/* Host platform me be something funny like EBCDIC, so we hardcode these values. */
private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B',
(byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H',
(byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T',
(byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
(byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l',
(byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r',
(byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
(byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9',
(byte) '+', (byte) '/' };
/**
* Translates a Base64 value to either its 6-bit reconstruction value or a negative
* number indicating some other meaning.
**/
private final static byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
62, // Plus sign at decimal 43
-9, -9, -9, // Decimal 44 - 46
63, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
/**
* Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: <a
* href
* ="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
* Notice that the last two bytes become "hyphen" and "underscore" instead of "plus"
* and "slash."
*/
private final static byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B',
(byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H',
(byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T',
(byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
(byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l',
(byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r',
(byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
(byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9',
(byte) '-', (byte) '_' };
/**
* Used in decoding URL- and Filename-safe dialects of Base64.
*/
private final static byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
-9, // Plus sign at decimal 43
-9, // Decimal 44
62, // Minus sign at decimal 45
-9, // Decimal 46
-9, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, // Decimal 91 - 94
63, // Underscore at decimal 95
-9, // Decimal 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
/**
* I don't get the point of this technique, but someone requested it, and it is
* described here: <a
* href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/
* qa/rfcc-1940.html</a>.
*/
private final static byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', (byte) '1',
(byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
(byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D',
(byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J',
(byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V',
(byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a',
(byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g',
(byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm',
(byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's',
(byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z' };
/**
* Used in decoding the "ordered" dialect of Base64.
*/
private final static byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
-9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
-9, // Plus sign at decimal 43
-9, // Decimal 44
0, // Minus sign at decimal 45
-9, // Decimal 46
-9, // Slash at decimal 47
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M'
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z'
-9, -9, -9, -9, // Decimal 91 - 94
37, // Underscore at decimal 95
-9, // Decimal 96
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm'
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
};
public static byte[] decode(byte[] bytes) {
return decode(bytes, 0, bytes.length, NO_OPTIONS);
}
public static byte[] encode(byte[] bytes) {
return encodeBytesToBytes(bytes, 0, bytes.length, NO_OPTIONS);
}
public static boolean isBase64(byte[] bytes) {
try {
decode(bytes);
}
catch (InvalidBase64CharacterException e) {
return false;
}
return true;
}
/**
* Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options
* specified. It's possible, though silly, to specify ORDERED <b>and</b> URLSAFE in
* which case one of them will be picked, though there is no guarantee as to which one
* will be picked.
*/
private static byte[] getAlphabet(int options) {
if ((options & URL_SAFE) == URL_SAFE) {
return _URL_SAFE_ALPHABET;
}
else if ((options & ORDERED) == ORDERED) {
return _ORDERED_ALPHABET;
}
else {
return _STANDARD_ALPHABET;
}
}
/**
* Returns one of the _SOMETHING_DECODABET byte arrays depending on the options
* specified. It's possible, though silly, to specify ORDERED and URL_SAFE in which
* case one of them will be picked, though there is no guarantee as to which one will
* be picked.
*/
private static byte[] getDecodabet(int options) {
if ((options & URL_SAFE) == URL_SAFE) {
return _URL_SAFE_DECODABET;
}
else if ((options & ORDERED) == ORDERED) {
return _ORDERED_DECODABET;
}
else {
return _STANDARD_DECODABET;
}
}
/* ******** E N C O D I N G M E T H O D S ******** */
/**
* <p>
* Encodes up to three bytes of the array <var>source</var> and writes the resulting
* four Base64 bytes to <var>destination</var>. The source and destination arrays can
* be manipulated anywhere along their length by specifying <var>srcOffset</var> and
* <var>destOffset</var>. This method does not check to make sure your arrays are
* large enough to accomodate <var>srcOffset</var> + 3 for the <var>source</var> array
* or <var>destOffset</var> + 4 for the <var>destination</var> array. The actual
* number of significant bytes in your array is given by <var>numSigBytes</var>.
* </p>
* <p>
* This is the lowest level of the encoding methods with all possible parameters.
* </p>
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param numSigBytes the number of significant bytes in your array
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @return the <var>destination</var> array
* @since 1.3
*/
private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes,
byte[] destination, int destOffset, int options) {
byte[] ALPHABET = getAlphabet(options);
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index ALPHABET
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an int.
int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
switch (numSigBytes) {
case 3:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
return destination;
case 2:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
case 1:
destination[destOffset] = ALPHABET[(inBuff >>> 18)];
destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = EQUALS_SIGN;
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
default:
return destination;
}
}
/**
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
* @param len Length of data to convert
* @param options Specified options
* @return The Base64-encoded data as a String
* @see Base64#DO_BREAK_LINES
* @throws java.io.IOException if there is an error
* @throws NullPointerException if source array is null
* @throws IllegalArgumentException if source array, offset, or length are invalid
* @since 2.3.1
*/
private static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) {
if (source == null) {
throw new NullPointerException("Cannot serialize a null array.");
} // end if: null
if (off < 0) {
throw new IllegalArgumentException("Cannot have negative offset: " + off);
} // end if: off < 0
if (len < 0) {
throw new IllegalArgumentException("Cannot have length offset: " + len);
} // end if: len < 0
if (off + len > source.length) {
throw new IllegalArgumentException(String.format(
"Cannot have offset of %d and length of %d with array of length %d",
off, len, source.length));
} // end if: off < 0
boolean breakLines = (options & DO_BREAK_LINES) > 0;
// int len43 = len * 4 / 3;
// byte[] outBuff = new byte[ ( len43 ) // Main 4:3
// + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding
// + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
// Try to determine more precisely how big the array needs to be.
// If we get it right, we don't have to do an array copy, and
// we save a bunch of memory.
int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual
// encoding
if (breakLines) {
encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
}
byte[] outBuff = new byte[encLen];
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
for (; d < len2; d += 3, e += 4) {
encode3to4(source, d + off, 3, outBuff, e, options);
lineLength += 4;
if (breakLines && lineLength >= MAX_LINE_LENGTH) {
outBuff[e + 4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // en dfor: each piece of array
if (d < len) {
encode3to4(source, d + off, len - d, outBuff, e, options);
e += 4;
} // end if: some padding needed
// Only resize array if we didn't guess it right.
if (e <= outBuff.length - 1) {
byte[] finalOut = new byte[e];
System.arraycopy(outBuff, 0, finalOut, 0, e);
// System.err.println("Having to resize array from " + outBuff.length + " to "
// + e );
return finalOut;
}
else {
// System.err.println("No need to resize array.");
return outBuff;
}
}
/* ******** D E C O D I N G M E T H O D S ******** */
/**
* Decodes four bytes from array <var>source</var> and writes the resulting bytes (up
* to three of them) to <var>destination</var>. The source and destination arrays can
* be manipulated anywhere along their length by specifying <var>srcOffset</var> and
* <var>destOffset</var>. This method does not check to make sure your arrays are
* large enough to accomodate <var>srcOffset</var> + 4 for the <var>source</var> array
* or <var>destOffset</var> + 3 for the <var>destination</var> array. This method
* returns the actual number of bytes that were converted from the Base64 encoding.
* <p>
* This is the lowest level of the decoding methods with all possible parameters.
* </p>
*
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param options alphabet type is pulled from this (standard, url-safe, ordered)
* @return the number of decoded bytes converted
* @throws NullPointerException if source or destination arrays are null
* @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is
* not enough room in the array.
* @since 1.3
*/
private static int decode4to3(final byte[] source, final int srcOffset,
final byte[] destination, final int destOffset, final int options) {
// Lots of error checking and exception throwing
if (source == null) {
throw new NullPointerException("Source array was null.");
} // end if
if (destination == null) {
throw new NullPointerException("Destination array was null.");
} // end if
if (srcOffset < 0 || srcOffset + 3 >= source.length) {
throw new IllegalArgumentException(
String.format(
"Source array with length %d cannot have offset of %d and still process four bytes.",
source.length, srcOffset));
} // end if
if (destOffset < 0 || destOffset + 2 >= destination.length) {
throw new IllegalArgumentException(
String.format(
"Destination array with length %d cannot have offset of %d and still store three bytes.",
destination.length, destOffset));
} // end if
byte[] DECODABET = getDecodabet(options);
// Example: Dk==
if (source[srcOffset + 2] == EQUALS_SIGN) {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);
destination[destOffset] = (byte) (outBuff >>> 16);
return 1;
}
// Example: DkL=
else if (source[srcOffset + 3] == EQUALS_SIGN) {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
// | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
| ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);
destination[destOffset] = (byte) (outBuff >>> 16);
destination[destOffset + 1] = (byte) (outBuff >>> 8);
return 2;
}
// Example: DkLE
else {
// Two ways to do the same thing. Don't know which way I like best.
// int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
// | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
// | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
| ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
| ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
| ((DECODABET[source[srcOffset + 3]] & 0xFF));
destination[destOffset] = (byte) (outBuff >> 16);
destination[destOffset + 1] = (byte) (outBuff >> 8);
destination[destOffset + 2] = (byte) (outBuff);
return 3;
}
}
/**
* Low-level access to decoding ASCII characters in the form of a byte array.
* <strong>Ignores GUNZIP option, if it's set.</strong> This is not generally a
* recommended method, although it is used internally as part of the decoding process.
* Special case: if len = 0, an empty array is returned. Still, if you need more speed
* and reduced memory footprint (and aren't gzipping), consider this method.
*
* @param source The Base64 encoded data
* @param off The offset of where to begin decoding
* @param len The length of characters to decode
* @param options Can specify options such as alphabet type to use
* @return decoded data
* @throws IllegalArgumentException If bogus characters exist in source data
*/
private static byte[] decode(final byte[] source, final int off, final int len,
final int options) {
// Lots of error checking and exception throwing
if (source == null) {
throw new NullPointerException("Cannot decode null source array.");
} // end if
if (off < 0 || off + len > source.length) {
throw new IllegalArgumentException(
String.format(
"Source array with length %d cannot have offset of %d and process %d bytes.",
source.length, off, len));
} // end if
if (len == 0) {
return new byte[0];
}
else if (len < 4) {
throw new IllegalArgumentException(
"Base64-encoded string must have at least four characters, but length specified was "
+ len);
} // end if
byte[] DECODABET = getDecodabet(options);
int len34 = len * 3 / 4; // Estimate on array size
byte[] outBuff = new byte[len34]; // Upper limit on size of output
int outBuffPosn = 0; // Keep track of where we're writing
byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
int b4Posn = 0; // Keep track of four byte input buffer
int i = 0; // Source array counter
byte sbiDecode = 0; // Special value from DECODABET
for (i = off; i < off + len; i++) { // Loop through source
sbiDecode = DECODABET[source[i] & 0xFF];
// White space, Equals sign, or legit Base64 character
// Note the values such as -5 and -9 in the
// DECODABETs at the top of the file.
if (sbiDecode >= WHITE_SPACE_ENC) {
if (sbiDecode >= EQUALS_SIGN_ENC) {
b4[b4Posn++] = source[i]; // Save non-whitespace
if (b4Posn > 3) { // Time to decode?
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options);
b4Posn = 0;
// If that was the equals sign, break out of 'for' loop
if (source[i] == EQUALS_SIGN) {
break;
}
}
}
}
else {
// There's a bad input character in the Base64 stream.
throw new InvalidBase64CharacterException(String.format(
"Bad Base64 input character decimal %d in array position %d",
(source[i]) & 0xFF, i));
}
}
byte[] out = new byte[outBuffPosn];
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
return out;
}
}
@SuppressWarnings("serial")
class InvalidBase64CharacterException extends IllegalArgumentException {
InvalidBase64CharacterException(String message) {
super(message);
}
}

View File

@@ -40,24 +40,36 @@ public class Environment {
private String label;
private List<PropertySource> propertySources = new ArrayList<PropertySource>();
private List<PropertySource> propertySources = new ArrayList<>();
private String version;
private String state;
public Environment(String name, String... profiles) {
this(name, profiles, "master", null);
this(name, profiles, "master", null, null);
}
/**
* Copies all fields except propertySources
* @param env
*/
public Environment(Environment env) {
this(env.getName(), env.getProfiles(), env.getLabel(), env.getVersion(), env.getState());
}
@JsonCreator
public Environment(@JsonProperty("name") String name,
@JsonProperty("profiles") String[] profiles,
@JsonProperty("label") String label,
@JsonProperty("version") String version) {
@JsonProperty("version") String version,
@JsonProperty("state") String state) {
super();
this.name = name;
this.profiles = profiles;
this.label = label;
this.version = version;
this.state = state;
}
public void add(PropertySource propertySource) {
@@ -104,11 +116,20 @@ public class Environment {
this.version = version;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
@Override
public String toString() {
return "Environment [name=" + name + ", profiles=" + Arrays.asList(profiles)
+ ", label=" + label + ", propertySources=" + propertySources
+ ", version=" + version+ "]";
+ ", version=" + version
+ ", state=" + state + "]";
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013-2016 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
*
* http://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 static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* @author Spencer Gibb
*/
public class ConfigClientWatchTests {
@Test
public void stateChangedWorks() {
ConfigClientWatch watch = new ConfigClientWatch(null);
assertThat(watch.stateChanged(null, "1"), is(true));
assertThat(watch.stateChanged("1", "2"), is(true));
assertThat(watch.stateChanged("1", null), is(true));
assertThat(watch.stateChanged("1", "1"), is(false));
}
}

View File

@@ -44,7 +44,7 @@ public class ConfigServicePropertySourceLocatorTests {
@Test
public void sunnyDay() {
Environment body = new Environment("app", "master");
mockRequestResponseWithoutLabel(new ResponseEntity<Environment>(body,
mockRequestResponseWithoutLabel(new ResponseEntity<>(body,
HttpStatus.OK));
this.locator.setRestTemplate(this.restTemplate);
assertNotNull(this.locator.locate(this.environment));
@@ -54,7 +54,7 @@ public class ConfigServicePropertySourceLocatorTests {
public void sunnyDayWithLabel() {
Environment body = new Environment("app", "master");
mockRequestResponseWithLabel(
new ResponseEntity<Environment>(body, HttpStatus.OK), "v1.0.0");
new ResponseEntity<>(body, HttpStatus.OK), "v1.0.0");
this.locator.setRestTemplate(this.restTemplate);
EnvironmentTestUtils.addEnvironment(this.environment,
"spring.cloud.config.label:v1.0.0");
@@ -71,7 +71,7 @@ public class ConfigServicePropertySourceLocatorTests {
@Test
public void failsQuietly() {
mockRequestResponseWithoutLabel(new ResponseEntity<String>("Wah!",
mockRequestResponseWithoutLabel(new ResponseEntity<>("Wah!",
HttpStatus.INTERNAL_SERVER_ERROR));
this.locator.setRestTemplate(this.restTemplate);
assertNull(this.locator.locate(this.environment));

View File

@@ -9,7 +9,7 @@
<relativePath/>
</parent>
<artifactId>spring-cloud-config-dependencies</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-config-dependencies</name>
<description>Spring Cloud Config Dependencies</description>
@@ -28,22 +28,22 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-monitor</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-config-monitor</artifactId>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -1,12 +1,26 @@
package sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class Application {
@Autowired
private Environment environment;
@RequestMapping("/")
public String query(@RequestParam("q") String q) {
return environment.getProperty(q);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.config.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor;
import org.springframework.cloud.config.server.environment.EnvironmentController;
@@ -42,9 +43,6 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private EnvironmentRepository repository;
@Autowired
private ResourceRepository resources;
@Autowired
private ConfigServerProperties server;
@@ -69,8 +67,9 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter {
}
@Bean
public ResourceController resourceController() {
ResourceController controller = new ResourceController(this.resources,
@ConditionalOnBean(ResourceRepository.class)
public ResourceController resourceController(ResourceRepository repository) {
ResourceController controller = new ResourceController(repository,
encrypted());
return controller;
}

View File

@@ -15,14 +15,19 @@
*/
package org.springframework.cloud.config.server.config;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.config.server.environment.ConsulEnvironmentWatch;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository;
import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository;
import org.springframework.cloud.config.server.environment.SvnKitEnvironmentRepository;
import org.springframework.cloud.config.server.environment.VaultEnvironmentRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@@ -96,4 +101,32 @@ public class EnvironmentRepositoryConfiguration {
}
}
@Configuration
@Profile("vault")
protected static class VaultConfiguration {
@Bean
public EnvironmentRepository environmentRepository(HttpServletRequest request, EnvironmentWatch watch) {
return new VaultEnvironmentRepository(request, watch);
}
}
@Configuration
@ConditionalOnProperty(value = "spring.cloud.config.server.consul.watch.enabled")
protected static class ConsulEnvironmentWatchConfiguration {
@Bean
public EnvironmentWatch environmentWatch() {
return new ConsulEnvironmentWatch();
}
}
@Configuration
@ConditionalOnMissingBean(EnvironmentWatch.class)
protected static class DefaultEnvironmentWatch {
@Bean
public EnvironmentWatch environmentWatch() {
return new EnvironmentWatch.Default();
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.cloud.config.server.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.config.server.environment.SearchPathLocator;
@@ -33,6 +34,7 @@ import org.springframework.context.annotation.Configuration;
public class ResourceRepositoryConfiguration {
@Bean
@ConditionalOnBean(SearchPathLocator.class)
public ResourceRepository resourceRepository(SearchPathLocator service) {
return new GenericResourceRepository(service);
}

View File

@@ -57,8 +57,7 @@ public class CipherEnvironmentEncryptor implements EnvironmentEncryptor {
}
private Environment decrypt(Environment environment, TextEncryptorLocator encryptor) {
Environment result = new Environment(environment.getName(),
environment.getProfiles(), environment.getLabel(), environment.getVersion());
Environment result = new Environment(environment);
for (PropertySource source : environment.getPropertySources()) {
Map<Object, Object> map = new LinkedHashMap<Object, Object>(
source.getSource());

View File

@@ -0,0 +1,126 @@
package org.springframework.cloud.config.server.environment;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.NotNull;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.config.server.consul.watch")
public class ConsulEnvironmentWatch implements EnvironmentWatch {
public static final String CONSUL_INDEX = "X-Consul-Index";
public static final String CONSUL_TOKEN = "X-Consul-Token";
private static Log LOG = LogFactory.getLog(ConsulEnvironmentWatch.class);
private static final String WATCH_URL = "{scheme}://{host}:{port}/v1/kv/{path}?keys&recurse&wait={wait}&index={index}";
public static final ParameterizedTypeReference<List<String>> RESPONSE_TYPE = new ParameterizedTypeReference<List<String>>() {
};
private RestTemplate restTemplate = new RestTemplate();
/** Consul agent scheme. Defaults to 'http'. */
@NotNull
private String scheme = "http";
/** Consul agent hostname. Defaults to 'localhost'. */
@NotNull
private String host = "localhost";
/** Consul agent port. Defaults to '8500'. */
@NotNull
private int port = 8500;
/** Path to watch in consul key/value store. */
@NotNull
private String path;
/** Consul wait value (eg, 3m or 30s). */
@NotNull
private String wait = "3m";
/** Consul ACL token. */
private String token;
@Override
public String watch(String state) {
ArrayList<String> params = new ArrayList<>();
params.add(this.scheme);
params.add(this.host);
params.add(String.valueOf(this.port));
params.add(this.path);
params.add(this.wait);
params.add (StringUtils.hasText(state) ? state : "");
try {
HttpHeaders headers = new HttpHeaders();
if (StringUtils.hasText(token)) {
headers.add(CONSUL_TOKEN, token);
}
HttpEntity<Object> request = new HttpEntity<>(headers);
ResponseEntity<List<String>> response = this.restTemplate.exchange(WATCH_URL,
HttpMethod.GET, request, RESPONSE_TYPE, params.toArray());
if (response.getStatusCode().is2xxSuccessful()) {
String consulIndex = response.getHeaders().getFirst(CONSUL_INDEX);
return consulIndex;
}
}
catch (HttpStatusCodeException e) {
if (!e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
LOG.error("Unable to watch consul path " + this.path, e);
return null;
}
}
// TODO: error handling?
return null;
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public void setScheme(String scheme) {
this.scheme = scheme;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setPath(String path) {
this.path = path;
if (this.path.startsWith("/")) {
this.path = this.path.substring(1);
}
}
public void setWait(String wait) {
this.wait = wait;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -25,8 +25,7 @@ import org.springframework.cloud.config.environment.PropertySource;
public class EnvironmentCleaner {
public Environment clean(Environment value, String workingDir, String uri) {
Environment result = new Environment(value.getName(), value.getProfiles(),
value.getLabel(), value.getVersion());
Environment result = new Environment(value);
for (PropertySource source : value.getPropertySources()) {
String name = source.getName().replace(workingDir, "");
name = name.replace("applicationConfig: [", "");

View File

@@ -277,7 +277,7 @@ public class EnvironmentController {
String[] keys = StringUtils.delimitedListToStringArray(stem, ".");
for (int i = 0; i < keys.length - 1; i++) {
if (current.get(keys[i]) == null) {
LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
current.put(keys[i], map);
current = map;
}
@@ -290,7 +290,7 @@ public class EnvironmentController {
}
String name = keys[keys.length - 1];
if (current.get(name) == null) {
current.put(name, new ArrayList<Object>());
current.put(name, new ArrayList<>());
}
@SuppressWarnings("unchecked")
List<Object> value = (List<Object>) current.get(name);

View File

@@ -64,7 +64,7 @@ public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRep
* @param overrides the overrides to set
*/
public void setOverrides(Map<String, String> overrides) {
this.overrides = new HashMap<String, String>(overrides);
this.overrides = new HashMap<>(overrides);
for (String key : overrides.keySet()) {
if (overrides.get(key).contains("\\{")) {
this.overrides.put(key, overrides.get(key).replace("\\{", "{"));

View File

@@ -0,0 +1,16 @@
package org.springframework.cloud.config.server.environment;
/**
* @author Spencer Gibb
*/
public interface EnvironmentWatch {
String watch(String state);
class Default implements EnvironmentWatch {
@Override
public String watch(String state) {
return null;
}
}
}

View File

@@ -178,7 +178,7 @@ public class NativeEnvironmentRepository
protected Environment clean(Environment value) {
Environment result = new Environment(value.getName(), value.getProfiles(),
value.getLabel(), this.version);
value.getLabel(), this.version, value.getState());
for (PropertySource source : value.getPropertySources()) {
String name = source.getName();
if (this.environment.getPropertySources().contains(name)) {

View File

@@ -60,7 +60,7 @@ public class PassthruEnvironmentRepository implements EnvironmentRepository {
@Override
public Environment findOne(String application, String env, String label) {
Environment result = new Environment(application, StringUtils.commaDelimitedListToStringArray(env), label, null);
Environment result = new Environment(application, StringUtils.commaDelimitedListToStringArray(env), label, null, null);
for (org.springframework.core.env.PropertySource<?> source : this.environment.getPropertySources()) {
String name = source.getName();
if (!this.standardSources.contains(name) && source instanceof MapPropertySource) {

View File

@@ -0,0 +1,247 @@
package org.springframework.cloud.config.server.environment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.JsonNode;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.annotation.JsonProperty;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.config.server.vault")
public class VaultEnvironmentRepository implements EnvironmentRepository {
public static final String VAULT_TOKEN = "X-Vault-Token";
/** Vault host. Defaults to 127.0.0.1. */
@NotEmpty
private String host = "127.0.0.1";
/** Vault port. Defaults to 8200. */
@Range(min = 1, max = 65535)
private int port = 8200;
/** Vault scheme. Defaults to http. */
private String scheme = "http";
/** Vault backend. Defaults to secret. */
@NotEmpty
private String backend = "secret";
/** The key in vault shared by all applications. Defaults to application. Set to empty to disable. */
private String defaultKey = "application";
/** Vault profile separator. Defaults to comma. */
@NotEmpty
private String profileSeparator = ",";
private RestTemplate rest = new RestTemplate();
//TODO: move to watchState:String on findOne?
private HttpServletRequest request;
private EnvironmentWatch watch;
public VaultEnvironmentRepository(HttpServletRequest request, EnvironmentWatch watch) {
this.request = request;
this.watch = watch;
}
@Override
public Environment findOne(String application, String profile, String label) {
String state = request.getHeader(STATE_HEADER);
String newState = this.watch.watch(state);
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
List<String> scrubbedProfiles = scrubProfiles(profiles);
List<String> keys = findKeys(application, scrubbedProfiles);
Environment environment = new Environment(application, profiles, label, null, newState);
for (String key : keys) {
// read raw 'data' key from vault
String data = read(key);
// data is in json format of which, yaml is a superset, so parse
final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ByteArrayResource(data.getBytes()));
Properties properties = yaml.getObject();
if (!properties.isEmpty()) {
environment.add(new PropertySource("vault:"+key, properties));
}
}
return environment;
}
private List<String> findKeys(String application, List<String> profiles) {
List<String> keys = new ArrayList<>();
if (StringUtils.hasText(this.defaultKey)) {
keys.add(this.defaultKey);
addProfiles(keys, this.defaultKey, profiles);
}
keys.add(application);
addProfiles(keys, application, profiles);
Collections.reverse(keys);
return keys;
}
private List<String> scrubProfiles(String[] profiles) {
List<String> scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles));
if (scrubbedProfiles.contains("default")) {
scrubbedProfiles.remove("default");
}
return scrubbedProfiles;
}
private void addProfiles(List<String> contexts, String baseContext,
List<String> profiles) {
for (String profile : profiles) {
contexts.add(baseContext + this.profileSeparator + profile);
}
}
String read(String key) {
String url = String.format("%s://%s:%s/v1/{backend}/{key}", this.scheme,
this.host, this.port);
HttpHeaders headers = new HttpHeaders();
String token = request.getHeader(TOKEN_HEADER);
if (!StringUtils.hasLength(token)) {
throw new IllegalArgumentException("Missing required header: "+TOKEN_HEADER);
}
headers.add(VAULT_TOKEN, token);
try {
ResponseEntity<VaultResponse> response = this.rest.exchange(url,
HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class,
this.backend, key);
HttpStatus status = response.getStatusCode();
if (status == HttpStatus.OK) {
return response.getBody().getData();
}
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
throw e;
}
return null;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setScheme(String scheme) {
this.scheme = scheme;
}
public void setBackend(String backend) {
this.backend = backend;
}
public void setDefaultKey(String defaultKey) {
this.defaultKey = defaultKey;
}
public void setProfileSeparator(String profileSeparator) {
this.profileSeparator = profileSeparator;
}
static class VaultResponse {
private String auth;
private Object data;
@JsonProperty("lease_duration")
private long leaseDuration;
@JsonProperty("lease_id")
private String leaseId;
private boolean renewable;
public VaultResponse() {
}
public String getAuth() {
return auth;
}
public void setAuth(String auth) {
this.auth = auth;
}
@JsonRawValue
public String getData() {
return data == null ? null : data.toString();
}
public void setData(JsonNode data) {
this.data = data;
}
public long getLeaseDuration() {
return leaseDuration;
}
public void setLeaseDuration(long leaseDuration) {
this.leaseDuration = leaseDuration;
}
public String getLeaseId() {
return leaseId;
}
public void setLeaseId(String leaseId) {
this.leaseId = leaseId;
}
public boolean isRenewable() {
return renewable;
}
public void setRenewable(boolean renewable) {
this.renewable = renewable;
}
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.cloud.config.server.environment;
import org.junit.Test;
/**
* @author Spencer Gibb
*/
public class VaultEnvironmentRepositoryTests {
@Test
public void testFindOne() {
//TODO: implement testFindOne
}
}

View File

@@ -5,10 +5,10 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-starter-config</artifactId>
<version>1.1.3.BUILD-SNAPSHOT</version>
<version>1.1.2.vault.BUILD-SNAPSHOT</version>
<name>spring-cloud-starter-config</name>
<description>Spring Cloud Starter</description>
<url>http://projects.spring.io/spring-cloud</url>