Add ResourceController for serving plain text config files

Can be adapted to serve content in any format (e.g. nginx config file,
XML configuration for logger, etc.) - basically anything that can be
stored in plain text and doesn't require streaming.

Fixes gh-147, see also gh-198
This commit is contained in:
Dave Syer
2015-09-30 13:50:37 +01:00
parent 02a9eb3cd7
commit 62846bda84
27 changed files with 738 additions and 124 deletions

View File

@@ -526,6 +526,80 @@ TIP: the `{name:value}` prefixes can also be added to plaintext posted
to the `/encrypt` endpoint, if you want to let the Config Server
handle all encryption as well as decryption.
== Serving Plain Text
Instead of using the `Environment` abstraction (or one of the
alternative representations of it in YAML or properties format) your
applications might need generic plain text configuration files,
tailored to their environment. The Config Server provides these
through an additional endpoint at `/{name}/{profile}/{label}/{path}`
where "name", "profile" and "label" have the same meaning as the
regular environment endpoint, but "path" is a file name
(e.g. `log.xml`). The source files for this endpoint are located in
the same way as for the environment endpoints: the same search path is
used as for properties or YAML files, but instead of aggregating all
matching resources, only the first one to match is returned.
After a resource is located, placeholders in the normal format
(`${...}`) are resolved using the effective `Environment` for the
application name, profile and label supplied. In this way the resource
endpoint is tightly integrated with the environment
endpoints. Example, if you have this layout for a GIT (or SVN)
repository:
----
application.yml
nginx.conf
----
where `nginx.conf` looks like this:
----
server {
listen 80;
server_name ${nginx.server.name};
}
----
and `application.yml` like this:
[source,yaml]
----
nginx:
server:
name: example.com
---
spring:
profiles: development
nginx:
server:
name: develop.com
----
then the `/foo/default/master/nginx.conf` resource looks like this:
----
server {
listen 80;
server_name example.com;
}
----
and `/foo/development/master/nginx.conf` like this:
----
server {
listen 80;
server_name develop.com;
}
----
NOTE: just like the source files for environment configuration, the
"profile" is used to resolve the file name, so if you want a
profile-specific file you can use a file called
`logback-development.xml` to be resolved by
`/\*/development/*/logback.xml`.
== Embedding the Config Server
The Config Server runs best as a standalone application, but if you

View File

@@ -34,7 +34,7 @@ import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.config.server.AbstractScmAccessor;
import org.springframework.cloud.config.server.AbstractScmEnvironmentRepository;
import org.springframework.cloud.config.server.NativeEnvironmentRepository;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.SmartLifecycle;
@@ -66,7 +66,7 @@ public class FileMonitorConfiguration implements SmartLifecycle, ResourceLoaderA
PropertyPathEndpoint endpoint;
@Autowired(required = false)
AbstractScmAccessor scmRepository;
AbstractScmEnvironmentRepository scmRepository;
@Autowired(required = false)
NativeEnvironmentRepository nativeEnvironmentRepository;

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2015 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.server;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* @author Dave Syer
*
*/
public abstract class AbstractScmEnvironmentRepository extends AbstractScmAccessor
implements EnvironmentRepository, ResourceLocationService {
private EnvironmentCleaner cleaner = new EnvironmentCleaner();
public AbstractScmEnvironmentRepository(ConfigurableEnvironment environment) {
super(environment);
}
@Override
public synchronized Environment findOne(String application, String profile, String label) {
NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(
getEnvironment());
delegate.setSearchLocations(getLocations(application, profile, label));
Environment result = delegate.findOne(application, profile, "");
result.setLabel(label);
return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(),
getUri());
}
}

View File

