Add bitbucket webhook watch.

fixes gh-243
This commit is contained in:
Spencer Gibb
2015-09-30 13:00:50 -06:00
parent cead15bf8d
commit 309d1fbfbc
6 changed files with 364 additions and 9 deletions

View File

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

View File

@@ -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<String, String> headers,
Map<String, Object> 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;
}
}

View File

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

View File

@@ -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<String, Object> 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<String, Object> value = readPayload(path);
setHeaders(eventKey);
PropertyPathNotification extracted = this.extractor.extract(this.headers, value);
assertNull(extracted);
}
private Map<String, Object> readPayload(String path) throws java.io.IOException {
return new ObjectMapper().readValue(
new ClassPathResource(path).getInputStream(),
new TypeReference<Map<String, Object>>() {
});
}
}

View File

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

View File

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