Merge remote-tracking branch 'origin/2.0.x'

This commit is contained in:
Ryan Baxter
2018-11-19 16:28:48 -05:00
10 changed files with 310 additions and 10 deletions

View File

@@ -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 <<composite-environment-repositories,composite environment repository configuration>>.
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`.

View File

@@ -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<IllegalStateException> {
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;
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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("");
}
}

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,5 @@
spring:
cloud:
config:
server:
bootstrap: true