##// END OF EJS Templates
webhook: abstract webhook handler to base for easier re-usage.
marcink -
r2585:70fa3b0c default
parent child Browse files
Show More
@@ -1,157 +1,282 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import string
23 import collections
24 import logging
22 from rhodecode.translation import _
25 from rhodecode.translation import _
23
26
27 log = logging.getLogger(__name__)
28
24
29
25 class IntegrationTypeBase(object):
30 class IntegrationTypeBase(object):
26 """ Base class for IntegrationType plugins """
31 """ Base class for IntegrationType plugins """
27 is_dummy = False
32 is_dummy = False
28 description = ''
33 description = ''
29
34
30 @classmethod
35 @classmethod
31 def icon(cls):
36 def icon(cls):
32 return '''
37 return '''
33 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
38 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
34 <svg
39 <svg
35 xmlns:dc="http://purl.org/dc/elements/1.1/"
40 xmlns:dc="http://purl.org/dc/elements/1.1/"
36 xmlns:cc="http://creativecommons.org/ns#"
41 xmlns:cc="http://creativecommons.org/ns#"
37 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
42 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
38 xmlns:svg="http://www.w3.org/2000/svg"
43 xmlns:svg="http://www.w3.org/2000/svg"
39 xmlns="http://www.w3.org/2000/svg"
44 xmlns="http://www.w3.org/2000/svg"
40 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
45 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
41 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
46 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
42 viewBox="0 -256 1792 1792"
47 viewBox="0 -256 1792 1792"
43 id="svg3025"
48 id="svg3025"
44 version="1.1"
49 version="1.1"
45 inkscape:version="0.48.3.1 r9886"
50 inkscape:version="0.48.3.1 r9886"
46 width="100%"
51 width="100%"
47 height="100%"
52 height="100%"
48 sodipodi:docname="cog_font_awesome.svg">
53 sodipodi:docname="cog_font_awesome.svg">
49 <metadata
54 <metadata
50 id="metadata3035">
55 id="metadata3035">
51 <rdf:RDF>
56 <rdf:RDF>
52 <cc:Work
57 <cc:Work
53 rdf:about="">
58 rdf:about="">
54 <dc:format>image/svg+xml</dc:format>
59 <dc:format>image/svg+xml</dc:format>
55 <dc:type
60 <dc:type
56 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
61 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
57 </cc:Work>
62 </cc:Work>
58 </rdf:RDF>
63 </rdf:RDF>
59 </metadata>
64 </metadata>
60 <defs
65 <defs
61 id="defs3033" />
66 id="defs3033" />
62 <sodipodi:namedview
67 <sodipodi:namedview
63 pagecolor="#ffffff"
68 pagecolor="#ffffff"
64 bordercolor="#666666"
69 bordercolor="#666666"
65 borderopacity="1"
70 borderopacity="1"
66 objecttolerance="10"
71 objecttolerance="10"
67 gridtolerance="10"
72 gridtolerance="10"
68 guidetolerance="10"
73 guidetolerance="10"
69 inkscape:pageopacity="0"
74 inkscape:pageopacity="0"
70 inkscape:pageshadow="2"
75 inkscape:pageshadow="2"
71 inkscape:window-width="640"
76 inkscape:window-width="640"
72 inkscape:window-height="480"
77 inkscape:window-height="480"
73 id="namedview3031"
78 id="namedview3031"
74 showgrid="false"
79 showgrid="false"
75 inkscape:zoom="0.13169643"
80 inkscape:zoom="0.13169643"
76 inkscape:cx="896"
81 inkscape:cx="896"
77 inkscape:cy="896"
82 inkscape:cy="896"
78 inkscape:window-x="0"
83 inkscape:window-x="0"
79 inkscape:window-y="25"
84 inkscape:window-y="25"
80 inkscape:window-maximized="0"
85 inkscape:window-maximized="0"
81 inkscape:current-layer="svg3025" />
86 inkscape:current-layer="svg3025" />
82 <g
87 <g
83 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
88 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
84 id="g3027">
89 id="g3027">
85 <path
90 <path
86 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
91 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
87 id="path3029"
92 id="path3029"
88 inkscape:connector-curvature="0"
93 inkscape:connector-curvature="0"
89 style="fill:currentColor" />
94 style="fill:currentColor" />
90 </g>
95 </g>
91 </svg>
96 </svg>
92 '''
97 '''
93
98
94 def __init__(self, settings):
99 def __init__(self, settings):
95 """
100 """
96 :param settings: dict of settings to be used for the integration
101 :param settings: dict of settings to be used for the integration
97 """
102 """
98 self.settings = settings
103 self.settings = settings
99
104
100 def settings_schema(self):
105 def settings_schema(self):
101 """
106 """
102 A colander schema of settings for the integration type
107 A colander schema of settings for the integration type
103 """
108 """
104 return colander.Schema()
109 return colander.Schema()
105
110
106
111
107 class EEIntegration(IntegrationTypeBase):
112 class EEIntegration(IntegrationTypeBase):
108 description = 'Integration available in RhodeCode EE edition.'
113 description = 'Integration available in RhodeCode EE edition.'
109 is_dummy = True
114 is_dummy = True
110
115
111 def __init__(self, name, key, settings=None):
116 def __init__(self, name, key, settings=None):
112 self.display_name = name
117 self.display_name = name
113 self.key = key
118 self.key = key
114 super(EEIntegration, self).__init__(settings)
119 super(EEIntegration, self).__init__(settings)
115
120
116
121
117 # Helpers #
122 # Helpers #
118 WEBHOOK_URL_VARS = [
123 WEBHOOK_URL_VARS = [
119 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
124 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
120 ('repo_name', 'Full name of the repository'),
125 ('repo_name', 'Full name of the repository'),
121 ('repo_type', 'VCS type of repository'),
126 ('repo_type', 'VCS type of repository'),
122 ('repo_id', 'Unique id of repository'),
127 ('repo_id', 'Unique id of repository'),
123 ('repo_url', 'Repository url'),
128 ('repo_url', 'Repository url'),
124 # extra repo fields
129 # extra repo fields
125 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
130 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
126
131
127 # special attrs below that we handle, using multi-call
132 # special attrs below that we handle, using multi-call
128 ('branch', 'Name of each brach submitted, if any.'),
133 ('branch', 'Name of each brach submitted, if any.'),
129 ('commit_id', 'Id of each commit submitted, if any.'),
134 ('commit_id', 'Id of each commit submitted, if any.'),
130
135
131 # pr events vars
136 # pr events vars
132 ('pull_request_id', 'Unique ID of the pull request.'),
137 ('pull_request_id', 'Unique ID of the pull request.'),
133 ('pull_request_url', 'Pull request url.'),
138 ('pull_request_url', 'Pull request url.'),
134 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
139 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
135
140
136 # user who triggers the call
141 # user who triggers the call
137 ('username', 'User who triggered the call.'),
142 ('username', 'User who triggered the call.'),
138 ('user_id', 'User id who triggered the call.'),
143 ('user_id', 'User id who triggered the call.'),
139 ]
144 ]
140
145
141 # common vars for url template used for CI plugins. Shared with webhook
146 # common vars for url template used for CI plugins. Shared with webhook
142 CI_URL_VARS = WEBHOOK_URL_VARS
147 CI_URL_VARS = WEBHOOK_URL_VARS
143
148
144
149
150 class WebhookDataHandler(object):
151 name = 'webhook'
152
153 def __init__(self, template_url, headers):
154 self.template_url = template_url
155 self.headers = headers
156
157 def get_base_parsed_template(self, data):
158 """
159 initially parses the passed in template with some common variables
160 available on ALL calls
161 """
162 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
163 common_vars = {
164 'repo_name': data['repo']['repo_name'],
165 'repo_type': data['repo']['repo_type'],
166 'repo_id': data['repo']['repo_id'],
167 'repo_url': data['repo']['url'],
168 'username': data['actor']['username'],
169 'user_id': data['actor']['user_id'],
170 'event_name': data['name']
171 }
172
173 extra_vars = {}
174 for extra_key, extra_val in data['repo']['extra_fields'].items():
175 extra_vars['extra__{}'.format(extra_key)] = extra_val
176 common_vars.update(extra_vars)
177
178 template_url = self.template_url.replace('${extra:', '${extra__')
179 return string.Template(template_url).safe_substitute(**common_vars)
180
181 def repo_push_event_handler(self, event, data):
182 url = self.get_base_parsed_template(data)
183 url_cals = []
184 branch_data = collections.OrderedDict()
185 for obj in data['push']['branches']:
186 branch_data[obj['name']] = obj
187
188 branches_commits = collections.OrderedDict()
189 for commit in data['push']['commits']:
190 if commit.get('git_ref_change'):
191 # special case for GIT that allows creating tags,
192 # deleting branches without associated commit
193 continue
194
195 if commit['branch'] not in branches_commits:
196 branch_commits = {'branch': branch_data[commit['branch']],
197 'commits': []}
198 branches_commits[commit['branch']] = branch_commits
199
200 branch_commits = branches_commits[commit['branch']]
201 branch_commits['commits'].append(commit)
202
203 if '${branch}' in url:
204 # call it multiple times, for each branch if used in variables
205 for branch, commit_ids in branches_commits.items():
206 branch_url = string.Template(url).safe_substitute(branch=branch)
207 # call further down for each commit if used
208 if '${commit_id}' in branch_url:
209 for commit_data in commit_ids['commits']:
210 commit_id = commit_data['raw_id']
211 commit_url = string.Template(branch_url).safe_substitute(
212 commit_id=commit_id)
213 # register per-commit call
214 log.debug(
215 'register %s call(%s) to url %s',
216 self.name, event, commit_url)
217 url_cals.append(
218 (commit_url, self.headers, data))
219
220 else:
221 # register per-branch call
222 log.debug(
223 'register %s call(%s) to url %s',
224 self.name, event, branch_url)
225 url_cals.append(
226 (branch_url, self.headers, data))
227
228 else:
229 log.debug(
230 'register %s call(%s) to url %s', self.name, event, url)
231 url_cals.append((url, self.headers, data))
232
233 return url_cals
234
235 def repo_create_event_handler(self, event, data):
236 url = self.get_base_parsed_template(data)
237 log.debug(
238 'register %s call(%s) to url %s', self.name, event, url)
239 return [(url, self.headers, data)]
240
241 def pull_request_event_handler(self, event, data):
242 url = self.get_base_parsed_template(data)
243 log.debug(
244 'register %s call(%s) to url %s', self.name, event, url)
245 url = string.Template(url).safe_substitute(
246 pull_request_id=data['pullrequest']['pull_request_id'],
247 pull_request_url=data['pullrequest']['url'],
248 pull_request_shadow_url=data['pullrequest']['shadow_url'],)
249 return [(url, self.headers, data)]
250
251 def __call__(self, event, data):
252 from rhodecode import events
253
254 if isinstance(event, events.RepoPushEvent):
255 return self.repo_push_event_handler(event, data)
256 elif isinstance(event, events.RepoCreateEvent):
257 return self.repo_create_event_handler(event, data)
258 elif isinstance(event, events.PullRequestEvent):
259 return self.pull_request_event_handler(event, data)
260 else:
261 raise ValueError(
262 'event type `%s` not in supported list: %s' % (
263 event.__class__, events))
264
265
145 def get_auth(settings):
266 def get_auth(settings):
146 from requests.auth import HTTPBasicAuth
267 from requests.auth import HTTPBasicAuth
147 username = settings.get('username')
268 username = settings.get('username')
148 password = settings.get('password')
269 password = settings.get('password')
149 if username and password:
270 if username and password:
150 return HTTPBasicAuth(username, password)
271 return HTTPBasicAuth(username, password)
151 return None
272 return None
152
273
153
274
275 def get_web_token(settings):
276 return settings['secret_token']
277
278
154 def get_url_vars(url_vars):
279 def get_url_vars(url_vars):
155 return '\n'.join(
280 return '\n'.join(
156 '{} - {}'.format('${' + key + '}', explanation)
281 '{} - {}'.format('${' + key + '}', explanation)
157 for key, explanation in url_vars)
282 for key, explanation in url_vars)
@@ -1,381 +1,274 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import string
23 import collections
24
22
25 import deform
23 import deform
26 import deform.widget
24 import deform.widget
27 import logging
25 import logging
28 import requests
26 import requests
29 import requests.adapters
27 import requests.adapters
30 import colander
28 import colander
31 from requests.packages.urllib3.util.retry import Retry
29 from requests.packages.urllib3.util.retry import Retry
32
30
33 import rhodecode
31 import rhodecode
34 from rhodecode import events
32 from rhodecode import events
35 from rhodecode.translation import _
33 from rhodecode.translation import _
36 from rhodecode.integrations.types.base import (
34 from rhodecode.integrations.types.base import (
37 IntegrationTypeBase, get_auth, get_url_vars, WEBHOOK_URL_VARS)
35 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
36 WebhookDataHandler, WEBHOOK_URL_VARS)
38 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
39 from rhodecode.model.validation_schema import widgets
38 from rhodecode.model.validation_schema import widgets
40
39
41 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
42
41
43
42
44 # updating this required to update the `common_vars` passed in url calling func
43 # updating this required to update the `common_vars` passed in url calling func
45
44
46 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
45 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
47
46
48
47
49 class WebhookHandler(object):
50 def __init__(self, template_url, secret_token, headers):
51 self.template_url = template_url
52 self.secret_token = secret_token
53 self.headers = headers
54
55 def get_base_parsed_template(self, data):
56 """
57 initially parses the passed in template with some common variables
58 available on ALL calls
59 """
60 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
61 common_vars = {
62 'repo_name': data['repo']['repo_name'],
63 'repo_type': data['repo']['repo_type'],
64 'repo_id': data['repo']['repo_id'],
65 'repo_url': data['repo']['url'],
66 'username': data['actor']['username'],
67 'user_id': data['actor']['user_id'],
68 'event_name': data['name']
69 }
70
71 extra_vars = {}
72 for extra_key, extra_val in data['repo']['extra_fields'].items():
73 extra_vars['extra__{}'.format(extra_key)] = extra_val
74 common_vars.update(extra_vars)
75
76 template_url = self.template_url.replace('${extra:', '${extra__')
77 return string.Template(template_url).safe_substitute(**common_vars)
78
79 def repo_push_event_handler(self, event, data):
80 url = self.get_base_parsed_template(data)
81 url_cals = []
82 branch_data = collections.OrderedDict()
83 for obj in data['push']['branches']:
84 branch_data[obj['name']] = obj
85
86 branches_commits = collections.OrderedDict()
87 for commit in data['push']['commits']:
88 if commit.get('git_ref_change'):
89 # special case for GIT that allows creating tags,
90 # deleting branches without associated commit
91 continue
92
93 if commit['branch'] not in branches_commits:
94 branch_commits = {'branch': branch_data[commit['branch']],
95 'commits': []}
96 branches_commits[commit['branch']] = branch_commits
97
98 branch_commits = branches_commits[commit['branch']]
99 branch_commits['commits'].append(commit)
100
101 if '${branch}' in url:
102 # call it multiple times, for each branch if used in variables
103 for branch, commit_ids in branches_commits.items():
104 branch_url = string.Template(url).safe_substitute(branch=branch)
105 # call further down for each commit if used
106 if '${commit_id}' in branch_url:
107 for commit_data in commit_ids['commits']:
108 commit_id = commit_data['raw_id']
109 commit_url = string.Template(branch_url).safe_substitute(
110 commit_id=commit_id)
111 # register per-commit call
112 log.debug(
113 'register webhook call(%s) to url %s', event, commit_url)
114 url_cals.append((commit_url, self.secret_token, self.headers, data))
115
116 else:
117 # register per-branch call
118 log.debug(
119 'register webhook call(%s) to url %s', event, branch_url)
120 url_cals.append((branch_url, self.secret_token, self.headers, data))
121
122 else:
123 log.debug(
124 'register webhook call(%s) to url %s', event, url)
125 url_cals.append((url, self.secret_token, self.headers, data))
126
127 return url_cals
128
129 def repo_create_event_handler(self, event, data):
130 url = self.get_base_parsed_template(data)
131 log.debug(
132 'register webhook call(%s) to url %s', event, url)
133 return [(url, self.secret_token, self.headers, data)]
134
135 def pull_request_event_handler(self, event, data):
136 url = self.get_base_parsed_template(data)
137 log.debug(
138 'register webhook call(%s) to url %s', event, url)
139 url = string.Template(url).safe_substitute(
140 pull_request_id=data['pullrequest']['pull_request_id'],
141 pull_request_url=data['pullrequest']['url'],
142 pull_request_shadow_url=data['pullrequest']['shadow_url'],)
143 return [(url, self.secret_token, self.headers, data)]
144
145 def __call__(self, event, data):
146 if isinstance(event, events.RepoPushEvent):
147 return self.repo_push_event_handler(event, data)
148 elif isinstance(event, events.RepoCreateEvent):
149 return self.repo_create_event_handler(event, data)
150 elif isinstance(event, events.PullRequestEvent):
151 return self.pull_request_event_handler(event, data)
152 else:
153 raise ValueError('event type not supported: %s' % events)
154
155
156 class WebhookSettingsSchema(colander.Schema):
48 class WebhookSettingsSchema(colander.Schema):
157 url = colander.SchemaNode(
49 url = colander.SchemaNode(
158 colander.String(),
50 colander.String(),
159 title=_('Webhook URL'),
51 title=_('Webhook URL'),
160 description=
52 description=
161 _('URL to which Webhook should submit data. If used some of the '
53 _('URL to which Webhook should submit data. If used some of the '
162 'variables would trigger multiple calls, like ${branch} or '
54 'variables would trigger multiple calls, like ${branch} or '
163 '${commit_id}. Webhook will be called as many times as unique '
55 '${commit_id}. Webhook will be called as many times as unique '
164 'objects in data in such cases.'),
56 'objects in data in such cases.'),
165 missing=colander.required,
57 missing=colander.required,
166 required=True,
58 required=True,
167 validator=colander.url,
59 validator=colander.url,
168 widget=widgets.CodeMirrorWidget(
60 widget=widgets.CodeMirrorWidget(
169 help_block_collapsable_name='Show url variables',
61 help_block_collapsable_name='Show url variables',
170 help_block_collapsable=(
62 help_block_collapsable=(
171 'E.g http://my-serv/trigger_job/${{event_name}}'
63 'E.g http://my-serv/trigger_job/${{event_name}}'
172 '?PR_ID=${{pull_request_id}}'
64 '?PR_ID=${{pull_request_id}}'
173 '\nFull list of vars:\n{}'.format(URL_VARS)),
65 '\nFull list of vars:\n{}'.format(URL_VARS)),
174 codemirror_mode='text',
66 codemirror_mode='text',
175 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
67 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
176 )
68 )
177 secret_token = colander.SchemaNode(
69 secret_token = colander.SchemaNode(
178 colander.String(),
70 colander.String(),
179 title=_('Secret Token'),
71 title=_('Secret Token'),
180 description=_('Optional string used to validate received payloads. '
72 description=_('Optional string used to validate received payloads. '
181 'It will be sent together with event data in JSON'),
73 'It will be sent together with event data in JSON'),
182 default='',
74 default='',
183 missing='',
75 missing='',
184 widget=deform.widget.TextInputWidget(
76 widget=deform.widget.TextInputWidget(
185 placeholder='e.g. secret_token'
77 placeholder='e.g. secret_token'
186 ),
78 ),
187 )
79 )
188 username = colander.SchemaNode(
80 username = colander.SchemaNode(
189 colander.String(),
81 colander.String(),
190 title=_('Username'),
82 title=_('Username'),
191 description=_('Optional username to authenticate the call.'),
83 description=_('Optional username to authenticate the call.'),
192 default='',
84 default='',
193 missing='',
85 missing='',
194 widget=deform.widget.TextInputWidget(
86 widget=deform.widget.TextInputWidget(
195 placeholder='e.g. admin'
87 placeholder='e.g. admin'
196 ),
88 ),
197 )
89 )
198 password = colander.SchemaNode(
90 password = colander.SchemaNode(
199 colander.String(),
91 colander.String(),
200 title=_('Password'),
92 title=_('Password'),
201 description=_('Optional password to authenticate the call.'),
93 description=_('Optional password to authenticate the call.'),
202 default='',
94 default='',
203 missing='',
95 missing='',
204 widget=deform.widget.PasswordWidget(
96 widget=deform.widget.PasswordWidget(
205 placeholder='e.g. secret.',
97 placeholder='e.g. secret.',
206 redisplay=True,
98 redisplay=True,
207 ),
99 ),
208 )
100 )
209 custom_header_key = colander.SchemaNode(
101 custom_header_key = colander.SchemaNode(
210 colander.String(),
102 colander.String(),
211 title=_('Custom Header Key'),
103 title=_('Custom Header Key'),
212 description=_('Custom Header name to be set when calling endpoint.'),
104 description=_('Custom Header name to be set when calling endpoint.'),
213 default='',
105 default='',
214 missing='',
106 missing='',
215 widget=deform.widget.TextInputWidget(
107 widget=deform.widget.TextInputWidget(
216 placeholder='e.g: Authorization'
108 placeholder='e.g: Authorization'
217 ),
109 ),
218 )
110 )
219 custom_header_val = colander.SchemaNode(
111 custom_header_val = colander.SchemaNode(
220 colander.String(),
112 colander.String(),
221 title=_('Custom Header Value'),
113 title=_('Custom Header Value'),
222 description=_('Custom Header value to be set when calling endpoint.'),
114 description=_('Custom Header value to be set when calling endpoint.'),
223 default='',
115 default='',
224 missing='',
116 missing='',
225 widget=deform.widget.TextInputWidget(
117 widget=deform.widget.TextInputWidget(
226 placeholder='e.g. Basic XxXxXx'
118 placeholder='e.g. Basic XxXxXx'
227 ),
119 ),
228 )
120 )
229 method_type = colander.SchemaNode(
121 method_type = colander.SchemaNode(
230 colander.String(),
122 colander.String(),
231 title=_('Call Method'),
123 title=_('Call Method'),
232 description=_('Select if the Webhook call should be made '
124 description=_('Select if the Webhook call should be made '
233 'with POST or GET.'),
125 'with POST or GET.'),
234 default='post',
126 default='post',
235 missing='',
127 missing='',
236 widget=deform.widget.RadioChoiceWidget(
128 widget=deform.widget.RadioChoiceWidget(
237 values=[('get', 'GET'), ('post', 'POST')],
129 values=[('get', 'GET'), ('post', 'POST')],
238 inline=True
130 inline=True
239 ),
131 ),
240 )
132 )
241
133
242
134
243 class WebhookIntegrationType(IntegrationTypeBase):
135 class WebhookIntegrationType(IntegrationTypeBase):
244 key = 'webhook'
136 key = 'webhook'
245 display_name = _('Webhook')
137 display_name = _('Webhook')
246 description = _('Post json events to a Webhook endpoint')
138 description = _('send JSON data to a url endpoint')
247
139
248 @classmethod
140 @classmethod
249 def icon(cls):
141 def icon(cls):
250 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
142 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
251
143
252 valid_events = [
144 valid_events = [
253 events.PullRequestCloseEvent,
145 events.PullRequestCloseEvent,
254 events.PullRequestMergeEvent,
146 events.PullRequestMergeEvent,
255 events.PullRequestUpdateEvent,
147 events.PullRequestUpdateEvent,
256 events.PullRequestCommentEvent,
148 events.PullRequestCommentEvent,
257 events.PullRequestReviewEvent,
149 events.PullRequestReviewEvent,
258 events.PullRequestCreateEvent,
150 events.PullRequestCreateEvent,
259 events.RepoPushEvent,
151 events.RepoPushEvent,
260 events.RepoCreateEvent,
152 events.RepoCreateEvent,
261 ]
153 ]
262
154
263 def settings_schema(self):
155 def settings_schema(self):
264 schema = WebhookSettingsSchema()
156 schema = WebhookSettingsSchema()
265 schema.add(colander.SchemaNode(
157 schema.add(colander.SchemaNode(
266 colander.Set(),
158 colander.Set(),
267 widget=deform.widget.CheckboxChoiceWidget(
159 widget=deform.widget.CheckboxChoiceWidget(
268 values=sorted(
160 values=sorted(
269 [(e.name, e.display_name) for e in self.valid_events]
161 [(e.name, e.display_name) for e in self.valid_events]
270 )
162 )
271 ),
163 ),
272 description="Events activated for this integration",
164 description="Events activated for this integration",
273 name='events'
165 name='events'
274 ))
166 ))
275 return schema
167 return schema
276
168
277 def send_event(self, event):
169 def send_event(self, event):
278 log.debug('handling event %s with Webhook integration %s',
170 log.debug(
279 event.name, self)
171 'handling event %s with Webhook integration %s', event.name, self)
280
172
281 if event.__class__ not in self.valid_events:
173 if event.__class__ not in self.valid_events:
282 log.debug('event not valid: %r' % event)
174 log.debug('event not valid: %r' % event)
283 return
175 return
284
176
285 if event.name not in self.settings['events']:
177 if event.name not in self.settings['events']:
286 log.debug('event ignored: %r' % event)
178 log.debug('event ignored: %r' % event)
287 return
179 return
288
180
289 data = event.as_dict()
181 data = event.as_dict()
290 template_url = self.settings['url']
182 template_url = self.settings['url']
291
183
292 headers = {}
184 headers = {}
293 head_key = self.settings.get('custom_header_key')
185 head_key = self.settings.get('custom_header_key')
294 head_val = self.settings.get('custom_header_val')
186 head_val = self.settings.get('custom_header_val')
295 if head_key and head_val:
187 if head_key and head_val:
296 headers = {head_key: head_val}
188 headers = {head_key: head_val}
297
189
298 handler = WebhookHandler(
190 handler = WebhookDataHandler(template_url, headers)
299 template_url, self.settings['secret_token'], headers)
300
191
301 url_calls = handler(event, data)
192 url_calls = handler(event, data)
302 log.debug('webhook: calling following urls: %s',
193 log.debug('webhook: calling following urls: %s',
303 [x[0] for x in url_calls])
194 [x[0] for x in url_calls])
304
195
305 run_task(post_to_webhook, url_calls, self.settings)
196 run_task(post_to_webhook, url_calls, self.settings)
306
197
307
198
308 @async_task(ignore_result=True, base=RequestContextTask)
199 @async_task(ignore_result=True, base=RequestContextTask)
309 def post_to_webhook(url_calls, settings):
200 def post_to_webhook(url_calls, settings):
310 """
201 """
311 Example data::
202 Example data::
312
203
313 {'actor': {'user_id': 2, 'username': u'admin'},
204 {'actor': {'user_id': 2, 'username': u'admin'},
314 'actor_ip': u'192.168.157.1',
205 'actor_ip': u'192.168.157.1',
315 'name': 'repo-push',
206 'name': 'repo-push',
316 'push': {'branches': [{'name': u'default',
207 'push': {'branches': [{'name': u'default',
317 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
208 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
318 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
209 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
319 'branch': u'default',
210 'branch': u'default',
320 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
211 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
321 'issues': [],
212 'issues': [],
322 'mentions': [],
213 'mentions': [],
323 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
214 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
324 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
215 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
325 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
216 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
326 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
217 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
327 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
218 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
328 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
219 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
329 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
220 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
330 'reviewers': [],
221 'reviewers': [],
331 'revision': 9L,
222 'revision': 9L,
332 'short_id': 'a815cc738b96',
223 'short_id': 'a815cc738b96',
333 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
224 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
334 'issues': {}},
225 'issues': {}},
335 'repo': {'extra_fields': '',
226 'repo': {'extra_fields': '',
336 'permalink_url': u'http://rc.local:8080/_7',
227 'permalink_url': u'http://rc.local:8080/_7',
337 'repo_id': 7,
228 'repo_id': 7,
338 'repo_name': u'hg-repo',
229 'repo_name': u'hg-repo',
339 'repo_type': u'hg',
230 'repo_type': u'hg',
340 'url': u'http://rc.local:8080/hg-repo'},
231 'url': u'http://rc.local:8080/hg-repo'},
341 'server_url': u'http://rc.local:8080',
232 'server_url': u'http://rc.local:8080',
342 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
233 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
343
234
344 """
235 """
345 max_retries = 3
236 max_retries = 3
346 retries = Retry(
237 retries = Retry(
347 total=max_retries,
238 total=max_retries,
348 backoff_factor=0.15,
239 backoff_factor=0.15,
349 status_forcelist=[500, 502, 503, 504])
240 status_forcelist=[500, 502, 503, 504])
350 call_headers = {
241 call_headers = {
351 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
242 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
352 rhodecode.__version__)
243 rhodecode.__version__)
353 } # updated below with custom ones, allows override
244 } # updated below with custom ones, allows override
354
245
355 auth = get_auth(settings)
246 auth = get_auth(settings)
356 for url, token, headers, data in url_calls:
247 token = get_web_token(settings)
248
249 for url, headers, data in url_calls:
357 req_session = requests.Session()
250 req_session = requests.Session()
358 req_session.mount( # retry max N times
251 req_session.mount( # retry max N times
359 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
252 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
360
253
361 method = settings.get('method_type') or 'post'
254 method = settings.get('method_type') or 'post'
362 call_method = getattr(req_session, method)
255 call_method = getattr(req_session, method)
363
256
364 headers = headers or {}
257 headers = headers or {}
365 call_headers.update(headers)
258 call_headers.update(headers)
366
259
367 log.debug('calling Webhook with method: %s, and auth:%s',
260 log.debug('calling Webhook with method: %s, and auth:%s',
368 call_method, auth)
261 call_method, auth)
369 if settings.get('log_data'):
262 if settings.get('log_data'):
370 log.debug('calling webhook with data: %s', data)
263 log.debug('calling webhook with data: %s', data)
371 resp = call_method(url, json={
264 resp = call_method(url, json={
372 'token': token,
265 'token': token,
373 'event': data
266 'event': data
374 }, headers=call_headers, auth=auth)
267 }, headers=call_headers, auth=auth)
375 log.debug('Got Webhook response: %s', resp)
268 log.debug('Got Webhook response: %s', resp)
376
269
377 try:
270 try:
378 resp.raise_for_status() # raise exception on a failed request
271 resp.raise_for_status() # raise exception on a failed request
379 except Exception:
272 except Exception:
380 log.error(resp.text)
273 log.error(resp.text)
381 raise
274 raise
@@ -1,111 +1,122 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode import events
23 from rhodecode import events
24 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.integrations.types.webhook import WebhookHandler
25 from rhodecode.integrations.types.webhook import WebhookDataHandler
26
26
27
27
28 @pytest.fixture
28 @pytest.fixture
29 def base_data():
29 def base_data():
30 return {
30 return {
31 'name': 'event',
31 'repo': {
32 'repo': {
32 'repo_name': 'foo',
33 'repo_name': 'foo',
33 'repo_type': 'hg',
34 'repo_type': 'hg',
34 'repo_id': '12',
35 'repo_id': '12',
35 'url': 'http://repo.url/foo',
36 'url': 'http://repo.url/foo',
36 'extra_fields': {},
37 'extra_fields': {},
37 },
38 },
38 'actor': {
39 'actor': {
39 'username': 'actor_name',
40 'username': 'actor_name',
40 'user_id': 1
41 'user_id': 1
41 }
42 }
42 }
43 }
43
44
44
45
45 def test_webhook_parse_url_invalid_event():
46 def test_webhook_parse_url_invalid_event():
46 template_url = 'http://server.com/${repo_name}/build'
47 template_url = 'http://server.com/${repo_name}/build'
47 handler = WebhookHandler(
48 handler = WebhookDataHandler(
48 template_url, 'secret_token', {'exmaple-header':'header-values'})
49 template_url, {'exmaple-header': 'header-values'})
50 event = events.RepoDeleteEvent('')
49 with pytest.raises(ValueError) as err:
51 with pytest.raises(ValueError) as err:
50 handler(events.RepoDeleteEvent(''), {})
52 handler(event, {})
51 assert str(err.value).startswith('event type not supported')
53
54 err = str(err.value)
55 assert err.startswith(
56 'event type `%s` not in supported list' % event.__class__)
52
57
53
58
54 @pytest.mark.parametrize('template,expected_urls', [
59 @pytest.mark.parametrize('template,expected_urls', [
55 ('http://server.com/${repo_name}/build', ['http://server.com/foo/build']),
60 ('http://server.com/${repo_name}/build',
56 ('http://server.com/${repo_name}/${repo_type}', ['http://server.com/foo/hg']),
61 ['http://server.com/foo/build']),
57 ('http://${server}.com/${repo_name}/${repo_id}', ['http://${server}.com/foo/12']),
62 ('http://server.com/${repo_name}/${repo_type}',
58 ('http://server.com/${branch}/build', ['http://server.com/${branch}/build']),
63 ['http://server.com/foo/hg']),
64 ('http://${server}.com/${repo_name}/${repo_id}',
65 ['http://${server}.com/foo/12']),
66 ('http://server.com/${branch}/build',
67 ['http://server.com/${branch}/build']),
59 ])
68 ])
60 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
69 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
61 headers = {'exmaple-header': 'header-values'}
70 headers = {'exmaple-header': 'header-values'}
62 handler = WebhookHandler(
71 handler = WebhookDataHandler(template, headers)
63 template, 'secret_token', headers)
64 urls = handler(events.RepoCreateEvent(''), base_data)
72 urls = handler(events.RepoCreateEvent(''), base_data)
65 assert urls == [
73 assert urls == [
66 (url, 'secret_token', headers, base_data) for url in expected_urls]
74 (url, headers, base_data) for url in expected_urls]
67
75
68
76
69 @pytest.mark.parametrize('template,expected_urls', [
77 @pytest.mark.parametrize('template,expected_urls', [
70 ('http://server.com/${repo_name}/${pull_request_id}', ['http://server.com/foo/999']),
78 ('http://server.com/${repo_name}/${pull_request_id}',
71 ('http://server.com/${repo_name}/${pull_request_url}', ['http://server.com/foo/http://pr-url.com']),
79 ['http://server.com/foo/999']),
80 ('http://server.com/${repo_name}/${pull_request_url}',
81 ['http://server.com/foo/http://pr-url.com']),
72 ])
82 ])
73 def test_webook_parse_url_for_pull_request_event(
83 def test_webook_parse_url_for_pull_request_event(
74 base_data, template, expected_urls):
84 base_data, template, expected_urls):
75
85
76 base_data['pullrequest'] = {
86 base_data['pullrequest'] = {
77 'pull_request_id': 999,
87 'pull_request_id': 999,
78 'url': 'http://pr-url.com',
88 'url': 'http://pr-url.com',
89 'shadow_url': 'http://pr-url.com/repository'
79 }
90 }
80 headers = {'exmaple-header': 'header-values'}
91 headers = {'exmaple-header': 'header-values'}
81 handler = WebhookHandler(
92 handler = WebhookDataHandler(template, headers)
82 template, 'secret_token', headers)
83 urls = handler(events.PullRequestCreateEvent(
93 urls = handler(events.PullRequestCreateEvent(
84 AttributeDict({'target_repo': 'foo'})), base_data)
94 AttributeDict({'target_repo': 'foo'})), base_data)
85 assert urls == [
95 assert urls == [
86 (url, 'secret_token', headers, base_data) for url in expected_urls]
96 (url, headers, base_data) for url in expected_urls]
87
97
88
98
89 @pytest.mark.parametrize('template,expected_urls', [
99 @pytest.mark.parametrize('template,expected_urls', [
90 ('http://server.com/${branch}/build', ['http://server.com/stable/build',
100 ('http://server.com/${branch}/build',
91 'http://server.com/dev/build']),
101 ['http://server.com/stable/build',
92 ('http://server.com/${branch}/${commit_id}', ['http://server.com/stable/stable-xxx',
102 'http://server.com/dev/build']),
93 'http://server.com/stable/stable-yyy',
103 ('http://server.com/${branch}/${commit_id}',
94 'http://server.com/dev/dev-xxx',
104 ['http://server.com/stable/stable-xxx',
95 'http://server.com/dev/dev-yyy']),
105 'http://server.com/stable/stable-yyy',
106 'http://server.com/dev/dev-xxx',
107 'http://server.com/dev/dev-yyy']),
96 ])
108 ])
97 def test_webook_parse_url_for_push_event(
109 def test_webook_parse_url_for_push_event(
98 baseapp, repo_push_event, base_data, template, expected_urls):
110 baseapp, repo_push_event, base_data, template, expected_urls):
99 base_data['push'] = {
111 base_data['push'] = {
100 'branches': [{'name': 'stable'}, {'name': 'dev'}],
112 'branches': [{'name': 'stable'}, {'name': 'dev'}],
101 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
113 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
102 {'branch': 'stable', 'raw_id': 'stable-yyy'},
114 {'branch': 'stable', 'raw_id': 'stable-yyy'},
103 {'branch': 'dev', 'raw_id': 'dev-xxx'},
115 {'branch': 'dev', 'raw_id': 'dev-xxx'},
104 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
116 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
105 }
117 }
106 headers = {'exmaple-header': 'header-values'}
118 headers = {'exmaple-header': 'header-values'}
107 handler = WebhookHandler(
119 handler = WebhookDataHandler(template, headers)
108 template, 'secret_token', headers)
109 urls = handler(repo_push_event, base_data)
120 urls = handler(repo_push_event, base_data)
110 assert urls == [
121 assert urls == [
111 (url, 'secret_token', headers, base_data) for url in expected_urls]
122 (url, headers, base_data) for url in expected_urls]
General Comments 0
You need to be logged in to leave comments. Login now