##// END OF EJS Templates
webhook: quote URL variables to prevent url errors with special chars like # in pr title.
ergo -
r3477:976a0af2 default
parent child Browse files
Show More
@@ -1,355 +1,363 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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
22 import string
23 import collections
23 import collections
24 import logging
24 import logging
25 import requests
25 import requests
26 import urllib
26 from requests.adapters import HTTPAdapter
27 from requests.adapters import HTTPAdapter
27 from requests.packages.urllib3.util.retry import Retry
28 from requests.packages.urllib3.util.retry import Retry
28
29
29 from mako import exceptions
30 from mako import exceptions
30
31
32 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.translation import _
33 from rhodecode.translation import _
32
34
33
35
34 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
35
37
36
38
39 class UrlTmpl(string.Template):
40
41 def safe_substitute(self, **kws):
42 # url encode the kw for usage in url
43 kws = {k: urllib.quote(safe_str(v)) for k, v in kws.items()}
44 return super(UrlTmpl, self).safe_substitute(**kws)
45
46
37 class IntegrationTypeBase(object):
47 class IntegrationTypeBase(object):
38 """ Base class for IntegrationType plugins """
48 """ Base class for IntegrationType plugins """
39 is_dummy = False
49 is_dummy = False
40 description = ''
50 description = ''
41
51
42 @classmethod
52 @classmethod
43 def icon(cls):
53 def icon(cls):
44 return '''
54 return '''
45 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
55 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
46 <svg
56 <svg
47 xmlns:dc="http://purl.org/dc/elements/1.1/"
57 xmlns:dc="http://purl.org/dc/elements/1.1/"
48 xmlns:cc="http://creativecommons.org/ns#"
58 xmlns:cc="http://creativecommons.org/ns#"
49 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
59 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
50 xmlns:svg="http://www.w3.org/2000/svg"
60 xmlns:svg="http://www.w3.org/2000/svg"
51 xmlns="http://www.w3.org/2000/svg"
61 xmlns="http://www.w3.org/2000/svg"
52 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
62 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
53 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
63 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
54 viewBox="0 -256 1792 1792"
64 viewBox="0 -256 1792 1792"
55 id="svg3025"
65 id="svg3025"
56 version="1.1"
66 version="1.1"
57 inkscape:version="0.48.3.1 r9886"
67 inkscape:version="0.48.3.1 r9886"
58 width="100%"
68 width="100%"
59 height="100%"
69 height="100%"
60 sodipodi:docname="cog_font_awesome.svg">
70 sodipodi:docname="cog_font_awesome.svg">
61 <metadata
71 <metadata
62 id="metadata3035">
72 id="metadata3035">
63 <rdf:RDF>
73 <rdf:RDF>
64 <cc:Work
74 <cc:Work
65 rdf:about="">
75 rdf:about="">
66 <dc:format>image/svg+xml</dc:format>
76 <dc:format>image/svg+xml</dc:format>
67 <dc:type
77 <dc:type
68 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
78 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
69 </cc:Work>
79 </cc:Work>
70 </rdf:RDF>
80 </rdf:RDF>
71 </metadata>
81 </metadata>
72 <defs
82 <defs
73 id="defs3033" />
83 id="defs3033" />
74 <sodipodi:namedview
84 <sodipodi:namedview
75 pagecolor="#ffffff"
85 pagecolor="#ffffff"
76 bordercolor="#666666"
86 bordercolor="#666666"
77 borderopacity="1"
87 borderopacity="1"
78 objecttolerance="10"
88 objecttolerance="10"
79 gridtolerance="10"
89 gridtolerance="10"
80 guidetolerance="10"
90 guidetolerance="10"
81 inkscape:pageopacity="0"
91 inkscape:pageopacity="0"
82 inkscape:pageshadow="2"
92 inkscape:pageshadow="2"
83 inkscape:window-width="640"
93 inkscape:window-width="640"
84 inkscape:window-height="480"
94 inkscape:window-height="480"
85 id="namedview3031"
95 id="namedview3031"
86 showgrid="false"
96 showgrid="false"
87 inkscape:zoom="0.13169643"
97 inkscape:zoom="0.13169643"
88 inkscape:cx="896"
98 inkscape:cx="896"
89 inkscape:cy="896"
99 inkscape:cy="896"
90 inkscape:window-x="0"
100 inkscape:window-x="0"
91 inkscape:window-y="25"
101 inkscape:window-y="25"
92 inkscape:window-maximized="0"
102 inkscape:window-maximized="0"
93 inkscape:current-layer="svg3025" />
103 inkscape:current-layer="svg3025" />
94 <g
104 <g
95 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
105 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
96 id="g3027">
106 id="g3027">
97 <path
107 <path
98 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"
108 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"
99 id="path3029"
109 id="path3029"
100 inkscape:connector-curvature="0"
110 inkscape:connector-curvature="0"
101 style="fill:currentColor" />
111 style="fill:currentColor" />
102 </g>
112 </g>
103 </svg>
113 </svg>
104 '''
114 '''
105
115
106 def __init__(self, settings):
116 def __init__(self, settings):
107 """
117 """
108 :param settings: dict of settings to be used for the integration
118 :param settings: dict of settings to be used for the integration
109 """
119 """
110 self.settings = settings
120 self.settings = settings
111
121
112 def settings_schema(self):
122 def settings_schema(self):
113 """
123 """
114 A colander schema of settings for the integration type
124 A colander schema of settings for the integration type
115 """
125 """
116 return colander.Schema()
126 return colander.Schema()
117
127
118
128
119 class EEIntegration(IntegrationTypeBase):
129 class EEIntegration(IntegrationTypeBase):
120 description = 'Integration available in RhodeCode EE edition.'
130 description = 'Integration available in RhodeCode EE edition.'
121 is_dummy = True
131 is_dummy = True
122
132
123 def __init__(self, name, key, settings=None):
133 def __init__(self, name, key, settings=None):
124 self.display_name = name
134 self.display_name = name
125 self.key = key
135 self.key = key
126 super(EEIntegration, self).__init__(settings)
136 super(EEIntegration, self).__init__(settings)
127
137
128
138
129 # Helpers #
139 # Helpers #
130 # updating this required to update the `common_vars` as well.
140 # updating this required to update the `common_vars` as well.
131 WEBHOOK_URL_VARS = [
141 WEBHOOK_URL_VARS = [
132 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
142 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
133 ('repo_name', 'Full name of the repository'),
143 ('repo_name', 'Full name of the repository'),
134 ('repo_type', 'VCS type of repository'),
144 ('repo_type', 'VCS type of repository'),
135 ('repo_id', 'Unique id of repository'),
145 ('repo_id', 'Unique id of repository'),
136 ('repo_url', 'Repository url'),
146 ('repo_url', 'Repository url'),
137 # extra repo fields
147 # extra repo fields
138 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
148 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
139
149
140 # special attrs below that we handle, using multi-call
150 # special attrs below that we handle, using multi-call
141 ('branch', 'Name of each branch submitted, if any.'),
151 ('branch', 'Name of each branch submitted, if any.'),
142 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
152 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
143 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
153 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
144
154
145 # pr events vars
155 # pr events vars
146 ('pull_request_id', 'Unique ID of the pull request.'),
156 ('pull_request_id', 'Unique ID of the pull request.'),
147 ('pull_request_title', 'Title of the pull request.'),
157 ('pull_request_title', 'Title of the pull request.'),
148 ('pull_request_url', 'Pull request url.'),
158 ('pull_request_url', 'Pull request url.'),
149 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
159 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
150 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
160 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
151 'Changes after PR update'),
161 'Changes after PR update'),
152
162
153 # user who triggers the call
163 # user who triggers the call
154 ('username', 'User who triggered the call.'),
164 ('username', 'User who triggered the call.'),
155 ('user_id', 'User id who triggered the call.'),
165 ('user_id', 'User id who triggered the call.'),
156 ]
166 ]
157
167
158 # common vars for url template used for CI plugins. Shared with webhook
168 # common vars for url template used for CI plugins. Shared with webhook
159 CI_URL_VARS = WEBHOOK_URL_VARS
169 CI_URL_VARS = WEBHOOK_URL_VARS
160
170
161
171
162 class CommitParsingDataHandler(object):
172 class CommitParsingDataHandler(object):
163
173
164 def aggregate_branch_data(self, branches, commits):
174 def aggregate_branch_data(self, branches, commits):
165 branch_data = collections.OrderedDict()
175 branch_data = collections.OrderedDict()
166 for obj in branches:
176 for obj in branches:
167 branch_data[obj['name']] = obj
177 branch_data[obj['name']] = obj
168
178
169 branches_commits = collections.OrderedDict()
179 branches_commits = collections.OrderedDict()
170 for commit in commits:
180 for commit in commits:
171 if commit.get('git_ref_change'):
181 if commit.get('git_ref_change'):
172 # special case for GIT that allows creating tags,
182 # special case for GIT that allows creating tags,
173 # deleting branches without associated commit
183 # deleting branches without associated commit
174 continue
184 continue
175 commit_branch = commit['branch']
185 commit_branch = commit['branch']
176
186
177 if commit_branch not in branches_commits:
187 if commit_branch not in branches_commits:
178 _branch = branch_data[commit_branch] \
188 _branch = branch_data[commit_branch] \
179 if commit_branch else commit_branch
189 if commit_branch else commit_branch
180 branch_commits = {'branch': _branch,
190 branch_commits = {'branch': _branch,
181 'branch_head': '',
191 'branch_head': '',
182 'commits': []}
192 'commits': []}
183 branches_commits[commit_branch] = branch_commits
193 branches_commits[commit_branch] = branch_commits
184
194
185 branch_commits = branches_commits[commit_branch]
195 branch_commits = branches_commits[commit_branch]
186 branch_commits['commits'].append(commit)
196 branch_commits['commits'].append(commit)
187 branch_commits['branch_head'] = commit['raw_id']
197 branch_commits['branch_head'] = commit['raw_id']
188 return branches_commits
198 return branches_commits
189
199
190
200
191 class WebhookDataHandler(CommitParsingDataHandler):
201 class WebhookDataHandler(CommitParsingDataHandler):
192 name = 'webhook'
202 name = 'webhook'
193
203
194 def __init__(self, template_url, headers):
204 def __init__(self, template_url, headers):
195 self.template_url = template_url
205 self.template_url = template_url
196 self.headers = headers
206 self.headers = headers
197
207
198 def get_base_parsed_template(self, data):
208 def get_base_parsed_template(self, data):
199 """
209 """
200 initially parses the passed in template with some common variables
210 initially parses the passed in template with some common variables
201 available on ALL calls
211 available on ALL calls
202 """
212 """
203 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
213 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
204 common_vars = {
214 common_vars = {
205 'repo_name': data['repo']['repo_name'],
215 'repo_name': data['repo']['repo_name'],
206 'repo_type': data['repo']['repo_type'],
216 'repo_type': data['repo']['repo_type'],
207 'repo_id': data['repo']['repo_id'],
217 'repo_id': data['repo']['repo_id'],
208 'repo_url': data['repo']['url'],
218 'repo_url': data['repo']['url'],
209 'username': data['actor']['username'],
219 'username': data['actor']['username'],
210 'user_id': data['actor']['user_id'],
220 'user_id': data['actor']['user_id'],
211 'event_name': data['name']
221 'event_name': data['name']
212 }
222 }
213
223
214 extra_vars = {}
224 extra_vars = {}
215 for extra_key, extra_val in data['repo']['extra_fields'].items():
225 for extra_key, extra_val in data['repo']['extra_fields'].items():
216 extra_vars['extra__{}'.format(extra_key)] = extra_val
226 extra_vars['extra__{}'.format(extra_key)] = extra_val
217 common_vars.update(extra_vars)
227 common_vars.update(extra_vars)
218
228
219 template_url = self.template_url.replace('${extra:', '${extra__')
229 template_url = self.template_url.replace('${extra:', '${extra__')
220 return string.Template(template_url).safe_substitute(**common_vars)
230 for k, v in common_vars.items():
231 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
232 return template_url
221
233
222 def repo_push_event_handler(self, event, data):
234 def repo_push_event_handler(self, event, data):
223 url = self.get_base_parsed_template(data)
235 url = self.get_base_parsed_template(data)
224 url_calls = []
236 url_calls = []
225
237
226 branches_commits = self.aggregate_branch_data(
238 branches_commits = self.aggregate_branch_data(
227 data['push']['branches'], data['push']['commits'])
239 data['push']['branches'], data['push']['commits'])
228 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
240 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
229 # call it multiple times, for each branch if used in variables
241 # call it multiple times, for each branch if used in variables
230 for branch, commit_ids in branches_commits.items():
242 for branch, commit_ids in branches_commits.items():
231 branch_url = string.Template(url).safe_substitute(branch=branch)
243 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
232
244
233 if '${branch_head}' in branch_url:
245 if '${branch_head}' in branch_url:
234 # last commit in the aggregate is the head of the branch
246 # last commit in the aggregate is the head of the branch
235 branch_head = commit_ids['branch_head']
247 branch_head = commit_ids['branch_head']
236 branch_url = string.Template(branch_url).safe_substitute(
248 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
237 branch_head=branch_head)
238
249
239 # call further down for each commit if used
250 # call further down for each commit if used
240 if '${commit_id}' in branch_url:
251 if '${commit_id}' in branch_url:
241 for commit_data in commit_ids['commits']:
252 for commit_data in commit_ids['commits']:
242 commit_id = commit_data['raw_id']
253 commit_id = commit_data['raw_id']
243 commit_url = string.Template(branch_url).safe_substitute(
254 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
244 commit_id=commit_id)
245 # register per-commit call
255 # register per-commit call
246 log.debug(
256 log.debug(
247 'register %s call(%s) to url %s',
257 'register %s call(%s) to url %s',
248 self.name, event, commit_url)
258 self.name, event, commit_url)
249 url_calls.append(
259 url_calls.append(
250 (commit_url, self.headers, data))
260 (commit_url, self.headers, data))
251
261
252 else:
262 else:
253 # register per-branch call
263 # register per-branch call
254 log.debug(
264 log.debug('register %s call(%s) to url %s',
255 'register %s call(%s) to url %s',
256 self.name, event, branch_url)
265 self.name, event, branch_url)
257 url_calls.append(
266 url_calls.append((branch_url, self.headers, data))
258 (branch_url, self.headers, data))
259
267
260 else:
268 else:
261 log.debug(
269 log.debug('register %s call(%s) to url %s', self.name, event, url)
262 'register %s call(%s) to url %s', self.name, event, url)
263 url_calls.append((url, self.headers, data))
270 url_calls.append((url, self.headers, data))
264
271
265 return url_calls
272 return url_calls
266
273
267 def repo_create_event_handler(self, event, data):
274 def repo_create_event_handler(self, event, data):
268 url = self.get_base_parsed_template(data)
275 url = self.get_base_parsed_template(data)
269 log.debug(
276 log.debug('register %s call(%s) to url %s', self.name, event, url)
270 'register %s call(%s) to url %s', self.name, event, url)
271 return [(url, self.headers, data)]
277 return [(url, self.headers, data)]
272
278
273 def pull_request_event_handler(self, event, data):
279 def pull_request_event_handler(self, event, data):
274 url = self.get_base_parsed_template(data)
280 url = self.get_base_parsed_template(data)
275 log.debug(
281 log.debug('register %s call(%s) to url %s', self.name, event, url)
276 'register %s call(%s) to url %s', self.name, event, url)
282 pr_vars = [
277 url = string.Template(url).safe_substitute(
283 ('pull_request_id', data['pullrequest']['pull_request_id']),
278 pull_request_id=data['pullrequest']['pull_request_id'],
284 ('pull_request_title', data['pullrequest']['title']),
279 pull_request_title=data['pullrequest']['title'],
285 ('pull_request_url', data['pullrequest']['url']),
280 pull_request_url=data['pullrequest']['url'],
286 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
281 pull_request_shadow_url=data['pullrequest']['shadow_url'],
287 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
282 pull_request_commits_uid=data['pullrequest']['commits_uid'],
288 ]
283 )
289 for k, v in pr_vars:
290 url = UrlTmpl(url).safe_substitute(**{k: v})
291
284 return [(url, self.headers, data)]
292 return [(url, self.headers, data)]
285
293
286 def __call__(self, event, data):
294 def __call__(self, event, data):
287 from rhodecode import events
295 from rhodecode import events
288
296
289 if isinstance(event, events.RepoPushEvent):
297 if isinstance(event, events.RepoPushEvent):
290 return self.repo_push_event_handler(event, data)
298 return self.repo_push_event_handler(event, data)
291 elif isinstance(event, events.RepoCreateEvent):
299 elif isinstance(event, events.RepoCreateEvent):
292 return self.repo_create_event_handler(event, data)
300 return self.repo_create_event_handler(event, data)
293 elif isinstance(event, events.PullRequestEvent):
301 elif isinstance(event, events.PullRequestEvent):
294 return self.pull_request_event_handler(event, data)
302 return self.pull_request_event_handler(event, data)
295 else:
303 else:
296 raise ValueError(
304 raise ValueError(
297 'event type `%s` not in supported list: %s' % (
305 'event type `%s` not in supported list: %s' % (
298 event.__class__, events))
306 event.__class__, events))
299
307
300
308
301 def get_auth(settings):
309 def get_auth(settings):
302 from requests.auth import HTTPBasicAuth
310 from requests.auth import HTTPBasicAuth
303 username = settings.get('username')
311 username = settings.get('username')
304 password = settings.get('password')
312 password = settings.get('password')
305 if username and password:
313 if username and password:
306 return HTTPBasicAuth(username, password)
314 return HTTPBasicAuth(username, password)
307 return None
315 return None
308
316
309
317
310 def get_web_token(settings):
318 def get_web_token(settings):
311 return settings['secret_token']
319 return settings['secret_token']
312
320
313
321
314 def get_url_vars(url_vars):
322 def get_url_vars(url_vars):
315 return '\n'.join(
323 return '\n'.join(
316 '{} - {}'.format('${' + key + '}', explanation)
324 '{} - {}'.format('${' + key + '}', explanation)
317 for key, explanation in url_vars)
325 for key, explanation in url_vars)
318
326
319
327
320 def render_with_traceback(template, *args, **kwargs):
328 def render_with_traceback(template, *args, **kwargs):
321 try:
329 try:
322 return template.render(*args, **kwargs)
330 return template.render(*args, **kwargs)
323 except Exception:
331 except Exception:
324 log.error(exceptions.text_error_template().render())
332 log.error(exceptions.text_error_template().render())
325 raise
333 raise
326
334
327
335
328 STATUS_400 = (400, 401, 403)
336 STATUS_400 = (400, 401, 403)
329 STATUS_500 = (500, 502, 504)
337 STATUS_500 = (500, 502, 504)
330
338
331
339
332 def requests_retry_call(
340 def requests_retry_call(
333 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
341 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
334 session=None):
342 session=None):
335 """
343 """
336 session = requests_retry_session()
344 session = requests_retry_session()
337 response = session.get('http://example.com')
345 response = session.get('http://example.com')
338
346
339 :param retries:
347 :param retries:
340 :param backoff_factor:
348 :param backoff_factor:
341 :param status_forcelist:
349 :param status_forcelist:
342 :param session:
350 :param session:
343 """
351 """
344 session = session or requests.Session()
352 session = session or requests.Session()
345 retry = Retry(
353 retry = Retry(
346 total=retries,
354 total=retries,
347 read=retries,
355 read=retries,
348 connect=retries,
356 connect=retries,
349 backoff_factor=backoff_factor,
357 backoff_factor=backoff_factor,
350 status_forcelist=status_forcelist,
358 status_forcelist=status_forcelist,
351 )
359 )
352 adapter = HTTPAdapter(max_retries=retry)
360 adapter = HTTPAdapter(max_retries=retry)
353 session.mount('http://', adapter)
361 session.mount('http://', adapter)
354 session.mount('https://', adapter)
362 session.mount('https://', adapter)
355 return session
363 return session
@@ -1,265 +1,265 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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
22
23 import deform
23 import deform
24 import deform.widget
24 import deform.widget
25 import logging
25 import logging
26 import colander
26 import colander
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode import events
29 from rhodecode import events
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.integrations.types.base import (
31 from rhodecode.integrations.types.base import (
32 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
32 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
33 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
33 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
34 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
34 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.model.validation_schema import widgets
35 from rhodecode.model.validation_schema import widgets
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 # updating this required to update the `common_vars` passed in url calling func
40 # updating this required to update the `common_vars` passed in url calling func
41
41
42 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
42 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
43
43
44
44
45 class WebhookSettingsSchema(colander.Schema):
45 class WebhookSettingsSchema(colander.Schema):
46 url = colander.SchemaNode(
46 url = colander.SchemaNode(
47 colander.String(),
47 colander.String(),
48 title=_('Webhook URL'),
48 title=_('Webhook URL'),
49 description=
49 description=
50 _('URL to which Webhook should submit data. If used some of the '
50 _('URL to which Webhook should submit data. If used some of the '
51 'variables would trigger multiple calls, like ${branch} or '
51 'variables would trigger multiple calls, like ${branch} or '
52 '${commit_id}. Webhook will be called as many times as unique '
52 '${commit_id}. Webhook will be called as many times as unique '
53 'objects in data in such cases.'),
53 'objects in data in such cases.'),
54 missing=colander.required,
54 missing=colander.required,
55 required=True,
55 required=True,
56 validator=colander.url,
56 validator=colander.url,
57 widget=widgets.CodeMirrorWidget(
57 widget=widgets.CodeMirrorWidget(
58 help_block_collapsable_name='Show url variables',
58 help_block_collapsable_name='Show url variables',
59 help_block_collapsable=(
59 help_block_collapsable=(
60 'E.g http://my-serv/trigger_job/${{event_name}}'
60 'E.g http://my-serv/trigger_job/${{event_name}}'
61 '?PR_ID=${{pull_request_id}}'
61 '?PR_ID=${{pull_request_id}}'
62 '\nFull list of vars:\n{}'.format(URL_VARS)),
62 '\nFull list of vars:\n{}'.format(URL_VARS)),
63 codemirror_mode='text',
63 codemirror_mode='text',
64 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
64 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
65 )
65 )
66 secret_token = colander.SchemaNode(
66 secret_token = colander.SchemaNode(
67 colander.String(),
67 colander.String(),
68 title=_('Secret Token'),
68 title=_('Secret Token'),
69 description=_('Optional string used to validate received payloads. '
69 description=_('Optional string used to validate received payloads. '
70 'It will be sent together with event data in JSON'),
70 'It will be sent together with event data in JSON'),
71 default='',
71 default='',
72 missing='',
72 missing='',
73 widget=deform.widget.TextInputWidget(
73 widget=deform.widget.TextInputWidget(
74 placeholder='e.g. secret_token'
74 placeholder='e.g. secret_token'
75 ),
75 ),
76 )
76 )
77 username = colander.SchemaNode(
77 username = colander.SchemaNode(
78 colander.String(),
78 colander.String(),
79 title=_('Username'),
79 title=_('Username'),
80 description=_('Optional username to authenticate the call.'),
80 description=_('Optional username to authenticate the call.'),
81 default='',
81 default='',
82 missing='',
82 missing='',
83 widget=deform.widget.TextInputWidget(
83 widget=deform.widget.TextInputWidget(
84 placeholder='e.g. admin'
84 placeholder='e.g. admin'
85 ),
85 ),
86 )
86 )
87 password = colander.SchemaNode(
87 password = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 title=_('Password'),
89 title=_('Password'),
90 description=_('Optional password to authenticate the call.'),
90 description=_('Optional password to authenticate the call.'),
91 default='',
91 default='',
92 missing='',
92 missing='',
93 widget=deform.widget.PasswordWidget(
93 widget=deform.widget.PasswordWidget(
94 placeholder='e.g. secret.',
94 placeholder='e.g. secret.',
95 redisplay=True,
95 redisplay=True,
96 ),
96 ),
97 )
97 )
98 custom_header_key = colander.SchemaNode(
98 custom_header_key = colander.SchemaNode(
99 colander.String(),
99 colander.String(),
100 title=_('Custom Header Key'),
100 title=_('Custom Header Key'),
101 description=_('Custom Header name to be set when calling endpoint.'),
101 description=_('Custom Header name to be set when calling endpoint.'),
102 default='',
102 default='',
103 missing='',
103 missing='',
104 widget=deform.widget.TextInputWidget(
104 widget=deform.widget.TextInputWidget(
105 placeholder='e.g: Authorization'
105 placeholder='e.g: Authorization'
106 ),
106 ),
107 )
107 )
108 custom_header_val = colander.SchemaNode(
108 custom_header_val = colander.SchemaNode(
109 colander.String(),
109 colander.String(),
110 title=_('Custom Header Value'),
110 title=_('Custom Header Value'),
111 description=_('Custom Header value to be set when calling endpoint.'),
111 description=_('Custom Header value to be set when calling endpoint.'),
112 default='',
112 default='',
113 missing='',
113 missing='',
114 widget=deform.widget.TextInputWidget(
114 widget=deform.widget.TextInputWidget(
115 placeholder='e.g. Basic XxXxXx'
115 placeholder='e.g. Basic XxXxXx'
116 ),
116 ),
117 )
117 )
118 method_type = colander.SchemaNode(
118 method_type = colander.SchemaNode(
119 colander.String(),
119 colander.String(),
120 title=_('Call Method'),
120 title=_('Call Method'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
122 default='post',
122 default='post',
123 missing='',
123 missing='',
124 widget=deform.widget.RadioChoiceWidget(
124 widget=deform.widget.RadioChoiceWidget(
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
126 inline=True
126 inline=True
127 ),
127 ),
128 )
128 )
129
129
130
130
131 class WebhookIntegrationType(IntegrationTypeBase):
131 class WebhookIntegrationType(IntegrationTypeBase):
132 key = 'webhook'
132 key = 'webhook'
133 display_name = _('Webhook')
133 display_name = _('Webhook')
134 description = _('send JSON data to a url endpoint')
134 description = _('send JSON data to a url endpoint')
135
135
136 @classmethod
136 @classmethod
137 def icon(cls):
137 def icon(cls):
138 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>'''
138 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>'''
139
139
140 valid_events = [
140 valid_events = [
141 events.PullRequestCloseEvent,
141 events.PullRequestCloseEvent,
142 events.PullRequestMergeEvent,
142 events.PullRequestMergeEvent,
143 events.PullRequestUpdateEvent,
143 events.PullRequestUpdateEvent,
144 events.PullRequestCommentEvent,
144 events.PullRequestCommentEvent,
145 events.PullRequestReviewEvent,
145 events.PullRequestReviewEvent,
146 events.PullRequestCreateEvent,
146 events.PullRequestCreateEvent,
147 events.RepoPushEvent,
147 events.RepoPushEvent,
148 events.RepoCreateEvent,
148 events.RepoCreateEvent,
149 ]
149 ]
150
150
151 def settings_schema(self):
151 def settings_schema(self):
152 schema = WebhookSettingsSchema()
152 schema = WebhookSettingsSchema()
153 schema.add(colander.SchemaNode(
153 schema.add(colander.SchemaNode(
154 colander.Set(),
154 colander.Set(),
155 widget=deform.widget.CheckboxChoiceWidget(
155 widget=deform.widget.CheckboxChoiceWidget(
156 values=sorted(
156 values=sorted(
157 [(e.name, e.display_name) for e in self.valid_events]
157 [(e.name, e.display_name) for e in self.valid_events]
158 )
158 )
159 ),
159 ),
160 description="Events activated for this integration",
160 description="Events activated for this integration",
161 name='events'
161 name='events'
162 ))
162 ))
163 return schema
163 return schema
164
164
165 def send_event(self, event):
165 def send_event(self, event):
166 log.debug(
166 log.debug(
167 'handling event %s with Webhook integration %s', event.name, self)
167 'handling event %s with Webhook integration %s', event.name, self)
168
168
169 if event.__class__ not in self.valid_events:
169 if event.__class__ not in self.valid_events:
170 log.debug('event not valid: %r', event)
170 log.debug('event not valid: %r', event)
171 return
171 return
172
172
173 allowed_events = self.settings['events']
173 allowed_events = self.settings['events']
174 if event.name not in allowed_events:
174 if event.name not in allowed_events:
175 log.debug('event ignored: %r event %s not in allowed events %s',
175 log.debug('event ignored: %r event %s not in allowed events %s',
176 event, event.name, allowed_events)
176 event, event.name, allowed_events)
177 return
177 return
178
178
179 data = event.as_dict()
179 data = event.as_dict()
180 template_url = self.settings['url']
180 template_url = self.settings['url']
181
181
182 headers = {}
182 headers = {}
183 head_key = self.settings.get('custom_header_key')
183 head_key = self.settings.get('custom_header_key')
184 head_val = self.settings.get('custom_header_val')
184 head_val = self.settings.get('custom_header_val')
185 if head_key and head_val:
185 if head_key and head_val:
186 headers = {head_key: head_val}
186 headers = {head_key: head_val}
187
187
188 handler = WebhookDataHandler(template_url, headers)
188 handler = WebhookDataHandler(template_url, headers)
189
189
190 url_calls = handler(event, data)
190 url_calls = handler(event, data)
191 log.debug('webhook: calling following urls: %s', [x[0] for x in url_calls])
191 log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
192
192
193 run_task(post_to_webhook, url_calls, self.settings)
193 run_task(post_to_webhook, url_calls, self.settings)
194
194
195
195
196 @async_task(ignore_result=True, base=RequestContextTask)
196 @async_task(ignore_result=True, base=RequestContextTask)
197 def post_to_webhook(url_calls, settings):
197 def post_to_webhook(url_calls, settings):
198 """
198 """
199 Example data::
199 Example data::
200
200
201 {'actor': {'user_id': 2, 'username': u'admin'},
201 {'actor': {'user_id': 2, 'username': u'admin'},
202 'actor_ip': u'192.168.157.1',
202 'actor_ip': u'192.168.157.1',
203 'name': 'repo-push',
203 'name': 'repo-push',
204 'push': {'branches': [{'name': u'default',
204 'push': {'branches': [{'name': u'default',
205 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
205 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
206 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
206 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
207 'branch': u'default',
207 'branch': u'default',
208 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
208 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
209 'issues': [],
209 'issues': [],
210 'mentions': [],
210 'mentions': [],
211 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
211 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
213 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
213 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
214 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
214 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
215 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
215 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
216 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
216 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
217 'refs': {'bookmarks': [],
217 'refs': {'bookmarks': [],
218 'branches': [u'default'],
218 'branches': [u'default'],
219 'tags': [u'tip']},
219 'tags': [u'tip']},
220 'reviewers': [],
220 'reviewers': [],
221 'revision': 9L,
221 'revision': 9L,
222 'short_id': 'a815cc738b96',
222 'short_id': 'a815cc738b96',
223 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
223 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
224 'issues': {}},
224 'issues': {}},
225 'repo': {'extra_fields': '',
225 'repo': {'extra_fields': '',
226 'permalink_url': u'http://rc.local:8080/_7',
226 'permalink_url': u'http://rc.local:8080/_7',
227 'repo_id': 7,
227 'repo_id': 7,
228 'repo_name': u'hg-repo',
228 'repo_name': u'hg-repo',
229 'repo_type': u'hg',
229 'repo_type': u'hg',
230 'url': u'http://rc.local:8080/hg-repo'},
230 'url': u'http://rc.local:8080/hg-repo'},
231 'server_url': u'http://rc.local:8080',
231 'server_url': u'http://rc.local:8080',
232 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
232 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
233 }
233 }
234 """
234 """
235
235
236 call_headers = {
236 call_headers = {
237 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
237 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
238 } # updated below with custom ones, allows override
238 } # updated below with custom ones, allows override
239
239
240 auth = get_auth(settings)
240 auth = get_auth(settings)
241 token = get_web_token(settings)
241 token = get_web_token(settings)
242
242
243 for url, headers, data in url_calls:
243 for url, headers, data in url_calls:
244 req_session = requests_retry_call()
244 req_session = requests_retry_call()
245
245
246 method = settings.get('method_type') or 'post'
246 method = settings.get('method_type') or 'post'
247 call_method = getattr(req_session, method)
247 call_method = getattr(req_session, method)
248
248
249 headers = headers or {}
249 headers = headers or {}
250 call_headers.update(headers)
250 call_headers.update(headers)
251
251
252 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
252 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
253 if settings.get('log_data'):
253 if settings.get('log_data'):
254 log.debug('calling webhook with data: %s', data)
254 log.debug('calling webhook with data: %s', data)
255 resp = call_method(url, json={
255 resp = call_method(url, json={
256 'token': token,
256 'token': token,
257 'event': data
257 'event': data
258 }, headers=call_headers, auth=auth, timeout=60)
258 }, headers=call_headers, auth=auth, timeout=60)
259 log.debug('Got Webhook response: %s', resp)
259 log.debug('Got Webhook response: %s', resp)
260
260
261 try:
261 try:
262 resp.raise_for_status() # raise exception on a failed request
262 resp.raise_for_status() # raise exception on a failed request
263 except Exception:
263 except Exception:
264 log.error(resp.text)
264 log.error(resp.text)
265 raise
265 raise
@@ -1,132 +1,135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 WebhookDataHandler
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 'name': 'event',
32 'repo': {
32 'repo': {
33 'repo_name': 'foo',
33 'repo_name': 'foo',
34 'repo_type': 'hg',
34 'repo_type': 'hg',
35 'repo_id': '12',
35 'repo_id': '12',
36 'url': 'http://repo.url/foo',
36 'url': 'http://repo.url/foo',
37 'extra_fields': {},
37 'extra_fields': {},
38 },
38 },
39 'actor': {
39 'actor': {
40 'username': 'actor_name',
40 'username': 'actor_name',
41 'user_id': 1
41 'user_id': 1
42 }
42 }
43 }
43 }
44
44
45
45
46 def test_webhook_parse_url_invalid_event():
46 def test_webhook_parse_url_invalid_event():
47 template_url = 'http://server.com/${repo_name}/build'
47 template_url = 'http://server.com/${repo_name}/build'
48 handler = WebhookDataHandler(
48 handler = WebhookDataHandler(
49 template_url, {'exmaple-header': 'header-values'})
49 template_url, {'exmaple-header': 'header-values'})
50 event = events.RepoDeleteEvent('')
50 event = events.RepoDeleteEvent('')
51 with pytest.raises(ValueError) as err:
51 with pytest.raises(ValueError) as err:
52 handler(event, {})
52 handler(event, {})
53
53
54 err = str(err.value)
54 err = str(err.value)
55 assert err.startswith(
55 assert err.startswith(
56 'event type `%s` not in supported list' % event.__class__)
56 'event type `%s` not in supported list' % event.__class__)
57
57
58
58
59 @pytest.mark.parametrize('template,expected_urls', [
59 @pytest.mark.parametrize('template,expected_urls', [
60 ('http://server.com/${repo_name}/build',
60 ('http://server.com/${repo_name}/build',
61 ['http://server.com/foo/build']),
61 ['http://server.com/foo/build']),
62 ('http://server.com/${repo_name}/${repo_type}',
62 ('http://server.com/${repo_name}/${repo_type}',
63 ['http://server.com/foo/hg']),
63 ['http://server.com/foo/hg']),
64 ('http://${server}.com/${repo_name}/${repo_id}',
64 ('http://${server}.com/${repo_name}/${repo_id}',
65 ['http://${server}.com/foo/12']),
65 ['http://${server}.com/foo/12']),
66 ('http://server.com/${branch}/build',
66 ('http://server.com/${branch}/build',
67 ['http://server.com/${branch}/build']),
67 ['http://server.com/${branch}/build']),
68 ])
68 ])
69 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):
70 headers = {'exmaple-header': 'header-values'}
70 headers = {'exmaple-header': 'header-values'}
71 handler = WebhookDataHandler(template, headers)
71 handler = WebhookDataHandler(template, headers)
72 urls = handler(events.RepoCreateEvent(''), base_data)
72 urls = handler(events.RepoCreateEvent(''), base_data)
73 assert urls == [
73 assert urls == [
74 (url, headers, base_data) for url in expected_urls]
74 (url, headers, base_data) for url in expected_urls]
75
75
76
76
77 @pytest.mark.parametrize('template,expected_urls', [
77 @pytest.mark.parametrize('template,expected_urls', [
78 ('http://server.com/${repo_name}/${pull_request_id}',
78 ('http://server.com/${repo_name}/${pull_request_id}',
79 ['http://server.com/foo/999']),
79 ['http://server.com/foo/999']),
80 ('http://server.com/${repo_name}/${pull_request_url}',
80 ('http://server.com/${repo_name}/${pull_request_url}',
81 ['http://server.com/foo/http://pr-url.com']),
81 ['http://server.com/foo/http%3A//pr-url.com']),
82 ('http://server.com/${repo_name}/${pull_request_url}/?TITLE=${pull_request_title}',
83 ['http://server.com/foo/http%3A//pr-url.com/?TITLE=example-pr-title%20Ticket%20%23123']),
84 ('http://server.com/${repo_name}/?SHADOW_URL=${pull_request_shadow_url}',
85 ['http://server.com/foo/?SHADOW_URL=http%3A//pr-url.com/repository']),
82 ])
86 ])
83 def test_webook_parse_url_for_pull_request_event(
87 def test_webook_parse_url_for_pull_request_event(base_data, template, expected_urls):
84 base_data, template, expected_urls):
85
88
86 base_data['pullrequest'] = {
89 base_data['pullrequest'] = {
87 'pull_request_id': 999,
90 'pull_request_id': 999,
88 'url': 'http://pr-url.com',
91 'url': 'http://pr-url.com',
89 'title': 'example-pr-title',
92 'title': 'example-pr-title Ticket #123',
90 'commits_uid': 'abcdefg1234',
93 'commits_uid': 'abcdefg1234',
91 'shadow_url': 'http://pr-url.com/repository'
94 'shadow_url': 'http://pr-url.com/repository'
92 }
95 }
93 headers = {'exmaple-header': 'header-values'}
96 headers = {'exmaple-header': 'header-values'}
94 handler = WebhookDataHandler(template, headers)
97 handler = WebhookDataHandler(template, headers)
95 urls = handler(events.PullRequestCreateEvent(
98 urls = handler(events.PullRequestCreateEvent(
96 AttributeDict({'target_repo': 'foo'})), base_data)
99 AttributeDict({'target_repo': 'foo'})), base_data)
97 assert urls == [
100 assert urls == [
98 (url, headers, base_data) for url in expected_urls]
101 (url, headers, base_data) for url in expected_urls]
99
102
100
103
101 @pytest.mark.parametrize('template,expected_urls', [
104 @pytest.mark.parametrize('template,expected_urls', [
102 ('http://server.com/${branch}/build',
105 ('http://server.com/${branch}/build',
103 ['http://server.com/stable/build',
106 ['http://server.com/stable/build',
104 'http://server.com/dev/build']),
107 'http://server.com/dev/build']),
105 ('http://server.com/${branch}/${commit_id}',
108 ('http://server.com/${branch}/${commit_id}',
106 ['http://server.com/stable/stable-xxx',
109 ['http://server.com/stable/stable-xxx',
107 'http://server.com/stable/stable-yyy',
110 'http://server.com/stable/stable-yyy',
108 'http://server.com/dev/dev-xxx',
111 'http://server.com/dev/dev-xxx',
109 'http://server.com/dev/dev-yyy']),
112 'http://server.com/dev/dev-yyy']),
110 ('http://server.com/${branch_head}',
113 ('http://server.com/${branch_head}',
111 ['http://server.com/stable-yyy',
114 ['http://server.com/stable-yyy',
112 'http://server.com/dev-yyy']),
115 'http://server.com/dev-yyy']),
113 ('http://server.com/${commit_id}',
116 ('http://server.com/${commit_id}',
114 ['http://server.com/stable-xxx',
117 ['http://server.com/stable-xxx',
115 'http://server.com/stable-yyy',
118 'http://server.com/stable-yyy',
116 'http://server.com/dev-xxx',
119 'http://server.com/dev-xxx',
117 'http://server.com/dev-yyy']),
120 'http://server.com/dev-yyy']),
118 ])
121 ])
119 def test_webook_parse_url_for_push_event(
122 def test_webook_parse_url_for_push_event(
120 baseapp, repo_push_event, base_data, template, expected_urls):
123 baseapp, repo_push_event, base_data, template, expected_urls):
121 base_data['push'] = {
124 base_data['push'] = {
122 'branches': [{'name': 'stable'}, {'name': 'dev'}],
125 'branches': [{'name': 'stable'}, {'name': 'dev'}],
123 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
126 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
124 {'branch': 'stable', 'raw_id': 'stable-yyy'},
127 {'branch': 'stable', 'raw_id': 'stable-yyy'},
125 {'branch': 'dev', 'raw_id': 'dev-xxx'},
128 {'branch': 'dev', 'raw_id': 'dev-xxx'},
126 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
129 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
127 }
130 }
128 headers = {'exmaple-header': 'header-values'}
131 headers = {'exmaple-header': 'header-values'}
129 handler = WebhookDataHandler(template, headers)
132 handler = WebhookDataHandler(template, headers)
130 urls = handler(repo_push_event, base_data)
133 urls = handler(repo_push_event, base_data)
131 assert urls == [
134 assert urls == [
132 (url, headers, base_data) for url in expected_urls]
135 (url, headers, base_data) for url in expected_urls]
General Comments 0
You need to be logged in to leave comments. Login now