Push events for config changes
Works from local repos or with explicit POST to /monitor with path=<serviceId>, also supports webhooks from github and gitlab.
This commit is contained in:
@@ -489,7 +489,7 @@ 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.
|
||||
|
||||
=== Embedding the Config Server
|
||||
== Embedding the Config Server
|
||||
|
||||
The Config Server runs best as a standalone application, but if you
|
||||
need to you can embed it in another application. Just use the
|
||||
@@ -505,6 +505,41 @@ own remote repository. The flag is off by default because it can delay
|
||||
startup, but when embedded in another application it makes sense to
|
||||
initialize the same way as any other application.
|
||||
|
||||
== Push Notifications and Spring Cloud Bus
|
||||
|
||||
Many source code repository providers (like Github or Gitlab for
|
||||
instance) will notify you of changes in a repository through a
|
||||
webhook. You can configure the webhook via the provider's user
|
||||
interface as a URL and a set of events in which you are
|
||||
interested. For instance
|
||||
https://developer.github.com/v3/activity/events/types/#pushevent[Github]
|
||||
will POST to the webhook with a JSON body containing a list of
|
||||
commits, and a header "X-Github-Event" equal to "push". If you add a
|
||||
dependency on the `spring-cloud-config-monitor` library and activate
|
||||
the Spring Cloud Bus in your Config Server, then a "/monitor" endpoint
|
||||
is enabled.
|
||||
|
||||
When the webhook is activated the Config Server will send a
|
||||
`RefreshRemoteApplicationEvent` targeted at the applications it thinks
|
||||
might have changed. The change detection can be strategized, but by
|
||||
default it just looks for changes in files that match the application
|
||||
name (e.g. "foo.properties" is targeted at the "foo" application, and
|
||||
"application.properties" is targeted at all applications). The strategy if you want to override the behaviour is `PropertyPathNotificationExtractor` which accepts the request headers and body as parameters and returns a list of file paths that changed.
|
||||
|
||||
The default configuration works out of the box with Github or
|
||||
Gitlab. In addition to the JSON notifications from Github and Gitlab
|
||||
you can trigger a change notification by POSTing to "/monitor" with a
|
||||
form-encoded body parameters `path={name}`. This will broadcast to
|
||||
applications matching the "{name}" pattern (can contain wildcards).
|
||||
|
||||
NOTE: the `RefreshRemoteApplicationEvent` will only be transmitted if
|
||||
the `spring-cloud-bus` is activated in the Config Server and in the
|
||||
client application.
|
||||
|
||||
NOTE: the default configuration also detects filesystem changes in
|
||||
local git repositories (the webhook is not used in that case but as
|
||||
soon as you edit a config file a refresh will be broadcast).
|
||||
|
||||
== Spring Cloud Config Client
|
||||
|
||||
A Spring Boot application can take immediate advantage of the Spring
|
||||
|
||||
9
pom.xml
9
pom.xml
@@ -22,13 +22,15 @@
|
||||
</scm>
|
||||
<properties>
|
||||
<bintray.package>config</bintray.package>
|
||||
<spring-cloud-bus.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-bus.version>
|
||||
<spring-cloud-commons.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
|
||||
</properties>
|
||||
<modules>
|
||||
<module>spring-cloud-config-client</module>
|
||||
<module>spring-cloud-config-server</module>
|
||||
<module>spring-cloud-config-monitor</module>
|
||||
<module>spring-cloud-config-sample</module>
|
||||
<module>spring-cloud-starter-config</module>
|
||||
<module>spring-cloud-starter-config</module>
|
||||
<module>docs</module>
|
||||
</modules>
|
||||
<dependencyManagement>
|
||||
@@ -55,6 +57,11 @@
|
||||
<artifactId>spring-cloud-config-server</artifactId>
|
||||
<version>1.1.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-monitor</artifactId>
|
||||
<version>1.1.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
|
||||
49
spring-cloud-config-monitor/pom.xml
Normal file
49
spring-cloud-config-monitor/pom.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config</artifactId>
|
||||
<version>1.1.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-config-monitor</artifactId>
|
||||
<name>spring-cloud-config-monitor</name>
|
||||
<description>Spring Cloud Config Monitor</description>
|
||||
<properties>
|
||||
<main.basedir>${basedir}/../..</main.basedir>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-bus-parent</artifactId>
|
||||
<version>${spring-cloud-bus.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-bus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!-- Only needed at compile time -->
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* A {@link PropertyPathNotificationExtractor} that cycles through a set of (ordered) delegates,
|
||||
* looking for the first non-null outcome.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class CompositePropertyPathNotificationExtractor
|
||||
implements PropertyPathNotificationExtractor {
|
||||
|
||||
private List<PropertyPathNotificationExtractor> extractors;
|
||||
|
||||
public CompositePropertyPathNotificationExtractor(
|
||||
List<PropertyPathNotificationExtractor> extractors) {
|
||||
this.extractors = new ArrayList<>();
|
||||
if (extractors != null) {
|
||||
this.extractors.addAll(extractors);
|
||||
}
|
||||
this.extractors.add(new SimplePropertyPathNotificationExtractor());
|
||||
AnnotationAwareOrderComparator.sort(this.extractors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyPathNotification extract(MultiValueMap<String, String> headers,
|
||||
Map<String, Object> request) {
|
||||
for (PropertyPathNotificationExtractor extractor : this.extractors) {
|
||||
PropertyPathNotification result = extractor.extract(headers, request);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Order(Ordered.LOWEST_PRECEDENCE - 200)
|
||||
private static class SimplePropertyPathNotificationExtractor
|
||||
implements PropertyPathNotificationExtractor {
|
||||
|
||||
@Override
|
||||
public PropertyPathNotification extract(MultiValueMap<String, String> headers,
|
||||
Map<String, Object> request) {
|
||||
Object object = request.get("path");
|
||||
if (object instanceof String) {
|
||||
return new PropertyPathNotification((String) object);
|
||||
}
|
||||
if (object instanceof Collection) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<String> collection = (Collection<String>) object;
|
||||
return new PropertyPathNotification(collection.toArray(new String[0]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnWebApplication
|
||||
@Import(FileMonitorConfiguration.class)
|
||||
public class EnvironmentMonitorAutoConfiguration {
|
||||
|
||||
@Autowired(required=false)
|
||||
private List<PropertyPathNotificationExtractor> extractors;
|
||||
|
||||
@Bean
|
||||
public PropertyPathEndpoint propertyPathEndpoint() {
|
||||
return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(this.extractors));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardWatchEventKinds;
|
||||
import java.nio.file.WatchEvent;
|
||||
import java.nio.file.WatchKey;
|
||||
import java.nio.file.WatchService;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.config.server.AbstractScmEnvironmentRepository;
|
||||
import org.springframework.cloud.config.server.NativeEnvironmentRepository;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
/**
|
||||
* Configuration for a file watcher that detects changes in local files related to the
|
||||
* environment repository. If any files change the {@link PropertyPathEndpoint} is pinged
|
||||
* with the paths of the files. This applies to the source files of a local git repository
|
||||
* (i.e. a git repository with a "file:" URI) or to a native repository.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@CommonsLog
|
||||
@EnableScheduling
|
||||
public class FileMonitorConfiguration implements SmartLifecycle, ResourceLoaderAware {
|
||||
|
||||
@Autowired
|
||||
PropertyPathEndpoint endpoint;
|
||||
|
||||
@Autowired(required = false)
|
||||
AbstractScmEnvironmentRepository scmRepository;
|
||||
|
||||
@Autowired(required = false)
|
||||
NativeEnvironmentRepository nativeEnvironmentRepository;
|
||||
|
||||
private boolean running;
|
||||
|
||||
private WatchService watcher;
|
||||
|
||||
private Set<Path> directory;
|
||||
|
||||
private int phase;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private String[] excludes = new String[] { ".*", "#*", "*#" };
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return this.phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* see {@link #getPhase()}
|
||||
* @param phase the phase.
|
||||
*/
|
||||
public void setPhase(int phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #isRunning()
|
||||
* @param running true if running.
|
||||
*/
|
||||
public void setRunning(boolean running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return this.autoStartup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #isAutoStartup()
|
||||
* @param autoStartup true to auto start.
|
||||
*/
|
||||
public void setAutoStartup(boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
if (!this.running) {
|
||||
this.directory = getFileRepo();
|
||||
if (this.directory != null && !this.directory.isEmpty()) {
|
||||
try {
|
||||
this.watcher = FileSystems.getDefault().newWatchService();
|
||||
for (Path path : this.directory) {
|
||||
walkDirectory(path);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
}
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
if (this.running) {
|
||||
try {
|
||||
this.watcher.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Failed to close watcher for " + this.directory.toString(), e);
|
||||
}
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Runnable callback) {
|
||||
stop();
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRateString = "${spring.cloud.config.server.monitor.fixedDelay:5000}")
|
||||
public void poll() {
|
||||
for (File file : filesFromEvents()) {
|
||||
this.endpoint.notifyByPath(new LinkedMultiValueMap<String, String>(),
|
||||
Collections.<String, Object> singletonMap("path",
|
||||
file.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Path> getFileRepo() {
|
||||
if (this.scmRepository != null
|
||||
&& this.scmRepository.getUri().startsWith("file:")) {
|
||||
try {
|
||||
return Collections.singleton(Paths.get(this.resourceLoader
|
||||
.getResource(this.scmRepository.getUri()).getURI()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Cannot resolve URI for path: " + this.scmRepository.getUri());
|
||||
}
|
||||
}
|
||||
if (this.nativeEnvironmentRepository != null) {
|
||||
Set<Path> paths = new LinkedHashSet<>();
|
||||
for (String path : this.nativeEnvironmentRepository.getSearchLocations()) {
|
||||
Resource resource = this.resourceLoader.getResource(path);
|
||||
if (resource.exists()) {
|
||||
try {
|
||||
paths.add(Paths.get(resource.getURI()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Cannot resolve URI for path: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Set<File> filesFromEvents() {
|
||||
WatchKey key = this.watcher.poll();
|
||||
Set<File> files = new LinkedHashSet<File>();
|
||||
while (key != null) {
|
||||
for (WatchEvent<?> event : key.pollEvents()) {
|
||||
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE
|
||||
|| event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
Path item = (Path) event.context();
|
||||
File file = new File(((Path) key.watchable()).toAbsolutePath()
|
||||
+ File.separator + item.getFileName());
|
||||
if (file.isDirectory()) {
|
||||
files.addAll(walkDirectory(file.toPath()));
|
||||
}
|
||||
else {
|
||||
if (!file.getPath().contains(".git") && !PatternMatchUtils
|
||||
.simpleMatch(this.excludes, file.getName())) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Watch Event: " + event.kind() + ": " + file);
|
||||
}
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Watch Event: " + event.kind() + ": context: "
|
||||
+ event.context());
|
||||
}
|
||||
if (event.context() != null && event.context() instanceof Path) {
|
||||
files.addAll(walkDirectory((Path) event.context()));
|
||||
}
|
||||
else {
|
||||
for (Path path : this.directory) {
|
||||
files.addAll(walkDirectory(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Watch Event: " + event.kind() + ": context: "
|
||||
+ event.context());
|
||||
}
|
||||
}
|
||||
}
|
||||
key.reset();
|
||||
key = this.watcher.poll();
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private Set<File> walkDirectory(Path directory) {
|
||||
final Set<File> walkedFiles = new LinkedHashSet<File>();
|
||||
try {
|
||||
registerWatch(directory);
|
||||
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir,
|
||||
BasicFileAttributes attrs) throws IOException {
|
||||
FileVisitResult fileVisitResult = super.preVisitDirectory(dir, attrs);
|
||||
registerWatch(dir);
|
||||
return fileVisitResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
FileVisitResult fileVisitResult = super.visitFile(file, attrs);
|
||||
walkedFiles.add(file.toFile());
|
||||
return fileVisitResult;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Failed to walk directory: " + directory.toString(), e);
|
||||
}
|
||||
return walkedFiles;
|
||||
}
|
||||
|
||||
private void registerWatch(Path dir) throws IOException {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("registering: " + dir + " for file creation events");
|
||||
}
|
||||
dir.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE,
|
||||
StandardWatchEventKinds.ENTRY_MODIFY);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Order(Ordered.LOWEST_PRECEDENCE - 300)
|
||||
public class GithubPropertyPathNotificationExtractor
|
||||
implements PropertyPathNotificationExtractor {
|
||||
|
||||
@Override
|
||||
public PropertyPathNotification extract(MultiValueMap<String, String> headers,
|
||||
Map<String, Object> request) {
|
||||
if ("push".equals(headers.getFirst("X-Github-Event"))) {
|
||||
if (request.get("commits") instanceof Collection) {
|
||||
Set<String> paths = new HashSet<>();
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Map<String, Object>> commits = (Collection<Map<String, Object>>) request
|
||||
.get("commits");
|
||||
for (Map<String, Object> commit : commits) {
|
||||
addAllPaths(paths, commit, "added");
|
||||
addAllPaths(paths, commit, "removed");
|
||||
addAllPaths(paths, commit, "modified");
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
return new PropertyPathNotification(paths.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addAllPaths(Set<String> paths, Map<String, Object> commit, String name) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<String> files = (Collection<String>) commit.get(name);
|
||||
if (files != null) {
|
||||
paths.addAll(files);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Order(Ordered.LOWEST_PRECEDENCE - 100)
|
||||
public class GitlabPropertyPathNotificationExtractor
|
||||
implements PropertyPathNotificationExtractor {
|
||||
|
||||
@Override
|
||||
public PropertyPathNotification extract(MultiValueMap<String, String> headers,
|
||||
Map<String, Object> request) {
|
||||
if ("Push Event".equals(headers.getFirst("X-Gitlab-Event"))) {
|
||||
if (request.get("commits") instanceof Collection) {
|
||||
// Gitlab doesn't tell us the files that changed so this is a broadcast to
|
||||
// all apps
|
||||
return new PropertyPathNotification("application.yml");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
/**
|
||||
* HTTP endpoint for webhooks coming from repository providers.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("${spring.cloud.config.monitor.endpoint.path:}/monitor")
|
||||
@CommonsLog
|
||||
public class PropertyPathEndpoint
|
||||
implements ApplicationEventPublisherAware, ApplicationContextAware {
|
||||
|
||||
private final PropertyPathNotificationExtractor extractor;
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
private String contextId = UUID.randomUUID().toString();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
this.contextId = applicationContext.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(
|
||||
ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public Set<String> notifyByPath(@RequestHeader MultiValueMap<String, String> headers,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
PropertyPathNotification notification = this.extractor.extract(headers, request);
|
||||
if (notification != null) {
|
||||
|
||||
Set<String> services = new HashSet<>();
|
||||
|
||||
for (String path : notification.getPaths()) {
|
||||
services.addAll(guessServiceName(path));
|
||||
}
|
||||
if (this.applicationEventPublisher != null) {
|
||||
for (String service : services) {
|
||||
log.info("Refresh for: " + service);
|
||||
this.applicationEventPublisher
|
||||
.publishEvent(new RefreshRemoteApplicationEvent(this,
|
||||
this.contextId, service));
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public Set<String> notifyByForm(@RequestHeader MultiValueMap<String, String> headers, @RequestParam("path") List<String> request) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String key = "path";
|
||||
map.put(key, request);
|
||||
return notifyByPath(headers, map);
|
||||
}
|
||||
|
||||
private Set<String> guessServiceName(String path) {
|
||||
Set<String> services = new HashSet<>();
|
||||
if (path != null) {
|
||||
String stem = StringUtils
|
||||
.stripFilenameExtension(StringUtils.getFilename(path));
|
||||
// TODO: correlate with service registry, and if stem=="application"
|
||||
// return all, otherwise return only the matching service
|
||||
if (services.isEmpty()) {
|
||||
if ("application".equals(stem)) {
|
||||
services.add("*");
|
||||
}
|
||||
else {
|
||||
services.add(stem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Simple abstraction of a list of paths that changed in a repository.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class PropertyPathNotification {
|
||||
|
||||
public PropertyPathNotification(String... paths) {
|
||||
this.paths = paths;
|
||||
}
|
||||
|
||||
private String[] paths;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Strategy for extracting a {@link PropertyPathNotification} from an incoming,
|
||||
* unstructured request. Different providers of notifications have different payloads for
|
||||
* their events, and different headers (e.g. HTTP headers for a webhook).
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public interface PropertyPathNotificationExtractor {
|
||||
|
||||
PropertyPathNotification extract(MultiValueMap<String, String> headers,
|
||||
Map<String, Object> payload);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Autoconfiguration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.config.monitor.EnvironmentMonitorAutoConfiguration
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class CompositePropertyPathNotificationExtractorTests {
|
||||
|
||||
private CompositePropertyPathNotificationExtractor extractor = new CompositePropertyPathNotificationExtractor(
|
||||
Arrays.asList(new GitlabPropertyPathNotificationExtractor(),
|
||||
new GithubPropertyPathNotificationExtractor()));
|
||||
|
||||
private HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@Test
|
||||
public void githubSample() throws Exception {
|
||||
// See https://developer.github.com/v3/activity/events/types/#pushevent
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitpush.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Github-Event", "push");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNotNull(extracted);
|
||||
assertEquals("README.md", extracted.getPaths()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gitlabDetected() throws Exception {
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitlab.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Gitlab-Event", "Push Event");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNotNull(extracted);
|
||||
assertEquals("application.yml", extracted.getPaths()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallback() throws Exception {
|
||||
Map<String, Object> value = Collections.<String, Object> singletonMap("path",
|
||||
"foo");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNotNull(extracted);
|
||||
assertEquals("foo", extracted.getPaths()[0]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class GithubPropertyPathNotificationExtractorTests {
|
||||
|
||||
private GithubPropertyPathNotificationExtractor extractor = new GithubPropertyPathNotificationExtractor();
|
||||
|
||||
private HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@Test
|
||||
public void githubSample() throws Exception {
|
||||
// See https://developer.github.com/v3/activity/events/types/#pushevent
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitpush.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Github-Event", "push");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNotNull(extracted);
|
||||
assertEquals("README.md", extracted.getPaths()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notAPushNotDetected() throws Exception {
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("github.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Github-Event", "issues");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNull(extracted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gitlabNotDetected() throws Exception {
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitlab.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Github-Event", "push");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNull(extracted);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.monitor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class GitlabPropertyPathNotificationExtractorTests {
|
||||
|
||||
private GitlabPropertyPathNotificationExtractor extractor = new GitlabPropertyPathNotificationExtractor();
|
||||
|
||||
private HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@Test
|
||||
public void pushEvent() throws Exception {
|
||||
// See http://doc.gitlab.com/ee/web_hooks/web_hooks.html#push-events
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitlab.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Gitlab-Event", "Push Event");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNotNull(extracted);
|
||||
assertEquals("application.yml", extracted.getPaths()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonPushEventNotDetected() throws Exception {
|
||||
// See http://doc.gitlab.com/ee/web_hooks/web_hooks.html#push-events
|
||||
Map<String, Object> value = new ObjectMapper().readValue(
|
||||
new ClassPathResource("gitlab.json").getInputStream(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
this.headers.set("X-Gitlab-Event", "Issue Event");
|
||||
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
|
||||
assertNull(extracted);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.monitor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class PropertyPathEndpointTests {
|
||||
|
||||
private PropertyPathEndpoint endpoint = new PropertyPathEndpoint(
|
||||
new CompositePropertyPathNotificationExtractor(
|
||||
Collections.<PropertyPathNotificationExtractor> emptyList()));
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
StaticApplicationContext publisher = new StaticApplicationContext();
|
||||
this.endpoint.setApplicationEventPublisher(publisher);
|
||||
publisher.refresh();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyByForm() throws Exception {
|
||||
assertEquals(0,
|
||||
this.endpoint.notifyByForm(new LinkedMultiValueMap<String, String>(),
|
||||
new ArrayList<String>()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifySeveral() throws Exception {
|
||||
List<String> request = new ArrayList<String>();
|
||||
request.add("/foo/bar.properties");
|
||||
request.add("/application.properties");
|
||||
assertEquals("[bar, *]",
|
||||
this.endpoint
|
||||
.notifyByForm(new LinkedMultiValueMap<String, String>(), request)
|
||||
.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyAll() throws Exception {
|
||||
assertEquals("[*]", this.endpoint
|
||||
.notifyByPath(new LinkedMultiValueMap<String, String>(), Collections
|
||||
.<String, Object> singletonMap("path", "application.yml"))
|
||||
.toString());
|
||||
}
|
||||
}
|
||||
161
spring-cloud-config-monitor/src/test/resources/github.json
Normal file
161
spring-cloud-config-monitor/src/test/resources/github.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"ref": "refs/heads/changes",
|
||||
"before": "9049f1265b7d61be4a8904a9a27120d2064dab3b",
|
||||
"after": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"created": false,
|
||||
"deleted": false,
|
||||
"forced": false,
|
||||
"base_ref": null,
|
||||
"compare": "https://github.com/baxterthehacker/public-repo/compare/9049f1265b7d...0d1a26e67d8f",
|
||||
"commits": [
|
||||
{
|
||||
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"distinct": true,
|
||||
"message": "Update README.md",
|
||||
"timestamp": "2015-05-05T19:40:15-04:00",
|
||||
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"author": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com",
|
||||
"username": "baxterthehacker"
|
||||
},
|
||||
"committer": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com",
|
||||
"username": "baxterthehacker"
|
||||
},
|
||||
"added": [
|
||||
|
||||
],
|
||||
"removed": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
"head_commit": {
|
||||
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"distinct": true,
|
||||
"message": "Update README.md",
|
||||
"timestamp": "2015-05-05T19:40:15-04:00",
|
||||
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
"author": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com",
|
||||
"username": "baxterthehacker"
|
||||
},
|
||||
"committer": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com",
|
||||
"username": "baxterthehacker"
|
||||
},
|
||||
"added": [
|
||||
|
||||
],
|
||||
"removed": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"README.md"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"id": 35129377,
|
||||
"name": "public-repo",
|
||||
"full_name": "baxterthehacker/public-repo",
|
||||
"owner": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/baxterthehacker/public-repo",
|
||||
"description": "",
|
||||
"fork": false,
|
||||
"url": "https://github.com/baxterthehacker/public-repo",
|
||||
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
|
||||
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
|
||||
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
|
||||
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
|
||||
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
|
||||
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
|
||||
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
|
||||
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
|
||||
"created_at": 1430869212,
|
||||
"updated_at": "2015-05-05T23:40:12Z",
|
||||
"pushed_at": 1430869217,
|
||||
"git_url": "git://github.com/baxterthehacker/public-repo.git",
|
||||
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
|
||||
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
|
||||
"svn_url": "https://github.com/baxterthehacker/public-repo",
|
||||
"homepage": null,
|
||||
"size": 0,
|
||||
"stargazers_count": 0,
|
||||
"watchers_count": 0,
|
||||
"language": null,
|
||||
"has_issues": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": true,
|
||||
"forks_count": 0,
|
||||
"mirror_url": null,
|
||||
"open_issues_count": 0,
|
||||
"forks": 0,
|
||||
"open_issues": 0,
|
||||
"watchers": 0,
|
||||
"default_branch": "master",
|
||||
"stargazers": 0,
|
||||
"master_branch": "master"
|
||||
},
|
||||
"pusher": {
|
||||
"name": "baxterthehacker",
|
||||
"email": "baxterthehacker@users.noreply.github.com"
|
||||
},
|
||||
"sender": {
|
||||
"login": "baxterthehacker",
|
||||
"id": 6752317,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/baxterthehacker",
|
||||
"html_url": "https://github.com/baxterthehacker",
|
||||
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
|
||||
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
|
||||
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
|
||||
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
42
spring-cloud-config-monitor/src/test/resources/gitlab.json
Normal file
42
spring-cloud-config-monitor/src/test/resources/gitlab.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"object_kind": "push",
|
||||
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
|
||||
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 4,
|
||||
"user_name": "John Smith",
|
||||
"user_email": "john@example.com",
|
||||
"project_id": 15,
|
||||
"repository": {
|
||||
"name": "Diaspora",
|
||||
"url": "git@example.com:mike/diasporadiaspora.git",
|
||||
"description": "",
|
||||
"homepage": "http://example.com/mike/diaspora",
|
||||
"git_http_url":"http://example.com/mike/diaspora.git",
|
||||
"git_ssh_url":"git@example.com:mike/diaspora.git",
|
||||
"visibility_level":0
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||
"message": "Update Catalan translation to e38cb41.",
|
||||
"timestamp": "2011-12-12T14:27:31+02:00",
|
||||
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||
"author": {
|
||||
"name": "Jordi Mallach",
|
||||
"email": "jordi@softcatala.org"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"message": "fixed readme",
|
||||
"timestamp": "2012-01-03T23:36:29+02:00",
|
||||
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"author": {
|
||||
"name": "GitLab dev user",
|
||||
"email": "gitlabdev@dv6700.(none)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 4
|
||||
}
|
||||
@@ -76,7 +76,6 @@ public class EnvironmentController {
|
||||
|
||||
public EnvironmentController(EnvironmentRepository repository,
|
||||
EnvironmentEncryptor environmentEncryptor) {
|
||||
super();
|
||||
this.repository = repository;
|
||||
this.defaultLabel = repository.getDefaultLabel();
|
||||
this.environmentEncryptor = environmentEncryptor;
|
||||
@@ -85,7 +84,7 @@ public class EnvironmentController {
|
||||
/**
|
||||
* Flag to indicate that YAML documents which are not a map should be stripped of the
|
||||
* "document" prefix that is added by Spring (to facilitate conversion to Properties).
|
||||
*
|
||||
*
|
||||
* @param stripDocument the flag to set
|
||||
*/
|
||||
public void setStripDocumentFromYaml(boolean stripDocument) {
|
||||
@@ -127,10 +126,11 @@ public class EnvironmentController {
|
||||
|
||||
@RequestMapping("/{label}/{name}-{profiles}.properties")
|
||||
public ResponseEntity<String> labelledProperties(@PathVariable String name,
|
||||
@PathVariable String profiles, @PathVariable String label) throws IOException {
|
||||
@PathVariable String profiles, @PathVariable String label)
|
||||
throws IOException {
|
||||
validateNameAndProfiles(name, profiles);
|
||||
Map<String, Object> properties = convertToProperties(labelled(name, profiles,
|
||||
label));
|
||||
Map<String, Object> properties = convertToProperties(
|
||||
labelled(name, profiles, label));
|
||||
return getSuccess(getPropertiesString(properties));
|
||||
}
|
||||
|
||||
@@ -167,7 +167,8 @@ public class EnvironmentController {
|
||||
return labelledYaml(name, profiles, this.defaultLabel);
|
||||
}
|
||||
|
||||
@RequestMapping({ "/{label}/{name}-{profiles}.yml", "/{label}/{name}-{profiles}.yaml" })
|
||||
@RequestMapping({ "/{label}/{name}-{profiles}.yml",
|
||||
"/{label}/{name}-{profiles}.yaml" })
|
||||
public ResponseEntity<String> labelledYaml(@PathVariable String name,
|
||||
@PathVariable String profiles, @PathVariable String label) throws Exception {
|
||||
validateNameAndProfiles(name, profiles);
|
||||
@@ -301,7 +302,7 @@ public class EnvironmentController {
|
||||
}
|
||||
|
||||
private void postProcessProperties(Map<String, Object> propertiesMap) {
|
||||
for (Iterator<String> iter = propertiesMap.keySet().iterator(); iter.hasNext(); ) {
|
||||
for (Iterator<String> iter = propertiesMap.keySet().iterator(); iter.hasNext();) {
|
||||
String key = iter.next();
|
||||
if (key.equals("spring.profiles")) {
|
||||
iter.remove();
|
||||
|
||||
Reference in New Issue
Block a user