@@ -24,6 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.cloud.config.server.config.ConfigServerEncryptionConfiguration;
import org.springframework.cloud.config.server.config.ConfigServerMvcConfiguration;
import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration;
import org.springframework.cloud.config.server.config.ResourceRepositoryConfiguration;
import org.springframework.context.annotation.Import;
/**
@@ -33,7 +34,7 @@ import org.springframework.context.annotation.Import;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ EnvironmentRepositoryConfiguration.class,
@Import({ EnvironmentRepositoryConfiguration.class, ResourceRepositoryConfiguration.class,
ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public @interface EnableConfigServer {

View File

@@ -74,6 +74,10 @@ public class EnvironmentController {
private boolean stripDocument = true;
public EnvironmentController(EnvironmentRepository repository) {
this(repository, null);
}
public EnvironmentController(EnvironmentRepository repository,
EnvironmentEncryptor environmentEncryptor) {
this.repository = repository;

View File

@@ -17,7 +17,6 @@ package org.springframework.cloud.config.server;
import org.springframework.cloud.config.environment.Environment;
/**
* @author Dave Syer
* @author Roy Clarkson

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2013-2015 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.server;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
/**
* An {@link ResourceRepository} backed by a {@link ResourceLocationService}.
*
* @author Dave Syer
*/
public class GenericResourceRepository
implements ResourceRepository, ResourceLoaderAware {
private ResourceLoader resourceLoader;
private ResourceLocationService service;
public GenericResourceRepository(ResourceLocationService service) {
this.service = service;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public synchronized Resource findOne(String application, String profile, String label,
String path) {
String[] locations = this.service.getLocations(application, "default", label);
try {
for (int i = locations.length; i-- > 0;) {
String location = locations[i];
for (String local : getProfilePaths(profile, path)) {
Resource file = this.resourceLoader.getResource(location)
.createRelative(local);
if (file.exists() && file.isReadable()) {
return file;
}
}
}
}
catch (IOException e) {
throw new NoSuchResourceException(
"Error : " + path + ". (" + e.getMessage() + ")");
}
throw new NoSuchResourceException("Not found: " + path);
}
private Collection<String> getProfilePaths(String profiles, String path) {
Set<String> paths = new LinkedHashSet<>();
for (String profile : StringUtils.commaDelimitedListToSet(profiles)) {
if (!StringUtils.hasText(profile) || "default".equals(profile)) {
paths.add(path);
}
else {
String ext = StringUtils.getFilenameExtension(path);
if (ext != null) {
ext = "." + ext;
path = StringUtils.stripFilenameExtension(path);
}
else {
ext = "";
}
paths.add(path + "-" + profile + ext);
}
}
return paths;
}
}

View File

@@ -39,7 +39,6 @@ import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.util.FileUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.UrlResource;
import org.springframework.util.Assert;
@@ -53,13 +52,15 @@ import com.jcraft.jsch.Session;
* @author Dave Syer
* @author Roy Clarkson
*/
public class JGitEnvironmentRepository extends AbstractScmAccessor implements EnvironmentRepository, InitializingBean {
public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository
implements EnvironmentRepository, ResourceLocationService, InitializingBean {
private static final String DEFAULT_LABEL = "master";
private static final String FILE_URI_PREFIX = "file:";
/**
* Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable). Default 5 seconds.
* Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable). Default
* 5 seconds.
*/
private int timeout = 5;
@@ -71,8 +72,6 @@ public class JGitEnvironmentRepository extends AbstractScmAccessor implements En
*/
private boolean cloneOnStart = false;
private EnvironmentCleaner cleaner = new EnvironmentCleaner();
private JGitEnvironmentRepository.JGitFactory gitFactory = new JGitEnvironmentRepository.JGitFactory();
public JGitEnvironmentRepository(ConfigurableEnvironment environment) {
@@ -109,12 +108,33 @@ public class JGitEnvironmentRepository extends AbstractScmAccessor implements En
}
@Override
public Environment findOne(String application, String profile, String label) {
public String[] getLocations(String application, String profile, String label) {
refresh(application, label);
return getSearchLocations(getWorkingDirectory());
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(getUri() != null,
"You need to configure a uri for the git repository");
if (this.cloneOnStart) {
initClonedRepository();
}
}
/**
* Get the working directory ready.
*/
private void refresh(String application, String label) {
initialize();
Git git = null;
try {
git = createGitClient();
return loadEnvironment(git, application, profile, label);
git.getRepository().getConfig().setString("branch", label, "merge", label);
Ref ref = checkout(git, label);
if (shouldPull(git, ref)) {
pull(git, label, ref);
}
}
catch (RefNotFoundException e) {
throw new NoSuchLabelException("No such label: " + label);
@@ -137,15 +157,6 @@ public class JGitEnvironmentRepository extends AbstractScmAccessor implements En
}
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(getUri() != null,
"You need to configure a uri for the git repository");
if (this.cloneOnStart) {
initClonedRepository();
}
}
/**
* Clones the remote repository and then opens a connection to it.
* @throws GitAPIException
@@ -160,21 +171,6 @@ public class JGitEnvironmentRepository extends AbstractScmAccessor implements En
}
private synchronized Environment loadEnvironment(Git git, String application,
String profile, String label) throws GitAPIException {
NativeEnvironmentRepository environment = new NativeEnvironmentRepository(
getEnvironment());
git.getRepository().getConfig().setString("branch", label, "merge", label);
Ref ref = checkout(git, label);
if (shouldPull(git, ref)) {
pull(git, label, ref);
}
environment.setSearchLocations(getSearchLocations(getWorkingDirectory()));
Environment result = environment.findOne(application, profile, "");
result.setLabel(label);
return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(), getUri());
}
private Ref checkout(Git git, String label) throws GitAPIException {
CheckoutCommand checkout = git.checkout();
if (shouldTrack(git, label)) {
@@ -299,8 +295,8 @@ public class JGitEnvironmentRepository extends AbstractScmAccessor implements En
private void trackBranch(Git git, CheckoutCommand checkout, String label) {
checkout.setCreateBranch(true).setName(label)
.setUpstreamMode(SetupUpstreamMode.TRACK)
.setStartPoint("origin/" + label);
.setUpstreamMode(SetupUpstreamMode.TRACK)
.setStartPoint("origin/" + label);
}
private boolean isBranch(Git git, String label) throws GitAPIException {

View File

@@ -79,6 +79,17 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
return this.repos;
}
@Override
public String[] getLocations(String application, String profile, String label) {
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
Environment source = repository.findOne(application, profile, label);
if (source != null) {
return repository.getLocations(application, profile, label);
}
}
return super.getLocations(application, profile, label);
}
@Override
public Environment findOne(String application, String profile, String label) {
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {

View File

@@ -47,7 +47,8 @@ import org.springframework.util.StringUtils;
* @author Roy Clarkson
*/
@ConfigurationProperties("spring.cloud.config.server.native")
public class NativeEnvironmentRepository implements EnvironmentRepository {
public class NativeEnvironmentRepository
implements EnvironmentRepository, ResourceLocationService {
private static Log logger = LogFactory.getLog(NativeEnvironmentRepository.class);
@@ -96,8 +97,8 @@ public class NativeEnvironmentRepository implements EnvironmentRepository {
String[] args = getArgs(config, label);
// Explicitly set the listeners (to exclude logging listener which would change
// log levels in the caller)
builder.application().setListeners(
Arrays.asList(new ConfigFileEnvironmentPostProcessor(),
builder.application()
.setListeners(Arrays.asList(new ConfigFileEnvironmentPostProcessor(),
new EnvironmentPostProcessingApplicationListener()));
ConfigurableApplicationContext context = builder.run(args);
environment.getPropertySources().remove("profiles");
@@ -110,13 +111,30 @@ public class NativeEnvironmentRepository implements EnvironmentRepository {
}
}
@Override
public String[] getLocations(String application, String profile, String label) {
String[] locations = this.searchLocations;
if (this.searchLocations == null) {
locations = DEFAULT_LOCATIONS;
}
List<String> output = new ArrayList<String>();
for (String location : locations) {
output.add(location);
}
for (String location : locations) {
if (isDirectory(location) && StringUtils.hasText(label)) {
output.add(location + label.trim() + "/");
}
}
return output.toArray(new String[0]);
}
private ConfigurableEnvironment getEnvironment(String profile) {
ConfigurableEnvironment environment = new StandardEnvironment();
environment.getPropertySources()
.addFirst(
new MapPropertySource("profiles", Collections
.<String, Object> singletonMap("spring.profiles.active",
profile)));
.addFirst(new MapPropertySource("profiles",
Collections.<String, Object> singletonMap(
"spring.profiles.active", profile)));
return environment;
}
@@ -134,18 +152,19 @@ public class NativeEnvironmentRepository implements EnvironmentRepository {
boolean matches = false;
String normal = name;
if (normal.startsWith("file:")) {
normal = StringUtils.cleanPath(new File(normal.substring("file:"
.length())).getAbsolutePath());
normal = StringUtils
.cleanPath(new File(normal.substring("file:".length()))
.getAbsolutePath());
}
for (String pattern : StringUtils
.commaDelimitedListToStringArray(getLocations(
this.searchLocations, result.getLabel()))) {
for (String pattern : getLocations(null, null, result.getLabel())) {
if (!pattern.contains(":")) {
pattern = "file:" + pattern;
}
if (pattern.startsWith("file:")) {
pattern = StringUtils.cleanPath(new File(pattern
.substring("file:".length())).getAbsolutePath()) + "/";
pattern = StringUtils
.cleanPath(new File(pattern.substring("file:".length()))
.getAbsolutePath())
+ "/";
}
if (logger.isTraceEnabled()) {
logger.trace("Testing pattern: " + pattern
@@ -179,27 +198,10 @@ public class NativeEnvironmentRepository implements EnvironmentRepository {
list.add("--spring.config.name=" + config);
list.add("--spring.cloud.bootstrap.enabled=false");
list.add("--encrypt.failOnError=" + this.failOnError);
String[] locations = this.searchLocations;
if (this.searchLocations == null) {
locations = DEFAULT_LOCATIONS;
}
list.add("--spring.config.location=" + getLocations(locations, label));
list.add("--spring.config.location=" + StringUtils.arrayToCommaDelimitedString(getLocations(null, null, label)));
return list.toArray(new String[0]);
}
private String getLocations(String[] locations, String label) {
List<String> output = new ArrayList<String>();
for (String location : locations) {
output.add(location);
}
for (String location : locations) {
if (isDirectory(location) && StringUtils.hasText(label)) {
output.add(location + label.trim() + "/");
}
}
return StringUtils.collectionToCommaDelimitedString(output);
}
public String[] getSearchLocations() {
return this.searchLocations;
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2015 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.server;
/**
* @author Dave Syer
*
*/
public class NoSuchResourceException extends RuntimeException {
public NoSuchResourceException(String string) {
super(string);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2015 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.server;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/**
* An HTTP endpoint for serving up templated plain text resources from an underlying
* repository. Can be used to supply config files for consumption by a wide variety of
* applications and services. A {@link ResourceRepository} is used to locate a
* {@link Resource}, specific to an application, and the contents are transformed to text.
* Then an {@link EnvironmentController} is used to supply key-value pairs which are used
* to replace placeholders in the resource text.
*
* @author Dave Syer
*
*/
@RestController
@RequestMapping(method = RequestMethod.GET, value = "${spring.cloud.config.server.prefix:}")
public class ResourceController {
private ResourceRepository resourceRepository;
private EnvironmentController environmentController;
public ResourceController(ResourceRepository resourceRepository,
EnvironmentController environmentController) {
this.resourceRepository = resourceRepository;
this.environmentController = environmentController;
}
@RequestMapping("/{name}/{profile}/{label}/{path:.*}")
public synchronized String resolve(@PathVariable String name,
@PathVariable String profile, @PathVariable String label,
@PathVariable String path) throws IOException {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
new EnvironmentPropertySource(
this.environmentController.labelled(name, profile, label)));
String text = StreamUtils.copyToString(
this.resourceRepository.findOne(name, profile, label, path).getInputStream(),
Charset.forName("UTF-8"));
// Mask out escaped placeholders
text = text.replace("\\${", "$_{");
return environment.resolvePlaceholders(text).replace("$_{", "${");
}
@ExceptionHandler(NoSuchResourceException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public void notFound(NoSuchResourceException e) {
}
private static class EnvironmentPropertySource extends PropertySource<Environment> {
public EnvironmentPropertySource(Environment sources) {
super("cloudEnvironment", sources);
}
@Override
public Object getProperty(String name) {
for (org.springframework.cloud.config.environment.PropertySource source : getSource()
.getPropertySources()) {
Map<?, ?> map = source.getSource();
if (map.containsKey(name)) {
return map.get(name);
}
}
return null;
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2015 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.server;
/**
* @author Dave Syer
*
*/
public interface ResourceLocationService {
String[] getLocations(String application, String profile, String label);
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2015 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.server;
import org.springframework.core.io.Resource;
/**
* @author Dave Syer
*
*/
public interface ResourceRepository {
Resource findOne(String name, String profile, String label, String path);
}

View File

@@ -23,7 +23,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -42,22 +41,20 @@ import org.tmatesoft.svn.core.wc2.SvnUpdate;
* @author Roy Clarkson
*/
@ConfigurationProperties("spring.cloud.config.server.svn")
public class SvnKitEnvironmentRepository extends AbstractScmAccessor
public class SvnKitEnvironmentRepository extends AbstractScmEnvironmentRepository
implements EnvironmentRepository, InitializingBean {
private static Log logger = LogFactory.getLog(SvnKitEnvironmentRepository.class);
private static final String DEFAULT_LABEL = "trunk";
private EnvironmentCleaner cleaner = new EnvironmentCleaner();
@Override
public String getDefaultLabel() {
return DEFAULT_LABEL;
}
@Override
public Environment findOne(String application, String profile, String label) {
public String[] getLocations(String application, String profile, String label) {
SvnOperationFactory svnOperationFactory = new SvnOperationFactory();
if (hasText(getUsername())) {
svnOperationFactory
@@ -71,8 +68,7 @@ public class SvnKitEnvironmentRepository extends AbstractScmAccessor
else {
checkout(svnOperationFactory);
}
return this.cleaner.clean(loadEnvironment(application, profile, label),
getWorkingDirectory().toURI().toString(), getUri());
return getLocations(label);
}
catch (SVNException e) {
throw new IllegalStateException("Cannot checkout repository", e);
@@ -82,10 +78,7 @@ public class SvnKitEnvironmentRepository extends AbstractScmAccessor
}
}
private synchronized Environment loadEnvironment(String application, String profile,
String label) {
final NativeEnvironmentRepository environmentRepository = new NativeEnvironmentRepository(
getEnvironment());
private String[] getLocations(String label) {
String[] locations = getSearchLocations(getSvnPath(getWorkingDirectory(), label));
boolean exists = false;
for (String location : locations) {
@@ -100,8 +93,7 @@ public class SvnKitEnvironmentRepository extends AbstractScmAccessor
if (!exists) {
throw new NoSuchLabelException("No label found for: " + label);
}
environmentRepository.setSearchLocations(locations);
return environmentRepository.findOne(application, profile, label);
return locations;
}
private void checkout(SvnOperationFactory svnOperationFactory) throws SVNException {

View File

@@ -20,6 +20,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.cloud.config.server.ConfigServerProperties;
import org.springframework.cloud.config.server.EnvironmentController;
import org.springframework.cloud.config.server.EnvironmentRepository;
import org.springframework.cloud.config.server.ResourceController;
import org.springframework.cloud.config.server.ResourceRepository;
import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -35,6 +37,9 @@ public class ConfigServerMvcConfiguration {
@Autowired
private EnvironmentRepository repository;
@Autowired
private ResourceRepository resources;
@Autowired
private ConfigServerProperties server;
@@ -43,19 +48,25 @@ public class ConfigServerMvcConfiguration {
@Bean
public EnvironmentController environmentController() {
EnvironmentController controller = new EnvironmentController(repository, environmentEncryptor);
EnvironmentController controller = new EnvironmentController(this.repository, this.environmentEncryptor);
controller.setDefaultLabel(getDefaultLabel());
controller.setOverrides(server.getOverrides());
controller.setStripDocumentFromYaml(server.isStripDocumentFromYaml());
controller.setOverrides(this.server.getOverrides());
controller.setStripDocumentFromYaml(this.server.isStripDocumentFromYaml());
return controller;
}
@Bean
public ResourceController resourceController() {
ResourceController controller = new ResourceController(this.resources, environmentController());
return controller;
}
private String getDefaultLabel() {
if (StringUtils.hasText(server.getDefaultLabel())) {
return server.getDefaultLabel();
if (StringUtils.hasText(this.server.getDefaultLabel())) {
return this.server.getDefaultLabel();
}
else {
return repository.getDefaultLabel();
return this.repository.getDefaultLabel();
}
}
}

View File

@@ -50,11 +50,11 @@ public class EnvironmentRepositoryConfiguration {
protected static class NativeRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
private ConfigurableEnvironment environment;
@Bean
public EnvironmentRepository environmentRepository() {
return new NativeEnvironmentRepository(environment);
return new NativeEnvironmentRepository(this.environment);
}
}
@@ -62,16 +62,16 @@ public class EnvironmentRepositoryConfiguration {
@Configuration
@ConditionalOnMissingBean(EnvironmentRepository.class)
protected static class GitRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
private ConfigurableEnvironment environment;
@Bean
public EnvironmentRepository environmentRepository() {
return new MultipleJGitEnvironmentRepository(environment);
return new MultipleJGitEnvironmentRepository(this.environment);
}
}
@Configuration
@Profile("subversion")
protected static class SvnRepositoryConfiguration {
@@ -80,7 +80,7 @@ public class EnvironmentRepositoryConfiguration {
@Bean
public EnvironmentRepository environmentRepository() {
return new SvnKitEnvironmentRepository(environment);
return new SvnKitEnvironmentRepository(this.environment);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2014 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.server.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.config.server.ConfigServerProperties;
import org.springframework.cloud.config.server.GenericResourceRepository;
import org.springframework.cloud.config.server.ResourceLocationService;
import org.springframework.cloud.config.server.ResourceRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnMissingBean(ResourceRepository.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
public class ResourceRepositoryConfiguration {
@Bean
public ResourceRepository resourceRepository(ResourceLocationService service) {
return new GenericResourceRepository(service);
}
}

View File

@@ -1,12 +1,16 @@
package org.springframework.cloud.config.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString;
import java.io.IOException;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -20,15 +24,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@IntegrationTest("server.port:0")
@@ -50,27 +50,34 @@ public class ConfigClientOffIntegrationTests {
@Test
public void contextLoads() {
Environment environment = new TestRestTemplate().getForObject("http://localhost:"
+ port + "/foo/development/", Environment.class);
+ this.port + "/foo/development/", Environment.class);
assertTrue(environment.getPropertySources().isEmpty());
}
@Test
public void configClientDisabled() throws Exception {
assertEquals(0, BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
assertEquals(0, BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.context,
ConfigServicePropertySourceLocator.class).length);
}
@Configuration
@Import(ConfigServerApplication.class)
protected static class TestConfiguration {
@Bean
public EnvironmentRepository environmentRepository() {
EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);
given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", ""));
return repository;
}
@Bean
public ResourceRepository resourceRepository() {
ResourceRepository repository = Mockito.mock(ResourceRepository.class);
given(repository.findOne(anyString(), anyString(), anyString(), anyString())).willReturn(new ByteArrayResource("".getBytes()));
return repository;
}
}
}

View File

@@ -1,12 +1,16 @@
package org.springframework.cloud.config.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString;
import java.io.IOException;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -20,15 +24,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@IntegrationTest({ "server.port:0", "spring.cloud.config.enabled:true" })
@@ -50,27 +50,34 @@ public class ConfigClientOnIntegrationTests {
@Test
public void contextLoads() {
Environment environment = new TestRestTemplate().getForObject("http://localhost:"
+ port + "/foo/development/", Environment.class);
+ this.port + "/foo/development/", Environment.class);
assertTrue(environment.getPropertySources().isEmpty());
}
@Test
public void configClientEnabled() throws Exception {
assertEquals(1, BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
assertEquals(1, BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.context,
ConfigServicePropertySourceLocator.class).length);
}
@Configuration
@Import(ConfigServerApplication.class)
protected static class TestConfiguration {
@Bean
public EnvironmentRepository environmentRepository() {
EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);
given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", ""));
return repository;
}
@Bean
public ResourceRepository resourceRepository() {
ResourceRepository repository = Mockito.mock(ResourceRepository.class);
given(repository.findOne(anyString(), anyString(), anyString(), anyString())).willReturn(new ByteArrayResource("".getBytes()));
return repository;
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2015 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.server;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author Dave Syer
*
*/
public class GenericResourceRepositoryTests {
private GenericResourceRepository repository;
private ConfigurableApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Before
public void init() {
this.context = new SpringApplicationBuilder(
NativeEnvironmentRepositoryTests.class).web(false).run();
this.repository = new GenericResourceRepository(
new NativeEnvironmentRepository(this.context.getEnvironment()));
this.repository.setResourceLoader(this.context);
this.context.close();
}
@Test
public void locateResource() {
assertNotNull(this.repository.findOne("blah", "default", "master", "foo.properties"));
}
@Test
public void locateProfiledResource() {
assertNotNull(this.repository.findOne("blah", "local", "master", "foo.txt"));
}
@Test(expected=NoSuchResourceException.class)
public void locateMissingResource() {
assertNotNull(this.repository.findOne("blah", "default", "master", "foo.txt"));
}
}

View File

@@ -71,7 +71,7 @@ public class NativeEnvironmentRepositoryTests {
this.repository.setSearchLocations("classpath:/test");
Environment environment = this.repository.findOne("foo", "development", "dev");
assertEquals(3, environment.getPropertySources().size());
// position 1 because it has higher precendence than anything except the
// position 1 because it has higher precedence than anything except the
// foo-development.properties
assertEquals("dev_bar",
environment.getPropertySources().get(1).getSource().get("foo"));

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2015 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.server;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author Dave Syer
*
*/
public class ResourceControllerTests {
private ResourceController controller;
private GenericResourceRepository repository;
private ConfigurableApplicationContext context;
private NativeEnvironmentRepository environmentRepository;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Before
public void init() {
this.context = new SpringApplicationBuilder(
NativeEnvironmentRepositoryTests.class).web(false).run();
this.environmentRepository = new NativeEnvironmentRepository(
this.context.getEnvironment());
this.repository = new GenericResourceRepository(this.environmentRepository);
this.repository.setResourceLoader(this.context);
this.controller = new ResourceController(this.repository,
new EnvironmentController(this.environmentRepository));
this.context.close();
}
@Test
public void templateReplacement() throws Exception {
this.environmentRepository.setSearchLocations("classpath:/test");
String resource = this.controller.resolve("foo", "bar", "dev", "template.json");
assertEquals("{\n \"foo\": \"dev_bar\"\n}", resource);
}
@Test
public void escapedPlaceholder() throws Exception {
this.environmentRepository.setSearchLocations("classpath:/test");
String resource = this.controller.resolve("foo", "bar", "dev", "placeholder.txt");
assertEquals("foo: ${foo}", resource);
}
}

View File

@@ -5,7 +5,4 @@ spring:
git:
basedir: target/config
overrides:
spring:
cloud:
config:
enabled: true
spring.cloud.config.enabled: true

View File

@@ -0,0 +1 @@
foo: local

View File

@@ -0,0 +1 @@
foo: \${foo}

View File

@@ -0,0 +1,3 @@
{
"foo": "${foo}"
}