##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 import string
23 import collections
24 import logging
22 25 from rhodecode.translation import _
23 26
27 log = logging.getLogger(__name__)
28
24 29
25 30 class IntegrationTypeBase(object):
26 31 """ Base class for IntegrationType plugins """
27 32 is_dummy = False
28 33 description = ''
29 34
30 35 @classmethod
31 36 def icon(cls):
32 37 return '''
33 38 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
34 39 <svg
35 40 xmlns:dc="http://purl.org/dc/elements/1.1/"
36 41 xmlns:cc="http://creativecommons.org/ns#"
37 42 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
38 43 xmlns:svg="http://www.w3.org/2000/svg"
39 44 xmlns="http://www.w3.org/2000/svg"
40 45 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
41 46 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
42 47 viewBox="0 -256 1792 1792"
43 48 id="svg3025"
44 49 version="1.1"
45 50 inkscape:version="0.48.3.1 r9886"
46 51 width="100%"
47 52 height="100%"
48 53 sodipodi:docname="cog_font_awesome.svg">
49 54 <metadata
50 55 id="metadata3035">
51 56 <rdf:RDF>
52 57 <cc:Work
53 58 rdf:about="">
54 59 <dc:format>image/svg+xml</dc:format>
55 60 <dc:type
56 61 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
57 62 </cc:Work>
58 63 </rdf:RDF>
59 64 </metadata>
60 65 <defs
61 66 id="defs3033" />
62 67 <sodipodi:namedview
63 68 pagecolor="#ffffff"
64 69 bordercolor="#666666"
65 70 borderopacity="1"
66 71 objecttolerance="10"
67 72 gridtolerance="10"
68 73 guidetolerance="10"
69 74 inkscape:pageopacity="0"
70 75 inkscape:pageshadow="2"
71 76 inkscape:window-width="640"
72 77 inkscape:window-height="480"
73 78 id="namedview3031"
74 79 showgrid="false"
75 80 inkscape:zoom="0.13169643"
76 81 inkscape:cx="896"
77 82 inkscape:cy="896"
78 83 inkscape:window-x="0"
79 84 inkscape:window-y="25"
80 85 inkscape:window-maximized="0"
81 86 inkscape:current-layer="svg3025" />
82 87 <g
83 88 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
84 89 id="g3027">
85 90 <path
86 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 92 id="path3029"
88 93 inkscape:connector-curvature="0"
89 94 style="fill:currentColor" />
90 95 </g>
91 96 </svg>
92 97 '''
93 98
94 99 def __init__(self, settings):
95 100 """
96 101 :param settings: dict of settings to be used for the integration
97 102 """
98 103 self.settings = settings
99 104
100 105 def settings_schema(self):
101 106 """
102 107 A colander schema of settings for the integration type
103 108 """
104 109 return colander.Schema()
105 110
106 111
107 112 class EEIntegration(IntegrationTypeBase):
108 113 description = 'Integration available in RhodeCode EE edition.'
109 114 is_dummy = True
110 115
111 116 def __init__(self, name, key, settings=None):
112 117 self.display_name = name
113 118 self.key = key
114 119 super(EEIntegration, self).__init__(settings)
115 120
116 121
117 122 # Helpers #
118 123 WEBHOOK_URL_VARS = [
119 124 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
120 125 ('repo_name', 'Full name of the repository'),
121 126 ('repo_type', 'VCS type of repository'),
122 127 ('repo_id', 'Unique id of repository'),
123 128 ('repo_url', 'Repository url'),
124 129 # extra repo fields
125 130 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
126 131
127 132 # special attrs below that we handle, using multi-call
128 133 ('branch', 'Name of each brach submitted, if any.'),
129 134 ('commit_id', 'Id of each commit submitted, if any.'),
130 135
131 136 # pr events vars
132 137 ('pull_request_id', 'Unique ID of the pull request.'),
133 138 ('pull_request_url', 'Pull request url.'),
134 139 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
135 140
136 141 # user who triggers the call
137 142 ('username', 'User who triggered the call.'),
138 143 ('user_id', 'User id who triggered the call.'),
139 144 ]
140 145
141 146 # common vars for url template used for CI plugins. Shared with webhook
142 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 266 def get_auth(settings):
146 267 from requests.auth import HTTPBasicAuth
147 268 username = settings.get('username')
148 269 password = settings.get('password')
149 270 if username and password:
150 271 return HTTPBasicAuth(username, password)
151 272 return None
152 273
153 274
275 def get_web_token(settings):
276 return settings['secret_token']
277
278
154 279 def get_url_vars(url_vars):
155 280 return '\n'.join(
156 281 '{} - {}'.format('${' + key + '}', explanation)
157 282 for key, explanation in url_vars)
@@ -1,381 +1,274 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from __future__ import unicode_literals
22 import string
23 import collections
24 22
25 23 import deform
26 24 import deform.widget
27 25 import logging
28 26 import requests
29 27 import requests.adapters
30 28 import colander
31 29 from requests.packages.urllib3.util.retry import Retry
32 30
33 31 import rhodecode
34 32 from rhodecode import events
35 33 from rhodecode.translation import _
36 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 37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
39 38 from rhodecode.model.validation_schema import widgets
40 39
41 40 log = logging.getLogger(__name__)
42 41
43 42
44 43 # updating this required to update the `common_vars` passed in url calling func
45 44
46 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 48 class WebhookSettingsSchema(colander.Schema):
157 49 url = colander.SchemaNode(
158 50 colander.String(),
159 51 title=_('Webhook URL'),
160 52 description=
161 53 _('URL to which Webhook should submit data. If used some of the '
162 54 'variables would trigger multiple calls, like ${branch} or '
163 55 '${commit_id}. Webhook will be called as many times as unique '
164 56 'objects in data in such cases.'),
165 57 missing=colander.required,
166 58 required=True,
167 59 validator=colander.url,
168 60 widget=widgets.CodeMirrorWidget(
169 61 help_block_collapsable_name='Show url variables',
170 62 help_block_collapsable=(
171 63 'E.g http://my-serv/trigger_job/${{event_name}}'
172 64 '?PR_ID=${{pull_request_id}}'
173 65 '\nFull list of vars:\n{}'.format(URL_VARS)),
174 66 codemirror_mode='text',
175 67 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
176 68 )
177 69 secret_token = colander.SchemaNode(
178 70 colander.String(),
179 71 title=_('Secret Token'),
180 72 description=_('Optional string used to validate received payloads. '
181 73 'It will be sent together with event data in JSON'),
182 74 default='',
183 75 missing='',
184 76 widget=deform.widget.TextInputWidget(
185 77 placeholder='e.g. secret_token'
186 78 ),
187 79 )
188 80 username = colander.SchemaNode(
189 81 colander.String(),
190 82 title=_('Username'),
191 83 description=_('Optional username to authenticate the call.'),
192 84 default='',
193 85 missing='',
194 86 widget=deform.widget.TextInputWidget(
195 87 placeholder='e.g. admin'
196 88 ),
197 89 )
198 90 password = colander.SchemaNode(
199 91 colander.String(),
200 92 title=_('Password'),
201 93 description=_('Optional password to authenticate the call.'),
202 94 default='',
203 95 missing='',
204 96 widget=deform.widget.PasswordWidget(
205 97 placeholder='e.g. secret.',
206 98 redisplay=True,
207 99 ),
208 100 )
209 101 custom_header_key = colander.SchemaNode(
210 102 colander.String(),
211 103 title=_('Custom Header Key'),
212 104 description=_('Custom Header name to be set when calling endpoint.'),
213 105 default='',
214 106 missing='',
215 107 widget=deform.widget.TextInputWidget(
216 108 placeholder='e.g: Authorization'
217 109 ),
218 110 )
219 111 custom_header_val = colander.SchemaNode(
220 112 colander.String(),
221 113 title=_('Custom Header Value'),
222 114 description=_('Custom Header value to be set when calling endpoint.'),
223 115 default='',
224 116 missing='',
225 117 widget=deform.widget.TextInputWidget(
226 118 placeholder='e.g. Basic XxXxXx'
227 119 ),
228 120 )
229 121 method_type = colander.SchemaNode(
230 122 colander.String(),
231 123 title=_('Call Method'),
232 124 description=_('Select if the Webhook call should be made '
233 125 'with POST or GET.'),
234 126 default='post',
235 127 missing='',
236 128 widget=deform.widget.RadioChoiceWidget(
237 129 values=[('get', 'GET'), ('post', 'POST')],
238 130 inline=True
239 131 ),
240 132 )
241 133
242 134
243 135 class WebhookIntegrationType(IntegrationTypeBase):
244 136 key = 'webhook'
245 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 140 @classmethod
249 141 def icon(cls):
250 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 144 valid_events = [
253 145 events.PullRequestCloseEvent,
254 146 events.PullRequestMergeEvent,
255 147 events.PullRequestUpdateEvent,
256 148 events.PullRequestCommentEvent,
257 149 events.PullRequestReviewEvent,
258 150 events.PullRequestCreateEvent,
259 151 events.RepoPushEvent,
260 152 events.RepoCreateEvent,
261 153 ]
262 154
263 155 def settings_schema(self):
264 156 schema = WebhookSettingsSchema()
265 157 schema.add(colander.SchemaNode(
266 158 colander.Set(),
267 159 widget=deform.widget.CheckboxChoiceWidget(
268 160 values=sorted(
269 161 [(e.name, e.display_name) for e in self.valid_events]
270 162 )
271 163 ),
272 164 description="Events activated for this integration",
273 165 name='events'
274 166 ))
275 167 return schema
276 168
277 169 def send_event(self, event):
278 log.debug('handling event %s with Webhook integration %s',
279 event.name, self)
170 log.debug(
171 'handling event %s with Webhook integration %s', event.name, self)
280 172
281 173 if event.__class__ not in self.valid_events:
282 174 log.debug('event not valid: %r' % event)
283 175 return
284 176
285 177 if event.name not in self.settings['events']:
286 178 log.debug('event ignored: %r' % event)
287 179 return
288 180
289 181 data = event.as_dict()
290 182 template_url = self.settings['url']
291 183
292 184 headers = {}
293 185 head_key = self.settings.get('custom_header_key')
294 186 head_val = self.settings.get('custom_header_val')
295 187 if head_key and head_val:
296 188 headers = {head_key: head_val}
297 189
298 handler = WebhookHandler(
299 template_url, self.settings['secret_token'], headers)
190 handler = WebhookDataHandler(template_url, headers)
300 191
301 192 url_calls = handler(event, data)
302 193 log.debug('webhook: calling following urls: %s',
303 194 [x[0] for x in url_calls])
304 195
305 196 run_task(post_to_webhook, url_calls, self.settings)
306 197
307 198
308 199 @async_task(ignore_result=True, base=RequestContextTask)
309 200 def post_to_webhook(url_calls, settings):
310 201 """
311 202 Example data::
312 203
313 204 {'actor': {'user_id': 2, 'username': u'admin'},
314 205 'actor_ip': u'192.168.157.1',
315 206 'name': 'repo-push',
316 207 'push': {'branches': [{'name': u'default',
317 208 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
318 209 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
319 210 'branch': u'default',
320 211 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
321 212 'issues': [],
322 213 'mentions': [],
323 214 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
324 215 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
325 216 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
326 217 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
327 218 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
328 219 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
329 220 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
330 221 'reviewers': [],
331 222 'revision': 9L,
332 223 'short_id': 'a815cc738b96',
333 224 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
334 225 'issues': {}},
335 226 'repo': {'extra_fields': '',
336 227 'permalink_url': u'http://rc.local:8080/_7',
337 228 'repo_id': 7,
338 229 'repo_name': u'hg-repo',
339 230 'repo_type': u'hg',
340 231 'url': u'http://rc.local:8080/hg-repo'},
341 232 'server_url': u'http://rc.local:8080',
342 233 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
343 234
344 235 """
345 236 max_retries = 3
346 237 retries = Retry(
347 238 total=max_retries,
348 239 backoff_factor=0.15,
349 240 status_forcelist=[500, 502, 503, 504])
350 241 call_headers = {
351 242 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
352 243 rhodecode.__version__)
353 244 } # updated below with custom ones, allows override
354 245
355 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 250 req_session = requests.Session()
358 251 req_session.mount( # retry max N times
359 252 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
360 253
361 254 method = settings.get('method_type') or 'post'
362 255 call_method = getattr(req_session, method)
363 256
364 257 headers = headers or {}
365 258 call_headers.update(headers)
366 259
367 260 log.debug('calling Webhook with method: %s, and auth:%s',
368 261 call_method, auth)
369 262 if settings.get('log_data'):
370 263 log.debug('calling webhook with data: %s', data)
371 264 resp = call_method(url, json={
372 265 'token': token,
373 266 'event': data
374 267 }, headers=call_headers, auth=auth)
375 268 log.debug('Got Webhook response: %s', resp)
376 269
377 270 try:
378 271 resp.raise_for_status() # raise exception on a failed request
379 272 except Exception:
380 273 log.error(resp.text)
381 274 raise
@@ -1,111 +1,122 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode import events
24 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 28 @pytest.fixture
29 29 def base_data():
30 30 return {
31 'name': 'event',
31 32 'repo': {
32 33 'repo_name': 'foo',
33 34 'repo_type': 'hg',
34 35 'repo_id': '12',
35 36 'url': 'http://repo.url/foo',
36 37 'extra_fields': {},
37 38 },
38 39 'actor': {
39 40 'username': 'actor_name',
40 41 'user_id': 1
41 42 }
42 43 }
43 44
44 45
45 46 def test_webhook_parse_url_invalid_event():
46 47 template_url = 'http://server.com/${repo_name}/build'
47 handler = WebhookHandler(
48 template_url, 'secret_token', {'exmaple-header':'header-values'})
48 handler = WebhookDataHandler(
49 template_url, {'exmaple-header': 'header-values'})
50 event = events.RepoDeleteEvent('')
49 51 with pytest.raises(ValueError) as err:
50 handler(events.RepoDeleteEvent(''), {})
51 assert str(err.value).startswith('event type not supported')
52 handler(event, {})
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 59 @pytest.mark.parametrize('template,expected_urls', [
55 ('http://server.com/${repo_name}/build', ['http://server.com/foo/build']),
56 ('http://server.com/${repo_name}/${repo_type}', ['http://server.com/foo/hg']),
57 ('http://${server}.com/${repo_name}/${repo_id}', ['http://${server}.com/foo/12']),
58 ('http://server.com/${branch}/build', ['http://server.com/${branch}/build']),
60 ('http://server.com/${repo_name}/build',
61 ['http://server.com/foo/build']),
62 ('http://server.com/${repo_name}/${repo_type}',
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 69 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
61 70 headers = {'exmaple-header': 'header-values'}
62 handler = WebhookHandler(
63 template, 'secret_token', headers)
71 handler = WebhookDataHandler(template, headers)
64 72 urls = handler(events.RepoCreateEvent(''), base_data)
65 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 77 @pytest.mark.parametrize('template,expected_urls', [
70 ('http://server.com/${repo_name}/${pull_request_id}', ['http://server.com/foo/999']),
71 ('http://server.com/${repo_name}/${pull_request_url}', ['http://server.com/foo/http://pr-url.com']),
78 ('http://server.com/${repo_name}/${pull_request_id}',
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 83 def test_webook_parse_url_for_pull_request_event(
74 84 base_data, template, expected_urls):
75 85
76 86 base_data['pullrequest'] = {
77 87 'pull_request_id': 999,
78 88 'url': 'http://pr-url.com',
89 'shadow_url': 'http://pr-url.com/repository'
79 90 }
80 91 headers = {'exmaple-header': 'header-values'}
81 handler = WebhookHandler(
82 template, 'secret_token', headers)
92 handler = WebhookDataHandler(template, headers)
83 93 urls = handler(events.PullRequestCreateEvent(
84 94 AttributeDict({'target_repo': 'foo'})), base_data)
85 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 99 @pytest.mark.parametrize('template,expected_urls', [
90 ('http://server.com/${branch}/build', ['http://server.com/stable/build',
91 'http://server.com/dev/build']),
92 ('http://server.com/${branch}/${commit_id}', ['http://server.com/stable/stable-xxx',
93 'http://server.com/stable/stable-yyy',
94 'http://server.com/dev/dev-xxx',
95 'http://server.com/dev/dev-yyy']),
100 ('http://server.com/${branch}/build',
101 ['http://server.com/stable/build',
102 'http://server.com/dev/build']),
103 ('http://server.com/${branch}/${commit_id}',
104 ['http://server.com/stable/stable-xxx',
105 'http://server.com/stable/stable-yyy',
106 'http://server.com/dev/dev-xxx',
107 'http://server.com/dev/dev-yyy']),
96 108 ])
97 109 def test_webook_parse_url_for_push_event(
98 110 baseapp, repo_push_event, base_data, template, expected_urls):
99 111 base_data['push'] = {
100 112 'branches': [{'name': 'stable'}, {'name': 'dev'}],
101 113 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
102 114 {'branch': 'stable', 'raw_id': 'stable-yyy'},
103 115 {'branch': 'dev', 'raw_id': 'dev-xxx'},
104 116 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
105 117 }
106 118 headers = {'exmaple-header': 'header-values'}
107 handler = WebhookHandler(
108 template, 'secret_token', headers)
119 handler = WebhookDataHandler(template, headers)
109 120 urls = handler(repo_push_event, base_data)
110 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