From 309d1fbfbcbd2f0ff750454f126468f1085383fa Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 30 Sep 2015 13:00:50 -0600 Subject: [PATCH] Add bitbucket webhook watch. fixes gh-243 --- .../main/asciidoc/spring-cloud-config.adoc | 20 +- ...cketPropertyPathNotificationExtractor.java | 51 +++++ .../EnvironmentMonitorAutoConfiguration.java | 6 + ...ropertyPathNotificationExtractorTests.java | 88 ++++++++ ...ironmentMonitorAutoConfigurationTests.java | 2 +- .../src/test/resources/bitbucket.json | 206 ++++++++++++++++++ 6 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractor.java create mode 100644 spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractorTests.java create mode 100644 spring-cloud-config-monitor/src/test/resources/bitbucket.json diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index c2be9207..64834e91 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -618,8 +618,8 @@ 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 +Many source code repository providers (like Github, Gitlab or Bitbucket +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 @@ -635,13 +635,17 @@ When the webhook is activated the Config Server will send a 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. +"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). +The default configuration works out of the box with Github, Gitlab or +Bitbucket. In addition to the JSON notifications from Github, Gitlab +or Bitbucket 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 diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractor.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractor.java new file mode 100644 index 00000000..b1d31ca3 --- /dev/null +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractor.java @@ -0,0 +1,51 @@ +/* + * 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; +import org.springframework.util.StringUtils; + +/** + * @author Spencer Gibb + * @author Dave Syer + * + */ +@Order(Ordered.LOWEST_PRECEDENCE - 100) +public class BitbucketPropertyPathNotificationExtractor + implements PropertyPathNotificationExtractor { + + @Override + public PropertyPathNotification extract(MultiValueMap headers, + Map request) { + if ("repo:push".equals(headers.getFirst("X-Event-Key")) && + StringUtils.hasText(headers.getFirst("X-Hook-UUID"))) { + Object push = request.get("push"); + if (push instanceof Map && ((Map)push).get("changes") instanceof Collection) { + // Bitbucket 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/EnvironmentMonitorAutoConfiguration.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java index 9ac46537..d9238c7d 100644 --- 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 @@ -54,4 +54,10 @@ public class EnvironmentMonitorAutoConfiguration { return new GitlabPropertyPathNotificationExtractor(); } + @Bean + @ConditionalOnProperty(value="spring.cloud.config.server.monitor.bitbucket.enabled", havingValue="true", matchIfMissing=true) + public BitbucketPropertyPathNotificationExtractor bitbucketPropertyPathNotificationExtractor() { + return new BitbucketPropertyPathNotificationExtractor(); + } + } diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractorTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractorTests.java new file mode 100644 index 00000000..2ff35dc6 --- /dev/null +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/BitbucketPropertyPathNotificationExtractorTests.java @@ -0,0 +1,88 @@ +/* + * 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 java.util.UUID; + +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 Spencer Gibb + * @author Dave Syer + * + */ +public class BitbucketPropertyPathNotificationExtractorTests { + + private BitbucketPropertyPathNotificationExtractor extractor = new BitbucketPropertyPathNotificationExtractor(); + + private HttpHeaders headers = new HttpHeaders(); + + @Test + public void bitbucketSample() throws Exception { + // https://confluence.atlassian.com/bitbucket/event-payloads-740262817.html#EventPayloads-Push + Map value = readPayload("bitbucket.json"); + setHeaders("repo:push"); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNotNull(extracted); + assertEquals("application.yml", extracted.getPaths()[0]); + } + + private void setHeaders(String eventKey) { + this.headers.set("X-Event-Key", eventKey); + this.headers.set("X-Hook-UUID", UUID.randomUUID().toString()); + } + + @Test + public void notAPushNotDetected() throws Exception { + assertNotExtracted("bitbucket.json", "issue:created"); + } + + @Test + public void gitlabNotDetected() throws Exception { + assertNotExtracted("gitlab.json", "repo:push"); + } + + @Test + public void githubNotDetected() throws Exception { + assertNotExtracted("github.json", "repo:push"); + } + + private void assertNotExtracted(String path, String eventKey) throws java.io.IOException { + Map value = readPayload(path); + setHeaders(eventKey); + PropertyPathNotification extracted = this.extractor.extract(this.headers, value); + assertNull(extracted); + } + + private Map readPayload(String path) throws java.io.IOException { + return new ObjectMapper().readValue( + new ClassPathResource(path).getInputStream(), + new TypeReference>() { + }); + } + +} diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java index 176bf765..28e2b22a 100644 --- a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java @@ -42,7 +42,7 @@ public class EnvironmentMonitorAutoConfigurationTests { PropertyPlaceholderAutoConfiguration.class).properties("server.port=-1") .run(); PropertyPathEndpoint endpoint = context.getBean(PropertyPathEndpoint.class); - assertEquals(3, + assertEquals(4, ((Collection) ReflectionTestUtils.getField( ReflectionTestUtils.getField(endpoint, "extractor"), "extractors")).size()); diff --git a/spring-cloud-config-monitor/src/test/resources/bitbucket.json b/spring-cloud-config-monitor/src/test/resources/bitbucket.json new file mode 100644 index 00000000..35610d5b --- /dev/null +++ b/spring-cloud-config-monitor/src/test/resources/bitbucket.json @@ -0,0 +1,206 @@ +{ + "actor": { + "username": "emmap1", + "display_name": "Emma", + "uuid": "{a54f16da-24e9-4d7f-a3a7-b1ba2cd98aa3}", + "links": { + "self": { + "href": "https://api.bitbucket.org/api/2.0/users/emmap1" + }, + "html": { + "href": "https://api.bitbucket.org/emmap1" + }, + "avatar": { + "href": "https://bitbucket-api-assetroot.s3.amazonaws.com/c/photos/2015/Feb/26/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "repository": { + "links": { + "self": { + "href": "https://api.bitbucket.org/api/2.0/repositories/bitbucket/bitbucket" + }, + "html": { + "href": "https://api.bitbucket.org/bitbucket/bitbucket" + }, + "avatar": { + "href": "https://api-staging-assetroot.s3.amazonaws.com/c/photos/2014/Aug/01/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}", + "full_name": "team_name/repo_name", + "name": "repo_name", + "scm": "git", + "is_private": true + }, + "push": { + "changes": [ + { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": { + "type": "commit", + "hash": "709d658dc5b6d6afcd46049c2f332ee3f515a67d", + "author": { + "username": "emmap1", + "display_name": "Emma", + "uuid": "{a54f16da-24e9-4d7f-a3a7-b1ba2cd98aa3}", + "links": { + "self": { + "href": "https://api.bitbucket.org/api/2.0/users/emmap1" + }, + "html": { + "href": "https://api.bitbucket.org/emmap1" + }, + "avatar": { + "href": "https://bitbucket-api-assetroot.s3.amazonaws.com/c/photos/2015/Feb/26/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "message": "new commit message\n", + "date": "2015-06-09T03:34:49+00:00", + "parents": [ + { + "type": "commit", + "hash": "1e65c05c1d5171631d92438a13901ca7dae9618c", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commit/8cbbd65829c7ad834a97841e0defc965718036a0" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/commits/8cbbd65829c7ad834a97841e0defc965718036a0" + } + } + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commit/c4b2b7914156a878aa7c9da452a09fb50c2091f2" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/commits/c4b2b7914156a878aa7c9da452a09fb50c2091f2" + } + } + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/refs/branches/master" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commits/master" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/branch/master" + } + } + }, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": { + "type": "commit", + "hash": "1e65c05c1d5171631d92438a13901ca7dae9618c", + "author": { + "username": "emmap1", + "display_name": "Emma", + "uuid": "{a54f16da-24e9-4d7f-a3a7-b1ba2cd98aa3}", + "links": { + "self": { + "href": "https://api.bitbucket.org/api/2.0/users/emmap1" + }, + "html": { + "href": "https://api.bitbucket.org/emmap1" + }, + "avatar": { + "href": "https://bitbucket-api-assetroot.s3.amazonaws.com/c/photos/2015/Feb/26/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "message": "old commit message\n", + "date": "2015-06-08T21:34:56+00:00", + "parents": [ + { + "type": "commit", + "hash": "e0d0c2041e09746be5ce4b55067d5a8e3098c843", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commit/9c4a3452da3bc4f37af5a6bb9c784246f44406f7" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/commits/9c4a3452da3bc4f37af5a6bb9c784246f44406f7" + } + } + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commit/b99ea6dad8f416e57c5ca78c1ccef590600d841b" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/commits/b99ea6dad8f416e57c5ca78c1ccef590600d841b" + } + } + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/refs/branches/master" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commits/master" + }, + "html": { + "href": "https://bitbucket.org/user_name/repo_name/branch/master" + } + } + }, + "links": { + "html": { + "href": "https://bitbucket.org/user_name/repo_name/branches/compare/c4b2b7914156a878aa7c9da452a09fb50c2091f2..b99ea6dad8f416e57c5ca78c1ccef590600d841b" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/diff/c4b2b7914156a878aa7c9da452a09fb50c2091f2..b99ea6dad8f416e57c5ca78c1ccef590600d841b" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/user_name/repo_name/commits?include=c4b2b7914156a878aa7c9da452a09fb50c2091f2&exclude=b99ea6dad8f416e57c5ca78c1ccef590600d841b" + } + }, + "created": false, + "forced": false, + "closed": false, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": { + "username": "emmap1", + "display_name": "Emma", + "uuid": "{a54f16da-24e9-4d7f-a3a7-b1ba2cd98aa3}", + "links": { + "self": { + "href": "https://api.bitbucket.org/api/2.0/users/emmap1" + }, + "html": { + "href": "https://api.bitbucket.org/emmap1" + }, + "avatar": { + "href": "https://bitbucket-api-assetroot.s3.amazonaws.com/c/photos/2015/Feb/26/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user/repo/commit/03f4a7270240708834de475bcf21532d6134777e" + }, + "html": { + "href": "https://bitbucket.org/user/repo/commits/03f4a7270240708834de475bcf21532d6134777e" + } + } + } + ], + "truncated": false + } + ] + } +} \ No newline at end of file