diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 0952753e..063f1925 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -3,6 +3,7 @@ :githubmaster: {github}/tree/master :docslink: {githubmaster}/docs/src/main/asciidoc :toc: +:nofooter: include::intro.adoc[] @@ -31,8 +32,8 @@ public class ConfigServer { Like all Spring Boot apps it runs on port 8080 by default, but you can switch it to the conventional port 8888 in various ways. The -easiest, which also sets a default configuration repository, -is by launching it with `spring.config.name=configserver` (there +easiest, which also sets a default configuration repository, +is by launching it with `spring.config.name=configserver` (there is a `configserver.yml` in the Config Server jar). Another is to use your own `application.properties`, e.g. @@ -46,12 +47,12 @@ spring.cloud.config.server.git.uri: file://${user.home}/config-repo where `${user.home}/config-repo` is a git repository containing YAML and properties files. -NOTE: in Windows you need an extra "/" in the file URL if it is +NOTE: in Windows you need an extra "/" in the file URL if it is absolute with a drive prefix, e.g. `file:///${user.home}/config-repo`. [TIP] ==== -Here's a recipe for creating the git repository in the example +Here's a recipe for creating the git repository in the example above: ---- @@ -66,7 +67,7 @@ $ git commit -m "Add application.properties" ==== WARNING: using the local filesystem for your git repository is -intended for testing only. Use a server to host your +intended for testing only. Use a server to host your configuration repositories in production. WARNING: the initial clone of your configuration repository will @@ -87,7 +88,7 @@ Server? The strategy that governs this behaviour is the * `{application}` maps to "spring.application.name" on the client side; -* `{profile}` maps to "spring.profiles.active" on the client (comma separated list); and +* `{profile}` maps to "spring.profiles.active" on the client (comma separated list); and * `{label}` which is a server side feature labelling a "versioned" set of config files. @@ -112,7 +113,7 @@ spring: ---- (as usual with a Spring Boot application, these properties could also -be set as environment variables or command line arguments). +be set as environment variables or command line arguments). If the repository is file-based, the server will create an `Environment` from `application.yml` (shared between all clients), and @@ -265,7 +266,7 @@ In this example the server searches for config files in the top level and in the "foo/" sub-directory and also any sub-directory whose name begins with "bar". -By default the server clones remote repositories when configuration +By default the server clones remote repositories when configuration is first requested. The server can be configured to clone the repositories at startup. For example at the top level: @@ -289,11 +290,11 @@ spring: team-c: pattern: team-c-* uri: http://git/team-a/config-repo.git - + ---- -In this example the server clones team-a's config-repo on startup before it -accepts any requests. All other repositories will not be cloned until +In this example the server clones team-a's config-repo on startup before it +accepts any requests. All other repositories will not be cloned until configuration from the repository is requested. NOTE: Setting a repository to be cloned when the Config Server starts up can @@ -305,7 +306,7 @@ not detect an error until an application requests configuration from that configuration source. To use HTTP basic authentication on the remote repository add the -"username" and "password" properties separately (not in the URL), +"username" and "password" properties separately (not in the URL), e.g. [source,yaml] @@ -352,8 +353,8 @@ spring: ---- searches the repository for files in the same name as the directory -(as well as the top level). Wildcards are also valid in a search -path with placeholders (any matching directory is included in the +(as well as the top level). Wildcards are also valid in a search +path with placeholders (any matching directory is included in the search). ==== Version Control Backend Filesystem Use @@ -367,7 +368,7 @@ Git, but just loads the config files from the local classpath or file system (any static URL you want to point to with "spring.cloud.config.server.native.searchLocations"). To use the native profile just launch the Config Server with -"spring.profiles.active=native". +"spring.profiles.active=native". NOTE: Remember to use the `file:` prefix for file resources (the default without a prefix is usually the classpath). Just as with any @@ -383,7 +384,7 @@ property sources present in the server are removed before being sent to the client. TIP: A filesystem backend is great for getting started quickly and -for testing. To use it in production you need to be sure that the +for testing. To use it in production you need to be sure that the file system is reliable, and shared across all instances of the Config Server. @@ -622,7 +623,7 @@ You can disable the Health Indicator by setting `spring.cloud.config.server.heal You are free to secure your Config Server in any way that makes sense to you (from physical network security to OAuth2 bearer tokens), and Spring Security and Spring Boot make it easy to do pretty -much anything. +much anything. To use the default Spring Boot configured HTTP Basic security, just include Spring Security on the classpath (e.g. through @@ -699,14 +700,14 @@ are special characters ('+' is particularly tricky). Take the encrypted value and add the `{cipher}` prefix before you put it in the YAML or properties file, and before you commit and push it -to a remote, potentially insecure store. +to a remote, potentially insecure store. The `/encrypt` and `/decrypt` endpoints also both accept paths of the form `/*/{name}/{profiles}` which can be used to control cryptography per application (name) and profile when clients call into the main Environment resource. -NOTE: to control the cryptography in this granular way you must also +NOTE: to control the cryptography in this granular way you must also provide a `@Bean` of type `TextEncryptorLocator` that creates a different encryptor per name and profiles. The one that is provided by default does not do this (so all encryptions use the same key). @@ -747,8 +748,8 @@ PEM-encoded text value (in `encrypt.key`), or via a keystore (e.g. as created by the `keytool` utility that comes with the JDK). The keystore properties are `encrypt.keyStore.\*` with `*` equal to -* `location` (a `Resource` location), -* `password` (to unlock the keystore) and +* `location` (a `Resource` location), +* `password` (to unlock the keystore) and * `alias` (to identify which key in the store is to be used). @@ -1082,7 +1083,7 @@ an Exception. === Config Client Retry If you expect that the config server may occasionally be unavailable when -your app starts, you can ask it to keep trying after a failure. First you need +your app starts, you can ask it to keep trying after a failure. First you need to set `spring.cloud.config.failFast=true`, and then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath. The default behaviour is to retry 6 times with an initial backoff interval of 1000ms and an @@ -1105,10 +1106,10 @@ All of them can be overridden by setting `spring.cloud.config.\*` (where `*` is "name", "profile" or "label"). The "label" is useful for rolling back to previous versions of configuration; with the default Config Server implementation it can be a git label, branch name or -commit id. Label can also be provided as a comma-separated list, in +commit id. Label can also be provided as a comma-separated list, in which case the items in the list are tried on-by-one until one succeeds. This can be useful when working on a feature branch, for instance, -when you might want to align the config label with your branch, but +when you might want to align the config label with your branch, but make it optional (e.g. `spring.cloud.config.label=myfeature,develop`). === Security diff --git a/mvnw b/mvnw index 53c0d721..fe1957e3 100755 --- a/mvnw +++ b/mvnw @@ -46,15 +46,6 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then fi -VERSION=$(awk '/ 0) {$0=$0} 1' `dirname $0`/pom.xml| grep '\(.*\)<.*/\1/') -if echo $VERSION | egrep -q 'M|RC'; then - echo Activating \"milestone\" profile for version=\"$VERSION\" - echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pmilestone" -else - echo Deactivating \"milestone\" profile for version=\"$VERSION\" - echo $MAVEN_ARGS | grep -q milestone && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pmilestone//') -fi - # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; @@ -66,27 +57,27 @@ case "`uname`" in # # Look for the Apple JDKs first to preserve the existing behaviour, and then look # for the new JDKs provided by Oracle. - # + # if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then # # Apple JDKs # export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home fi - + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then # # Apple JDKs # export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home fi - + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then # # Oracle JDKs # export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi + fi if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then # @@ -228,16 +219,30 @@ concat_lines() { export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" -# Provide a "standardized" way to retrieve the CLI args that will +# Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +VERSION=$(exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + -Dexec.executable="echo" -Dexec.args='${project.version}' ${WRAPPER_LAUNCHER} -q --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec ) + +if echo $VERSION | egrep -q 'M|RC'; then + echo Activating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pmilestone" +else + echo Deactivating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pmilestone//') +fi + + exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} ${MAVEN_ARGS} "$@" - 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 890f5347..fefdc8b1 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 @@ -24,14 +24,18 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; +import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ListBranchCommand; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.ResetCommand; +import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.StatusCommand; import org.eclipse.jgit.api.TransportCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.RefNotFoundException; @@ -55,6 +59,7 @@ import com.jcraft.jsch.Session; * @author Dave Syer * @author Roy Clarkson * @author Marcos Barbero + * @author Daniel Lavoie */ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository implements EnvironmentRepository, SearchPathLocator, InitializingBean { @@ -166,6 +171,14 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository Ref ref = checkout(git, label); if (shouldPull(git, ref)) { pull(git, label, ref); + + if (!isClean(git)) { + logger.warn("The local repository is dirty. Reseting it to origin/" + + label + "."); + + fetch(git, label, "origin"); + resetHard(git, label, "refs/remotes/origin/" + label); + } } return ref; } @@ -264,6 +277,36 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository return isBranch(git, label) && !isLocalBranch(git, label); } + private void fetch(Git git, String label, String remote) { + FetchCommand fetch = git.fetch().setRemote(remote); + setTimeout(fetch); + try { + if (hasText(getUsername())) { + setCredentialsProvider(fetch); + } + + fetch.call(); + } + catch (Exception ex) { + this.logger.warn("Could not fetch remote for " + label + " remote: " + git + .getRepository().getConfig().getString("remote", "origin", "url")); + } + } + + private void resetHard(Git git, String label, String ref) { + ResetCommand reset = git.reset(); + reset.setRef(ref); + reset.setMode(ResetType.HARD); + try { + reset.call(); + } + catch (Exception ex) { + this.logger.warn("Could not reset to remote for " + label + " (current ref=" + + ref + "), remote: " + git.getRepository().getConfig() + .getString("remote", "origin", "url")); + } + } + /** * Assumes we are on a tracking branch (should be safe) */ @@ -369,6 +412,20 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository pull.setTimeout(this.timeout); } + private boolean isClean(Git git) { + StatusCommand status = git.status(); + try { + return status.call().isClean(); + } + catch (Exception e) { + this.logger + .warn("Could not execute status command on local repository. Cause: (" + + e.getClass().getSimpleName() + ") " + e.getMessage()); + + return false; + } + } + private void trackBranch(Git git, CheckoutCommand checkout, String label) { checkout.setCreateBranch(true).setName(label) .setUpstreamMode(SetupUpstreamMode.TRACK) diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/test/ConfigServerTestUtils.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/test/ConfigServerTestUtils.java index 184aaec0..80b4d520 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/test/ConfigServerTestUtils.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/test/ConfigServerTestUtils.java @@ -15,6 +15,9 @@ */ package org.springframework.cloud.config.server.test; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.StringUtils; @@ -24,8 +27,22 @@ import java.io.IOException; /** * @author Dave Syer + * @author Daniel Lavoie */ public class ConfigServerTestUtils { + public static Repository prepareBareRemote() throws IOException { + // Create a folder in the temp folder that will act as the remote repository + File remoteDir = File.createTempFile("remote", ""); + remoteDir.delete(); + remoteDir.mkdirs(); + + // Create a bare repository + FileKey fileKey = FileKey.exact(remoteDir, FS.DETECTED); + Repository remoteRepo = fileKey.open(false); + remoteRepo.create(true); + + return remoteRepo; + } public static String prepareLocalRepo() throws IOException { return prepareLocalRepo("./", "target/repos", "config-repo", "target/config"); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java index 263856e9..e6a6b940 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java @@ -26,12 +26,21 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.Arrays; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.util.FileUtils; import org.hamcrest.Matchers; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; @@ -48,6 +57,7 @@ import org.springframework.util.StreamUtils; /** * @author Dave Syer * @author Roy Clarkson + * @author Daniel Lavoie */ public class JGitEnvironmentRepositoryIntegrationTests { @@ -108,6 +118,67 @@ public class JGitEnvironmentRepositoryIntegrationTests { environment.getPropertySources().get(0).getSource().get("foo")); } + /** + * Tests a special use case where the remote repository has been updated with a forced + * push conflicting with the local repo of the Config Server. The Config Server has to + * reset hard on the new reference because a simple pull operation could result in a + * conflicting local repository. + */ + @Test + public void pullDirtyRepo() throws Exception { + ConfigServerTestUtils.prepareLocalRepo(); + String uri = ConfigServerTestUtils.copyLocalRepo("config-copy"); + + // Create a remote bare repository. + Repository remote = ConfigServerTestUtils.prepareBareRemote(); + + Git git = Git.open(ResourceUtils.getFile(uri).getAbsoluteFile()); + StoredConfig config = git.getRepository().getConfig(); + config.setString("remote", "origin", "url", + remote.getDirectory().getAbsolutePath()); + config.setString("remote", "origin", "fetch", + "+refs/heads/*:refs/remotes/origin/*"); + config.save(); + + // Pushes the raw branch to remote repository. + git.push().call(); + + String commitToRevertBeforePull = git.log().setMaxCount(1).call().iterator() + .next().getName(); + + this.context = new SpringApplicationBuilder(TestConfiguration.class).web(false) + .run("--spring.cloud.config.server.git.uri=" + uri); + + JGitEnvironmentRepository repository = this.context + .getBean(JGitEnvironmentRepository.class); + + // Fetches the repository for the first time. + repository.getLocations("bar", "test", "raw"); + + // Resets to the original commit. + git.reset().setMode(ResetType.HARD).setRef("master").call(); + + // Generate a conflicting commit who will be forced on the origin. + Path applicationFilePath = Paths + .get(ResourceUtils.getFile(uri).getAbsoluteFile() + "/application.yml"); + + Files.write(applicationFilePath, + Arrays.asList("info:", " foo: bar", "raw: false"), + StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Conflicting commit.").call(); + git.push().setForce(true).call(); + + // Reset to the raw branch. + git.reset().setMode(ResetType.HARD).setRef(commitToRevertBeforePull).call(); + + // Triggers the repository refresh. + repository.getLocations("bar", "test", "raw"); + + Assert.assertTrue("Local repository is not cleaned after retreiving resources.", + git.status().call().isClean()); + } + @Test public void nested() throws IOException { String uri = ConfigServerTestUtils.prepareLocalRepo("another-config-repo");