Show More
@@ -29,6 +29,7 b' import colander' | |||||
29 | from celery.task import task |
|
29 | from celery.task import task | |
30 | from requests.packages.urllib3.util.retry import Retry |
|
30 | from requests.packages.urllib3.util.retry import Retry | |
31 |
|
31 | |||
|
32 | import rhodecode | |||
32 | from rhodecode import events |
|
33 | from rhodecode import events | |
33 | from rhodecode.translation import _ |
|
34 | from rhodecode.translation import _ | |
34 | from rhodecode.integrations.types.base import IntegrationTypeBase |
|
35 | from rhodecode.integrations.types.base import IntegrationTypeBase | |
@@ -61,9 +62,10 b" URL_VARS = ', '.join('${' + x + '}' for " | |||||
61 |
|
62 | |||
62 |
|
63 | |||
63 | class WebhookHandler(object): |
|
64 | class WebhookHandler(object): | |
64 | def __init__(self, template_url, secret_token): |
|
65 | def __init__(self, template_url, secret_token, headers): | |
65 | self.template_url = template_url |
|
66 | self.template_url = template_url | |
66 | self.secret_token = secret_token |
|
67 | self.secret_token = secret_token | |
|
68 | self.headers = headers | |||
67 |
|
69 | |||
68 | def get_base_parsed_template(self, data): |
|
70 | def get_base_parsed_template(self, data): | |
69 | """ |
|
71 | """ | |
@@ -117,18 +119,18 b' class WebhookHandler(object):' | |||||
117 | # register per-commit call |
|
119 | # register per-commit call | |
118 | log.debug( |
|
120 | log.debug( | |
119 | 'register webhook call(%s) to url %s', event, commit_url) |
|
121 | 'register webhook call(%s) to url %s', event, commit_url) | |
120 | url_cals.append((commit_url, self.secret_token, data)) |
|
122 | url_cals.append((commit_url, self.secret_token, self.headers, data)) | |
121 |
|
123 | |||
122 | else: |
|
124 | else: | |
123 | # register per-branch call |
|
125 | # register per-branch call | |
124 | log.debug( |
|
126 | log.debug( | |
125 | 'register webhook call(%s) to url %s', event, branch_url) |
|
127 | 'register webhook call(%s) to url %s', event, branch_url) | |
126 | url_cals.append((branch_url, self.secret_token, data)) |
|
128 | url_cals.append((branch_url, self.secret_token, self.headers, data)) | |
127 |
|
129 | |||
128 | else: |
|
130 | else: | |
129 | log.debug( |
|
131 | log.debug( | |
130 | 'register webhook call(%s) to url %s', event, url) |
|
132 | 'register webhook call(%s) to url %s', event, url) | |
131 | url_cals.append((url, self.secret_token, data)) |
|
133 | url_cals.append((url, self.secret_token, self.headers, data)) | |
132 |
|
134 | |||
133 | return url_cals |
|
135 | return url_cals | |
134 |
|
136 | |||
@@ -136,7 +138,7 b' class WebhookHandler(object):' | |||||
136 | url = self.get_base_parsed_template(data) |
|
138 | url = self.get_base_parsed_template(data) | |
137 | log.debug( |
|
139 | log.debug( | |
138 | 'register webhook call(%s) to url %s', event, url) |
|
140 | 'register webhook call(%s) to url %s', event, url) | |
139 | return [(url, self.secret_token, data)] |
|
141 | return [(url, self.secret_token, self.headers, data)] | |
140 |
|
142 | |||
141 | def pull_request_event_handler(self, event, data): |
|
143 | def pull_request_event_handler(self, event, data): | |
142 | url = self.get_base_parsed_template(data) |
|
144 | url = self.get_base_parsed_template(data) | |
@@ -145,7 +147,7 b' class WebhookHandler(object):' | |||||
145 | url = string.Template(url).safe_substitute( |
|
147 | url = string.Template(url).safe_substitute( | |
146 | pull_request_id=data['pullrequest']['pull_request_id'], |
|
148 | pull_request_id=data['pullrequest']['pull_request_id'], | |
147 | pull_request_url=data['pullrequest']['url']) |
|
149 | pull_request_url=data['pullrequest']['url']) | |
148 | return [(url, self.secret_token, data)] |
|
150 | return [(url, self.secret_token, self.headers, data)] | |
149 |
|
151 | |||
150 | def __call__(self, event, data): |
|
152 | def __call__(self, event, data): | |
151 | if isinstance(event, events.RepoPushEvent): |
|
153 | if isinstance(event, events.RepoPushEvent): | |
@@ -178,11 +180,32 b' class WebhookSettingsSchema(colander.Sch' | |||||
178 | secret_token = colander.SchemaNode( |
|
180 | secret_token = colander.SchemaNode( | |
179 | colander.String(), |
|
181 | colander.String(), | |
180 | title=_('Secret Token'), |
|
182 | title=_('Secret Token'), | |
181 |
description=_('String used to validate received payloads.' |
|
183 | description=_('String used to validate received payloads. It will be ' | |
|
184 | 'sent together with event data in JSON'), | |||
182 | default='', |
|
185 | default='', | |
183 | missing='', |
|
186 | missing='', | |
184 | widget=deform.widget.TextInputWidget( |
|
187 | widget=deform.widget.TextInputWidget( | |
185 | placeholder='secret_token' |
|
188 | placeholder='e.g. secret_token' | |
|
189 | ), | |||
|
190 | ) | |||
|
191 | custom_header_key = colander.SchemaNode( | |||
|
192 | colander.String(), | |||
|
193 | title=_('Custom Header Key'), | |||
|
194 | description=_('Custom Header name to be set when calling endpoint.'), | |||
|
195 | default='', | |||
|
196 | missing='', | |||
|
197 | widget=deform.widget.TextInputWidget( | |||
|
198 | placeholder='e.g.Authorization' | |||
|
199 | ), | |||
|
200 | ) | |||
|
201 | custom_header_val = colander.SchemaNode( | |||
|
202 | colander.String(), | |||
|
203 | title=_('Custom Header Value'), | |||
|
204 | description=_('Custom Header value to be set when calling endpoint.'), | |||
|
205 | default='', | |||
|
206 | missing='', | |||
|
207 | widget=deform.widget.TextInputWidget( | |||
|
208 | placeholder='e.g. RcLogin auth=xxxx' | |||
186 | ), |
|
209 | ), | |
187 | ) |
|
210 | ) | |
188 | method_type = colander.SchemaNode( |
|
211 | method_type = colander.SchemaNode( | |
@@ -245,7 +268,15 b' class WebhookIntegrationType(Integration' | |||||
245 | data = event.as_dict() |
|
268 | data = event.as_dict() | |
246 | template_url = self.settings['url'] |
|
269 | template_url = self.settings['url'] | |
247 |
|
270 | |||
248 | handler = WebhookHandler(template_url, self.settings['secret_token']) |
|
271 | headers = {} | |
|
272 | head_key = self.settings['custom_header_key'] | |||
|
273 | head_val = self.settings['custom_header_val'] | |||
|
274 | if head_key and head_val: | |||
|
275 | headers = {head_key: head_val} | |||
|
276 | ||||
|
277 | handler = WebhookHandler( | |||
|
278 | template_url, self.settings['secret_token'], headers) | |||
|
279 | ||||
249 | url_calls = handler(event, data) |
|
280 | url_calls = handler(event, data) | |
250 | log.debug('webhook: calling following urls: %s', |
|
281 | log.debug('webhook: calling following urls: %s', | |
251 | [x[0] for x in url_calls]) |
|
282 | [x[0] for x in url_calls]) | |
@@ -259,7 +290,12 b' def post_to_webhook(url_calls, settings)' | |||||
259 | total=max_retries, |
|
290 | total=max_retries, | |
260 | backoff_factor=0.15, |
|
291 | backoff_factor=0.15, | |
261 | status_forcelist=[500, 502, 503, 504]) |
|
292 | status_forcelist=[500, 502, 503, 504]) | |
262 | for url, token, data in url_calls: |
|
293 | call_headers = { | |
|
294 | 'User-Agent': 'RhodeCode-webhook-caller/{}'.format( | |||
|
295 | rhodecode.__version__) | |||
|
296 | } # updated below with custom ones, allows override | |||
|
297 | ||||
|
298 | for url, token, headers, data in url_calls: | |||
263 | req_session = requests.Session() |
|
299 | req_session = requests.Session() | |
264 | req_session.mount( # retry max N times |
|
300 | req_session.mount( # retry max N times | |
265 | 'http://', requests.adapters.HTTPAdapter(max_retries=retries)) |
|
301 | 'http://', requests.adapters.HTTPAdapter(max_retries=retries)) | |
@@ -267,10 +303,14 b' def post_to_webhook(url_calls, settings)' | |||||
267 | method = settings.get('method_type') or 'post' |
|
303 | method = settings.get('method_type') or 'post' | |
268 | call_method = getattr(req_session, method) |
|
304 | call_method = getattr(req_session, method) | |
269 |
|
305 | |||
|
306 | headers = headers or {} | |||
|
307 | call_headers.update(headers) | |||
|
308 | ||||
270 | log.debug('calling WEBHOOK with method: %s', call_method) |
|
309 | log.debug('calling WEBHOOK with method: %s', call_method) | |
271 | resp = call_method(url, json={ |
|
310 | resp = call_method(url, json={ | |
272 | 'token': token, |
|
311 | 'token': token, | |
273 | 'event': data |
|
312 | 'event': data | |
274 | }) |
|
313 | }, headers=call_headers) | |
275 | log.debug('Got WEBHOOK response: %s', resp) |
|
314 | log.debug('Got WEBHOOK response: %s', resp) | |
|
315 | ||||
276 | resp.raise_for_status() # raise exception on a failed request |
|
316 | resp.raise_for_status() # raise exception on a failed request |
@@ -44,7 +44,8 b' def base_data():' | |||||
44 |
|
44 | |||
45 | def test_webhook_parse_url_invalid_event(): |
|
45 | def test_webhook_parse_url_invalid_event(): | |
46 | template_url = 'http://server.com/${repo_name}/build' |
|
46 | template_url = 'http://server.com/${repo_name}/build' | |
47 |
handler = WebhookHandler( |
|
47 | handler = WebhookHandler( | |
|
48 | template_url, 'secret_token', {'exmaple-header':'header-values'}) | |||
48 | with pytest.raises(ValueError) as err: |
|
49 | with pytest.raises(ValueError) as err: | |
49 | handler(events.RepoDeleteEvent(''), {}) |
|
50 | handler(events.RepoDeleteEvent(''), {}) | |
50 | assert str(err.value).startswith('event type not supported') |
|
51 | assert str(err.value).startswith('event type not supported') | |
@@ -57,24 +58,32 b' def test_webhook_parse_url_invalid_event' | |||||
57 | ('http://server.com/${branch}/build', ['http://server.com/${branch}/build']), |
|
58 | ('http://server.com/${branch}/build', ['http://server.com/${branch}/build']), | |
58 | ]) |
|
59 | ]) | |
59 | def test_webook_parse_url_for_create_event(base_data, template, expected_urls): |
|
60 | def test_webook_parse_url_for_create_event(base_data, template, expected_urls): | |
60 | handler = WebhookHandler(template, 'secret_token') |
|
61 | headers = {'exmaple-header': 'header-values'} | |
|
62 | handler = WebhookHandler( | |||
|
63 | template, 'secret_token', headers) | |||
61 | urls = handler(events.RepoCreateEvent(''), base_data) |
|
64 | urls = handler(events.RepoCreateEvent(''), base_data) | |
62 | assert urls == [(url, 'secret_token', base_data) for url in expected_urls] |
|
65 | assert urls == [ | |
|
66 | (url, 'secret_token', headers, base_data) for url in expected_urls] | |||
63 |
|
67 | |||
64 |
|
68 | |||
65 | @pytest.mark.parametrize('template,expected_urls', [ |
|
69 | @pytest.mark.parametrize('template,expected_urls', [ | |
66 | ('http://server.com/${repo_name}/${pull_request_id}', ['http://server.com/foo/999']), |
|
70 | ('http://server.com/${repo_name}/${pull_request_id}', ['http://server.com/foo/999']), | |
67 | ('http://server.com/${repo_name}/${pull_request_url}', ['http://server.com/foo/http://pr-url.com']), |
|
71 | ('http://server.com/${repo_name}/${pull_request_url}', ['http://server.com/foo/http://pr-url.com']), | |
68 | ]) |
|
72 | ]) | |
69 |
def test_webook_parse_url_for_pull_request_event( |
|
73 | def test_webook_parse_url_for_pull_request_event( | |
|
74 | base_data, template, expected_urls): | |||
|
75 | ||||
70 | base_data['pullrequest'] = { |
|
76 | base_data['pullrequest'] = { | |
71 | 'pull_request_id': 999, |
|
77 | 'pull_request_id': 999, | |
72 | 'url': 'http://pr-url.com', |
|
78 | 'url': 'http://pr-url.com', | |
73 | } |
|
79 | } | |
74 | handler = WebhookHandler(template, 'secret_token') |
|
80 | headers = {'exmaple-header': 'header-values'} | |
|
81 | handler = WebhookHandler( | |||
|
82 | template, 'secret_token', headers) | |||
75 | urls = handler(events.PullRequestCreateEvent( |
|
83 | urls = handler(events.PullRequestCreateEvent( | |
76 | AttributeDict({'target_repo': 'foo'})), base_data) |
|
84 | AttributeDict({'target_repo': 'foo'})), base_data) | |
77 | assert urls == [(url, 'secret_token', base_data) for url in expected_urls] |
|
85 | assert urls == [ | |
|
86 | (url, 'secret_token', headers, base_data) for url in expected_urls] | |||
78 |
|
87 | |||
79 |
|
88 | |||
80 | @pytest.mark.parametrize('template,expected_urls', [ |
|
89 | @pytest.mark.parametrize('template,expected_urls', [ | |
@@ -85,7 +94,8 b' def test_webook_parse_url_for_pull_reque' | |||||
85 | 'http://server.com/dev/dev-xxx', |
|
94 | 'http://server.com/dev/dev-xxx', | |
86 | 'http://server.com/dev/dev-yyy']), |
|
95 | 'http://server.com/dev/dev-yyy']), | |
87 | ]) |
|
96 | ]) | |
88 | def test_webook_parse_url_for_push_event(pylonsapp, repo_push_event, base_data, template, expected_urls): |
|
97 | def test_webook_parse_url_for_push_event( | |
|
98 | pylonsapp, repo_push_event, base_data, template, expected_urls): | |||
89 | base_data['push'] = { |
|
99 | base_data['push'] = { | |
90 | 'branches': [{'name': 'stable'}, {'name': 'dev'}], |
|
100 | 'branches': [{'name': 'stable'}, {'name': 'dev'}], | |
91 | 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'}, |
|
101 | 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'}, | |
@@ -93,6 +103,9 b' def test_webook_parse_url_for_push_event' | |||||
93 | {'branch': 'dev', 'raw_id': 'dev-xxx'}, |
|
103 | {'branch': 'dev', 'raw_id': 'dev-xxx'}, | |
94 | {'branch': 'dev', 'raw_id': 'dev-yyy'}] |
|
104 | {'branch': 'dev', 'raw_id': 'dev-yyy'}] | |
95 | } |
|
105 | } | |
96 | handler = WebhookHandler(template, 'secret_token') |
|
106 | headers = {'exmaple-header': 'header-values'} | |
|
107 | handler = WebhookHandler( | |||
|
108 | template, 'secret_token', headers) | |||
97 | urls = handler(repo_push_event, base_data) |
|
109 | urls = handler(repo_push_event, base_data) | |
98 | assert urls == [(url, 'secret_token', base_data) for url in expected_urls] |
|
110 | assert urls == [ | |
|
111 | (url, 'secret_token', headers, base_data) for url in expected_urls] |
General Comments 0
You need to be logged in to leave comments.
Login now