From 8d8e07a78e73c3e1e08f4277a76c6e1a4104f103 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 8 Sep 2015 08:54:24 +0100 Subject: [PATCH] Push events for config changes Works from local repos or with explicit POST to /monitor with path=, also supports webhooks from github and gitlab. --- .../main/asciidoc/spring-cloud-config.adoc | 37 ++- pom.xml | 9 +- spring-cloud-config-monitor/pom.xml | 49 +++ ...sitePropertyPathNotificationExtractor.java | 84 +++++ .../EnvironmentMonitorAutoConfiguration.java | 43 +++ .../monitor/FileMonitorConfiguration.java | 295 ++++++++++++++++++ ...thubPropertyPathNotificationExtractor.java | 66 ++++ ...tlabPropertyPathNotificationExtractor.java | 47 +++ .../config/monitor/PropertyPathEndpoint.java | 128 ++++++++ .../monitor/PropertyPathNotification.java | 38 +++ .../PropertyPathNotificationExtractor.java | 36 +++ .../main/resources/META-INF/spring.factories | 3 + ...ropertyPathNotificationExtractorTests.java | 79 +++++ ...ropertyPathNotificationExtractorTests.java | 77 +++++ ...ropertyPathNotificationExtractorTests.java | 67 ++++ .../monitor/PropertyPathEndpointTests.java | 72 +++++ .../src/test/resources/github.json | 161 ++++++++++ .../src/test/resources/gitlab.json | 42 +++ .../config/server/EnvironmentController.java | 15 +- 19 files changed, 1339 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-config-monitor/pom.xml create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractor.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/FileMonitorConfiguration.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractor.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractor.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotification.java create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotificationExtractor.java create mode 100644 spring-cloud-config-monitor/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractorTests.java create mode 100644 spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractorTests.java create mode 100644 spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractorTests.java create mode 100644 spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java create mode 100644 spring-cloud-config-monitor/src/test/resources/github.json create mode 100644 spring-cloud-config-monitor/src/test/resources/gitlab.json diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 321f780e..4bf1e230 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -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 diff --git a/pom.xml b/pom.xml index 36c3d3c3..64fe29d2 100644 --- a/pom.xml +++ b/pom.xml @@ -22,13 +22,15 @@ config + 1.1.0.BUILD-SNAPSHOT 1.1.0.BUILD-SNAPSHOT spring-cloud-config-client spring-cloud-config-server + spring-cloud-config-monitor spring-cloud-config-sample - spring-cloud-starter-config + spring-cloud-starter-config docs @@ -55,6 +57,11 @@ spring-cloud-config-server 1.1.0.BUILD-SNAPSHOT + + org.springframework.cloud + spring-cloud-config-monitor + 1.1.0.BUILD-SNAPSHOT + org.springframework.retry spring-retry diff --git a/spring-cloud-config-monitor/pom.xml b/spring-cloud-config-monitor/pom.xml new file mode 100644 index 00000000..08d32b15 --- /dev/null +++ b/spring-cloud-config-monitor/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-config + 1.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-config-monitor + spring-cloud-config-monitor + Spring Cloud Config Monitor + + ${basedir}/../.. + + + + + org.springframework.cloud + spring-cloud-bus-parent + ${spring-cloud-bus.version} + pom + import + + + + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-bus + + + org.projectlombok + lombok + + true + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractor.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractor.java new file mode 100644 index 00000000..4d244ddd --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractor.java @@ -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 extractors; + + public CompositePropertyPathNotificationExtractor( + List 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 headers, + Map 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 headers, + Map request) { + Object object = request.get("path"); + if (object instanceof String) { + return new PropertyPathNotification((String) object); + } + if (object instanceof Collection) { + @SuppressWarnings("unchecked") + Collection collection = (Collection) object; + return new PropertyPathNotification(collection.toArray(new String[0])); + } + return null; + } + + } + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java new file mode 100644 index 00000000..2e937799 --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java @@ -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 extractors; + + @Bean + public PropertyPathEndpoint propertyPathEndpoint() { + return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(this.extractors)); + } +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/FileMonitorConfiguration.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/FileMonitorConfiguration.java new file mode 100644 index 00000000..9a859c6a --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/FileMonitorConfiguration.java @@ -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 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(), + Collections. singletonMap("path", + file.getAbsolutePath())); + } + } + + private Set 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 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 filesFromEvents() { + WatchKey key = this.watcher.poll(); + Set files = new LinkedHashSet(); + 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 walkDirectory(Path directory) { + final Set walkedFiles = new LinkedHashSet(); + try { + registerWatch(directory); + Files.walkFileTree(directory, new SimpleFileVisitor() { + + @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); + } + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractor.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractor.java new file mode 100644 index 00000000..f117784a --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractor.java @@ -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 headers, + Map request) { + if ("push".equals(headers.getFirst("X-Github-Event"))) { + if (request.get("commits") instanceof Collection) { + Set paths = new HashSet<>(); + @SuppressWarnings("unchecked") + Collection> commits = (Collection>) request + .get("commits"); + for (Map 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 paths, Map commit, String name) { + @SuppressWarnings("unchecked") + Collection files = (Collection) commit.get(name); + if (files != null) { + paths.addAll(files); + } + } + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractor.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractor.java new file mode 100644 index 00000000..b9796776 --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractor.java @@ -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 headers, + Map 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; + } + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java new file mode 100644 index 00000000..ddc6daeb --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java @@ -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 notifyByPath(@RequestHeader MultiValueMap headers, + @RequestBody Map request) { + PropertyPathNotification notification = this.extractor.extract(headers, request); + if (notification != null) { + + Set 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 notifyByForm(@RequestHeader MultiValueMap headers, @RequestParam("path") List request) { + Map map = new HashMap<>(); + String key = "path"; + map.put(key, request); + return notifyByPath(headers, map); + } + + private Set guessServiceName(String path) { + Set 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; + } + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotification.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotification.java new file mode 100644 index 00000000..cd52774d --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotification.java @@ -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; + +} diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotificationExtractor.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotificationExtractor.java new file mode 100644 index 00000000..0fc03bd9 --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathNotificationExtractor.java @@ -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 headers, + Map payload); + +} diff --git a/spring-cloud-config-monitor/src/main/resources/META-INF/spring.factories b/spring-cloud-config-monitor/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..62436d49 --- /dev/null +++ b/spring-cloud-config-monitor/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Autoconfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.config.monitor.EnvironmentMonitorAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractorTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractorTests.java new file mode 100644 index 00000000..b4a1be39 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/CompositePropertyPathNotificationExtractorTests.java @@ -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 value = new ObjectMapper().readValue( + new ClassPathResource("gitpush.json").getInputStream(), + new TypeReference>() { + }); + 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 value = new ObjectMapper().readValue( + new ClassPathResource("gitlab.json").getInputStream(), + new TypeReference>() { + }); + 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 value = Collections. singletonMap("path", + "foo"); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNotNull(extracted); + assertEquals("foo", extracted.getPaths()[0]); + } + +} diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractorTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractorTests.java new file mode 100644 index 00000000..f45ba4a8 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GithubPropertyPathNotificationExtractorTests.java @@ -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 value = new ObjectMapper().readValue( + new ClassPathResource("gitpush.json").getInputStream(), + new TypeReference>() { + }); + 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 value = new ObjectMapper().readValue( + new ClassPathResource("github.json").getInputStream(), + new TypeReference>() { + }); + this.headers.set("X-Github-Event", "issues"); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNull(extracted); + } + + @Test + public void gitlabNotDetected() throws Exception { + Map value = new ObjectMapper().readValue( + new ClassPathResource("gitlab.json").getInputStream(), + new TypeReference>() { + }); + this.headers.set("X-Github-Event", "push"); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNull(extracted); + } + +} diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractorTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractorTests.java new file mode 100644 index 00000000..38c8f72a --- /dev/null +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/GitlabPropertyPathNotificationExtractorTests.java @@ -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 value = new ObjectMapper().readValue( + new ClassPathResource("gitlab.json").getInputStream(), + new TypeReference>() { + }); + 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 value = new ObjectMapper().readValue( + new ClassPathResource("gitlab.json").getInputStream(), + new TypeReference>() { + }); + this.headers.set("X-Gitlab-Event", "Issue Event"); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNull(extracted); + } + +} diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java new file mode 100644 index 00000000..d33c29d5 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java @@ -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. 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(), + new ArrayList()).size()); + } + + @Test + public void testNotifySeveral() throws Exception { + List request = new ArrayList(); + request.add("/foo/bar.properties"); + request.add("/application.properties"); + assertEquals("[bar, *]", + this.endpoint + .notifyByForm(new LinkedMultiValueMap(), request) + .toString()); + } + + @Test + public void testNotifyAll() throws Exception { + assertEquals("[*]", this.endpoint + .notifyByPath(new LinkedMultiValueMap(), Collections + . singletonMap("path", "application.yml")) + .toString()); + } +} diff --git a/spring-cloud-config-monitor/src/test/resources/github.json b/spring-cloud-config-monitor/src/test/resources/github.json new file mode 100644 index 00000000..59e1a2f2 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/resources/github.json @@ -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 + } +} \ No newline at end of file diff --git a/spring-cloud-config-monitor/src/test/resources/gitlab.json b/spring-cloud-config-monitor/src/test/resources/gitlab.json new file mode 100644 index 00000000..266fda50 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/resources/gitlab.json @@ -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 +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java index 455b038e..edf244d5 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java @@ -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 labelledProperties(@PathVariable String name, - @PathVariable String profiles, @PathVariable String label) throws IOException { + @PathVariable String profiles, @PathVariable String label) + throws IOException { validateNameAndProfiles(name, profiles); - Map properties = convertToProperties(labelled(name, profiles, - label)); + Map 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 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 propertiesMap) { - for (Iterator iter = propertiesMap.keySet().iterator(); iter.hasNext(); ) { + for (Iterator iter = propertiesMap.keySet().iterator(); iter.hasNext();) { String key = iter.next(); if (key.equals("spring.profiles")) { iter.remove();