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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: [", "");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("\\{", "{"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user