diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index fdb02a84..27c800b9 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -503,6 +503,14 @@ spring: NOTE: The default value for `deleteUntrackedBranches` property is `false`. +===== Git Refresh Rate + +You can control how often the config server will fetch updated configuration data +from your Git backend by using `spring.cloud.config.server.git.refreshRate`. The +value of this property is specified in seconds. By default the value is 0, meaning +the config server will fetch updated configuration from the Git repo every time it +is requested. + ==== Version Control Backend Filesystem Use WARNING: With VCS-based backends (git, svn), files are checked out or cloned to the local filesystem. @@ -545,7 +553,7 @@ Vault is a tool for securely accessing secrets. A secret is anything that to which you want to tightly control access, such as API keys, passwords, certificates, and other sensitive information. Vault provides a unified interface to any secret while providing tight access control and recording a detailed audit log. **** -For more information on Vault, see the https://www.vaultproject.io/intro/index.html[Vault quick start guide]. +For more information on Vault, see the https://learn.hashicorp.com/vault/?track=getting-started#getting-started[Vault quick start guide]. To enable the config server to use a Vault backend, you can run your config server with the `vault` profile. For example, in your config server's `application.properties`, you can add `spring.profiles.active=vault`. @@ -601,8 +609,8 @@ First, place some data in you Vault, as shown in the following example: [source,sh] ---- -$ vault write secret/application foo=bar baz=bam -$ vault write secret/myapp foo=myappsbar +$ vault kv put secret/application foo=bar baz=bam +$ vault kv put secret/myapp foo=myappsbar ---- Second, make an HTTP request to your config server to retrieve the values, as shown in the following example: @@ -963,8 +971,11 @@ The asymmetric choice is superior in terms of security, but it is often more con To configure a symmetric key, you need to set `encrypt.key` to a secret String (or use the `ENCRYPT_KEY` environment variable to keep it out of plain-text configuration files). -To configure an asymmetric key, you can either set the key as a PEM-encoded text value (in `encrypt.key`) or use a keystore (such as the keystore created by the `keytool` utility that comes with the JDK). -The following table describes the keystore properties: +NOTE: You cannot configure an asymmetric key using `encrypt.key`. + +To configure an asymmetric key use a keystore (e.g. as +created by the `keytool` utility that comes with the JDK). The +keystore properties are `encrypt.keyStore.\*` with `*` equal to [options="header"] |=== @@ -1122,6 +1133,24 @@ An optional property named `spring.cloud.config.server.bootstrap` can be useful It is a flag to indicate whether the server should configure itself from its own remote repository. By default, the flag is off, because it can delay startup. However, when embedded in another application, it makes sense to initialize the same way as any other application. +When setting `spring.cloud.config.server.bootstrap` to `true` you must also use a <>. +For example + +[source,yaml] +---- +spring: + application: + name: configserver + profiles: + active: composite + cloud: + config: + server: + composite: + - type: native + search-locations: ${HOME}/Desktop/config + bootstrap: true +---- NOTE: If you use the bootstrap flag, the config server needs to have its name and repository URI configured in `bootstrap.yml`. diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/diagnostics/GitUriFailureAnalyzer.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/diagnostics/GitUriFailureAnalyzer.java new file mode 100644 index 00000000..1fc04280 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/diagnostics/GitUriFailureAnalyzer.java @@ -0,0 +1,25 @@ +package org.springframework.cloud.config.server.diagnostics; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.cloud.config.server.environment.JGitEnvironmentRepository; + +/** + * @author Ryan Baxter + */ +public class GitUriFailureAnalyzer extends AbstractFailureAnalyzer { + + public static final String DESCRIPTION = "Invalid config server configuration."; + public static final String ACTION = "If you are using the git profile, you need to set a Git URI in your " + + "configuration. If you are using a native profile and have spring.cloud.config.server.bootstrap=true, " + + "you need to use a composite configuration."; + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, IllegalStateException cause) { + if(JGitEnvironmentRepository.MESSAGE.equalsIgnoreCase(cause.getMessage())) { + return new FailureAnalysis(DESCRIPTION, ACTION, cause); + } + return null; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java index 214fe5fb..e7c5ab5a 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java @@ -80,6 +80,8 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository implements EnvironmentRepository, SearchPathLocator, InitializingBean { + public static final String MESSAGE = "You need to configure a uri for the git repository."; + private static final String FILE_URI_PREFIX = "file:"; private static final String LOCAL_BRANCH_REF_PREFIX = "refs/remotes/origin/"; @@ -241,9 +243,9 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository } @Override - public void afterPropertiesSet() throws Exception { + public synchronized void afterPropertiesSet() throws Exception { Assert.state(getUri() != null, - "You need to configure a uri for the git repository"); + MESSAGE); initialize(); if (this.cloneOnStart) { initClonedRepository(); @@ -572,6 +574,7 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository return clone.call(); } catch (GitAPIException e) { + logger.warn("Error occured cloning to base directory.", e); deleteBaseDirIfExists(); throw e; } @@ -683,7 +686,7 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and * {@link org.eclipse.jgit.api.CloneCommand} allowing for easier unit testing. */ - static class JGitFactory { + public static class JGitFactory { public Git getGitByOpen(File file) throws IOException { Git git = Git.open(file); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactory.java index d8daa2b5..42ea7e8e 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactory.java @@ -34,7 +34,9 @@ public class NativeEnvironmentRepositoryFactory implements EnvironmentRepository @Override public NativeEnvironmentRepository build(NativeEnvironmentProperties environmentProperties) { NativeEnvironmentRepository repository = new NativeEnvironmentRepository(environment, environmentProperties); - repository.setDefaultLabel(properties.getDefaultLabel()); + if(properties.getDefaultLabel() != null) { + repository.setDefaultLabel(properties.getDefaultLabel()); + } return repository; } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GitSkipSslValidationCredentialsProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GitSkipSslValidationCredentialsProvider.java index c3d8a61a..955fc184 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GitSkipSslValidationCredentialsProvider.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/GitSkipSslValidationCredentialsProvider.java @@ -17,6 +17,7 @@ package org.springframework.cloud.config.server.support; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.internal.JGitText; @@ -37,6 +38,8 @@ import org.eclipse.jgit.transport.URIish; */ public class GitSkipSslValidationCredentialsProvider extends CredentialsProvider { + private static final Pattern FORMAT_PLACEHOLDER_PATTERN = Pattern.compile("\\s*\\{\\d}\\s*"); + private final CredentialsProvider delegate; public GitSkipSslValidationCredentialsProvider(CredentialsProvider delegate) { @@ -128,6 +131,6 @@ public class GitSkipSslValidationCredentialsProvider extends CredentialsProvider } private static String stripFormattingPlaceholders(String string) { - return string.replaceAll("\\s*\\{\\d}\\s*", ""); + return FORMAT_PLACEHOLDER_PATTERN.matcher(string).replaceAll(""); } } diff --git a/spring-cloud-config-server/src/main/resources/META-INF/spring.factories b/spring-cloud-config-server/src/main/resources/META-INF/spring.factories index f145f03c..4c3a8b2e 100644 --- a/spring-cloud-config-server/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-config-server/src/main/resources/META-INF/spring.factories @@ -11,3 +11,6 @@ org.springframework.cloud.config.server.bootstrap.ConfigServerBootstrapApplicati org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration,\ org.springframework.cloud.config.server.config.EncryptionAutoConfiguration + +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.cloud.config.server.diagnostics.GitUriFailureAnalyzer diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/NativeBootstrapFailureAnalyzerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/NativeBootstrapFailureAnalyzerTests.java new file mode 100644 index 00000000..5666ed40 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/NativeBootstrapFailureAnalyzerTests.java @@ -0,0 +1,34 @@ +package org.springframework.cloud.config.server; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.config.server.diagnostics.GitUriFailureAnalyzer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Ryan Baxter + */ +public class NativeBootstrapFailureAnalyzerTests { + + @Rule + public OutputCapture outputCapture = new OutputCapture(); + + @Test + public void contextLoads(){ + try { + new SpringApplicationBuilder(ConfigServerApplication.class) + .web(WebApplicationType.SERVLET).properties("spring.cloud.bootstrap.name:enable-nativebootstrap").profiles("test","native").run(); + fail("Application started successfully"); + } + catch (Exception ex) { + assertThat(this.outputCapture.toString()) + .contains(GitUriFailureAnalyzer.ACTION); + assertThat(this.outputCapture.toString()).contains(GitUriFailureAnalyzer.DESCRIPTION); + } + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryConcurrencyTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryConcurrencyTests.java index 4a8e802e..40c5e7ac 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryConcurrencyTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryConcurrencyTests.java @@ -17,12 +17,24 @@ package org.springframework.cloud.config.server.environment; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.FetchCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.*; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.util.FileUtils; import org.junit.After; import org.junit.Before; @@ -96,6 +108,63 @@ public class JGitEnvironmentRepositoryConcurrencyTests { assertEquals("master", environment.getLabel()); } + protected Log logger = LogFactory.getLog(getClass()); + + /** + * Simulates following actions in parallel: + * - Client tries to obtain configuration with specified label + * - Spring Refresh Context Event occurs + */ + @Test + public void concurrentRefreshContextAndGetLabels() throws Exception { + // Prepare the repo + final JGitConfigServerTestData testData = JGitConfigServerTestData.prepareClonedGitRepository(TestConfiguration.class); + JGitEnvironmentRepository repository = testData.getRepository(); + repository.setCloneOnStart(true); + repository.setGitFactory(new DelayedGitFactoryMock()); + repository.setBasedir(testData.getClonedGit().getGitWorkingDirectory()); + repository.setUri(testData.getServerGit().getGitWorkingDirectory().getAbsolutePath().replace("file://", "")); + + final AtomicInteger errorCount = new AtomicInteger(); + + // Prepare two threads to do the parallel work + Thread client = new Thread(new Runnable() { + @Override + public void run() { + logger.info("client start."); + try { + Environment environment = testData.getRepository().findOne("bar", "staging", "master"); + } catch (Exception e) { + errorCount.incrementAndGet(); + e.printStackTrace(); + } + logger.info("client end."); + } + }); + + Thread refresh = new Thread(new Runnable() { + @Override + public void run() { + try { + logger.info("refresh start."); + testData.getRepository().afterPropertiesSet(); + logger.info("refresh end."); + } catch (Exception e) { + errorCount.incrementAndGet(); + e.printStackTrace(); + } + } + }); + + // Start the parallel actions and wait till the end. + refresh.start(); + client.start(); + refresh.join(); + client.join(); + + assertEquals(0, errorCount.get()); + } + @Configuration @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ PropertyPlaceholderAutoConfiguration.class, @@ -103,4 +172,80 @@ public class JGitEnvironmentRepositoryConcurrencyTests { protected static class TestConfiguration { } + private static class DelayedGitFactoryMock extends JGitEnvironmentRepository.JGitFactory { + + @Override + public Git getGitByOpen(File file) throws IOException { + Git originalGit = DelayedGitMock.open(file); + return new DelayedGitMock(originalGit.getRepository()); + } + + @Override + public CloneCommand getCloneCommandByCloneRepository() { + return new DelayedCloneCommand(); + } + } + + private static class DelayedGitMock extends Git { + + public DelayedGitMock(Repository repo) { + super(repo); + } + + @Override + public FetchCommand fetch() { + return new DelayedFetchCommand(getRepository()); + } + + @Override + public CheckoutCommand checkout() { + return new DelayedCheckoutCommand(getRepository()); + } + } + + private static class DelayedCloneCommand extends CloneCommand { + @Override + public Git call() throws GitAPIException, InvalidRemoteException, TransportException { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return super.call(); + } + } + + private static class DelayedFetchCommand extends FetchCommand { + + public DelayedFetchCommand(Repository repo) { + super(repo); + } + + @Override + public FetchResult call() throws GitAPIException, InvalidRemoteException, TransportException { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return super.call(); + } + } + + private static class DelayedCheckoutCommand extends CheckoutCommand { + public DelayedCheckoutCommand(Repository repo) { + super(repo); + } + + @Override + public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException, CheckoutConflictException { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return super.call(); + } + } + } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactoryTest.java new file mode 100644 index 00000000..3c7ce758 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryFactoryTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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.environment; + +import org.junit.Test; +import org.springframework.cloud.config.server.config.ConfigServerProperties; +import org.springframework.core.env.StandardEnvironment; + +import static org.junit.Assert.assertEquals; + + +/** + * @author Ryan Baxter + */ +public class NativeEnvironmentRepositoryFactoryTest { + + @Test + public void testDefaultLabel() { + ConfigServerProperties props = new ConfigServerProperties(); + props.setDefaultLabel("mylabel"); + NativeEnvironmentRepositoryFactory factory = new NativeEnvironmentRepositoryFactory(new StandardEnvironment(), props); + NativeEnvironmentProperties environmentProperties = new NativeEnvironmentProperties(); + NativeEnvironmentRepository repo = factory.build(environmentProperties); + assertEquals("mylabel", repo.getDefaultLabel()); + + factory = new NativeEnvironmentRepositoryFactory(new StandardEnvironment(), props); + environmentProperties = new NativeEnvironmentProperties(); + environmentProperties.setDefaultLabel("mynewlabel"); + repo = factory.build(environmentProperties); + assertEquals("mylabel", repo.getDefaultLabel()); + + factory = new NativeEnvironmentRepositoryFactory(new StandardEnvironment(), new ConfigServerProperties()); + environmentProperties = new NativeEnvironmentProperties(); + environmentProperties.setDefaultLabel("mynewlabel"); + repo = factory.build(environmentProperties); + assertEquals("mynewlabel", repo.getDefaultLabel()); + } +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/resources/enable-nativebootstrap.yml b/spring-cloud-config-server/src/test/resources/enable-nativebootstrap.yml new file mode 100644 index 00000000..66226479 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/enable-nativebootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + server: + bootstrap: true