##// END OF EJS Templates
webhook: fixed extra variable replacement.
marcink -
r2545:51dda326 stable
parent child Browse files
Show More
@@ -1,394 +1,395 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import string
22 import string
23 from collections import OrderedDict
23 from collections import OrderedDict
24
24
25 import deform
25 import deform
26 import deform.widget
26 import deform.widget
27 import logging
27 import logging
28 import requests
28 import requests
29 import requests.adapters
29 import requests.adapters
30 import colander
30 import colander
31 from requests.packages.urllib3.util.retry import Retry
31 from requests.packages.urllib3.util.retry import Retry
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode import events
34 from rhodecode import events
35 from rhodecode.translation import _
35 from rhodecode.translation import _
36 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
37 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 # updating this required to update the `common_vars` passed in url calling func
42 # updating this required to update the `common_vars` passed in url calling func
43 WEBHOOK_URL_VARS = [
43 WEBHOOK_URL_VARS = [
44 'repo_name',
44 'repo_name',
45 'repo_type',
45 'repo_type',
46 'repo_id',
46 'repo_id',
47 'repo_url',
47 'repo_url',
48 # extra repo fields
48 # extra repo fields
49 'extra:<extra_key_name>',
49 'extra:<extra_key_name>',
50
50
51 # special attrs below that we handle, using multi-call
51 # special attrs below that we handle, using multi-call
52 'branch',
52 'branch',
53 'commit_id',
53 'commit_id',
54
54
55 # pr events vars
55 # pr events vars
56 'pull_request_id',
56 'pull_request_id',
57 'pull_request_url',
57 'pull_request_url',
58
58
59 # user who triggers the call
59 # user who triggers the call
60 'username',
60 'username',
61 'user_id',
61 'user_id',
62
62
63 ]
63 ]
64 URL_VARS = ', '.join('${' + x + '}' for x in WEBHOOK_URL_VARS)
64 URL_VARS = ', '.join('${' + x + '}' for x in WEBHOOK_URL_VARS)
65
65
66
66
67 def get_auth(settings):
67 def get_auth(settings):
68 from requests.auth import HTTPBasicAuth
68 from requests.auth import HTTPBasicAuth
69 username = settings.get('username')
69 username = settings.get('username')
70 password = settings.get('password')
70 password = settings.get('password')
71 if username and password:
71 if username and password:
72 return HTTPBasicAuth(username, password)
72 return HTTPBasicAuth(username, password)
73 return None
73 return None
74
74
75
75
76 class WebhookHandler(object):
76 class WebhookHandler(object):
77 def __init__(self, template_url, secret_token, headers):
77 def __init__(self, template_url, secret_token, headers):
78 self.template_url = template_url
78 self.template_url = template_url
79 self.secret_token = secret_token
79 self.secret_token = secret_token
80 self.headers = headers
80 self.headers = headers
81
81
82 def get_base_parsed_template(self, data):
82 def get_base_parsed_template(self, data):
83 """
83 """
84 initially parses the passed in template with some common variables
84 initially parses the passed in template with some common variables
85 available on ALL calls
85 available on ALL calls
86 """
86 """
87 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
87 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
88 common_vars = {
88 common_vars = {
89 'repo_name': data['repo']['repo_name'],
89 'repo_name': data['repo']['repo_name'],
90 'repo_type': data['repo']['repo_type'],
90 'repo_type': data['repo']['repo_type'],
91 'repo_id': data['repo']['repo_id'],
91 'repo_id': data['repo']['repo_id'],
92 'repo_url': data['repo']['url'],
92 'repo_url': data['repo']['url'],
93 'username': data['actor']['username'],
93 'username': data['actor']['username'],
94 'user_id': data['actor']['user_id']
94 'user_id': data['actor']['user_id']
95 }
95 }
96
96 extra_vars = {}
97 extra_vars = {}
97 for extra_key, extra_val in data['repo']['extra_fields'].items():
98 for extra_key, extra_val in data['repo']['extra_fields'].items():
98 extra_vars['extra:{}'.format(extra_key)] = extra_val
99 extra_vars['extra__{}'.format(extra_key)] = extra_val
99 common_vars.update(extra_vars)
100 common_vars.update(extra_vars)
100
101
101 return string.Template(
102 template_url = self.template_url.replace('${extra:', '${extra__')
102 self.template_url).safe_substitute(**common_vars)
103 return string.Template(template_url).safe_substitute(**common_vars)
103
104
104 def repo_push_event_handler(self, event, data):
105 def repo_push_event_handler(self, event, data):
105 url = self.get_base_parsed_template(data)
106 url = self.get_base_parsed_template(data)
106 url_cals = []
107 url_cals = []
107 branch_data = OrderedDict()
108 branch_data = OrderedDict()
108 for obj in data['push']['branches']:
109 for obj in data['push']['branches']:
109 branch_data[obj['name']] = obj
110 branch_data[obj['name']] = obj
110
111
111 branches_commits = OrderedDict()
112 branches_commits = OrderedDict()
112 for commit in data['push']['commits']:
113 for commit in data['push']['commits']:
113 if commit.get('git_ref_change'):
114 if commit.get('git_ref_change'):
114 # special case for GIT that allows creating tags,
115 # special case for GIT that allows creating tags,
115 # deleting branches without associated commit
116 # deleting branches without associated commit
116 continue
117 continue
117
118
118 if commit['branch'] not in branches_commits:
119 if commit['branch'] not in branches_commits:
119 branch_commits = {'branch': branch_data[commit['branch']],
120 branch_commits = {'branch': branch_data[commit['branch']],
120 'commits': []}
121 'commits': []}
121 branches_commits[commit['branch']] = branch_commits
122 branches_commits[commit['branch']] = branch_commits
122
123
123 branch_commits = branches_commits[commit['branch']]
124 branch_commits = branches_commits[commit['branch']]
124 branch_commits['commits'].append(commit)
125 branch_commits['commits'].append(commit)
125
126
126 if '${branch}' in url:
127 if '${branch}' in url:
127 # call it multiple times, for each branch if used in variables
128 # call it multiple times, for each branch if used in variables
128 for branch, commit_ids in branches_commits.items():
129 for branch, commit_ids in branches_commits.items():
129 branch_url = string.Template(url).safe_substitute(branch=branch)
130 branch_url = string.Template(url).safe_substitute(branch=branch)
130 # call further down for each commit if used
131 # call further down for each commit if used
131 if '${commit_id}' in branch_url:
132 if '${commit_id}' in branch_url:
132 for commit_data in commit_ids['commits']:
133 for commit_data in commit_ids['commits']:
133 commit_id = commit_data['raw_id']
134 commit_id = commit_data['raw_id']
134 commit_url = string.Template(branch_url).safe_substitute(
135 commit_url = string.Template(branch_url).safe_substitute(
135 commit_id=commit_id)
136 commit_id=commit_id)
136 # register per-commit call
137 # register per-commit call
137 log.debug(
138 log.debug(
138 'register webhook call(%s) to url %s', event, commit_url)
139 'register webhook call(%s) to url %s', event, commit_url)
139 url_cals.append((commit_url, self.secret_token, self.headers, data))
140 url_cals.append((commit_url, self.secret_token, self.headers, data))
140
141
141 else:
142 else:
142 # register per-branch call
143 # register per-branch call
143 log.debug(
144 log.debug(
144 'register webhook call(%s) to url %s', event, branch_url)
145 'register webhook call(%s) to url %s', event, branch_url)
145 url_cals.append((branch_url, self.secret_token, self.headers, data))
146 url_cals.append((branch_url, self.secret_token, self.headers, data))
146
147
147 else:
148 else:
148 log.debug(
149 log.debug(
149 'register webhook call(%s) to url %s', event, url)
150 'register webhook call(%s) to url %s', event, url)
150 url_cals.append((url, self.secret_token, self.headers, data))
151 url_cals.append((url, self.secret_token, self.headers, data))
151
152
152 return url_cals
153 return url_cals
153
154
154 def repo_create_event_handler(self, event, data):
155 def repo_create_event_handler(self, event, data):
155 url = self.get_base_parsed_template(data)
156 url = self.get_base_parsed_template(data)
156 log.debug(
157 log.debug(
157 'register webhook call(%s) to url %s', event, url)
158 'register webhook call(%s) to url %s', event, url)
158 return [(url, self.secret_token, self.headers, data)]
159 return [(url, self.secret_token, self.headers, data)]
159
160
160 def pull_request_event_handler(self, event, data):
161 def pull_request_event_handler(self, event, data):
161 url = self.get_base_parsed_template(data)
162 url = self.get_base_parsed_template(data)
162 log.debug(
163 log.debug(
163 'register webhook call(%s) to url %s', event, url)
164 'register webhook call(%s) to url %s', event, url)
164 url = string.Template(url).safe_substitute(
165 url = string.Template(url).safe_substitute(
165 pull_request_id=data['pullrequest']['pull_request_id'],
166 pull_request_id=data['pullrequest']['pull_request_id'],
166 pull_request_url=data['pullrequest']['url'])
167 pull_request_url=data['pullrequest']['url'])
167 return [(url, self.secret_token, self.headers, data)]
168 return [(url, self.secret_token, self.headers, data)]
168
169
169 def __call__(self, event, data):
170 def __call__(self, event, data):
170 if isinstance(event, events.RepoPushEvent):
171 if isinstance(event, events.RepoPushEvent):
171 return self.repo_push_event_handler(event, data)
172 return self.repo_push_event_handler(event, data)
172 elif isinstance(event, events.RepoCreateEvent):
173 elif isinstance(event, events.RepoCreateEvent):
173 return self.repo_create_event_handler(event, data)
174 return self.repo_create_event_handler(event, data)
174 elif isinstance(event, events.PullRequestEvent):
175 elif isinstance(event, events.PullRequestEvent):
175 return self.pull_request_event_handler(event, data)
176 return self.pull_request_event_handler(event, data)
176 else:
177 else:
177 raise ValueError('event type not supported: %s' % events)
178 raise ValueError('event type not supported: %s' % events)
178
179
179
180
180 class WebhookSettingsSchema(colander.Schema):
181 class WebhookSettingsSchema(colander.Schema):
181 url = colander.SchemaNode(
182 url = colander.SchemaNode(
182 colander.String(),
183 colander.String(),
183 title=_('Webhook URL'),
184 title=_('Webhook URL'),
184 description=
185 description=
185 _('URL to which Webhook should submit data. Following variables '
186 _('URL to which Webhook should submit data. Following variables '
186 'are allowed to be used: {vars}. Some of the variables would '
187 'are allowed to be used: {vars}. Some of the variables would '
187 'trigger multiple calls, like ${{branch}} or ${{commit_id}}. '
188 'trigger multiple calls, like ${{branch}} or ${{commit_id}}. '
188 'Webhook will be called as many times as unique objects in '
189 'Webhook will be called as many times as unique objects in '
189 'data in such cases.').format(vars=URL_VARS),
190 'data in such cases.').format(vars=URL_VARS),
190 missing=colander.required,
191 missing=colander.required,
191 required=True,
192 required=True,
192 validator=colander.url,
193 validator=colander.url,
193 widget=deform.widget.TextInputWidget(
194 widget=deform.widget.TextInputWidget(
194 placeholder='https://www.example.com/webhook'
195 placeholder='https://www.example.com/webhook'
195 ),
196 ),
196 )
197 )
197 secret_token = colander.SchemaNode(
198 secret_token = colander.SchemaNode(
198 colander.String(),
199 colander.String(),
199 title=_('Secret Token'),
200 title=_('Secret Token'),
200 description=_('Optional string used to validate received payloads. '
201 description=_('Optional string used to validate received payloads. '
201 'It will be sent together with event data in JSON'),
202 'It will be sent together with event data in JSON'),
202 default='',
203 default='',
203 missing='',
204 missing='',
204 widget=deform.widget.TextInputWidget(
205 widget=deform.widget.TextInputWidget(
205 placeholder='e.g. secret_token'
206 placeholder='e.g. secret_token'
206 ),
207 ),
207 )
208 )
208 username = colander.SchemaNode(
209 username = colander.SchemaNode(
209 colander.String(),
210 colander.String(),
210 title=_('Username'),
211 title=_('Username'),
211 description=_('Optional username to authenticate the call.'),
212 description=_('Optional username to authenticate the call.'),
212 default='',
213 default='',
213 missing='',
214 missing='',
214 widget=deform.widget.TextInputWidget(
215 widget=deform.widget.TextInputWidget(
215 placeholder='e.g. admin'
216 placeholder='e.g. admin'
216 ),
217 ),
217 )
218 )
218 password = colander.SchemaNode(
219 password = colander.SchemaNode(
219 colander.String(),
220 colander.String(),
220 title=_('Password'),
221 title=_('Password'),
221 description=_('Optional password to authenticate the call.'),
222 description=_('Optional password to authenticate the call.'),
222 default='',
223 default='',
223 missing='',
224 missing='',
224 widget=deform.widget.PasswordWidget(
225 widget=deform.widget.PasswordWidget(
225 placeholder='e.g. secret.',
226 placeholder='e.g. secret.',
226 redisplay=True,
227 redisplay=True,
227 ),
228 ),
228 )
229 )
229 custom_header_key = colander.SchemaNode(
230 custom_header_key = colander.SchemaNode(
230 colander.String(),
231 colander.String(),
231 title=_('Custom Header Key'),
232 title=_('Custom Header Key'),
232 description=_('Custom Header name to be set when calling endpoint.'),
233 description=_('Custom Header name to be set when calling endpoint.'),
233 default='',
234 default='',
234 missing='',
235 missing='',
235 widget=deform.widget.TextInputWidget(
236 widget=deform.widget.TextInputWidget(
236 placeholder='e.g.Authorization'
237 placeholder='e.g.Authorization'
237 ),
238 ),
238 )
239 )
239 custom_header_val = colander.SchemaNode(
240 custom_header_val = colander.SchemaNode(
240 colander.String(),
241 colander.String(),
241 title=_('Custom Header Value'),
242 title=_('Custom Header Value'),
242 description=_('Custom Header value to be set when calling endpoint.'),
243 description=_('Custom Header value to be set when calling endpoint.'),
243 default='',
244 default='',
244 missing='',
245 missing='',
245 widget=deform.widget.TextInputWidget(
246 widget=deform.widget.TextInputWidget(
246 placeholder='e.g. RcLogin auth=xxxx'
247 placeholder='e.g. RcLogin auth=xxxx'
247 ),
248 ),
248 )
249 )
249 method_type = colander.SchemaNode(
250 method_type = colander.SchemaNode(
250 colander.String(),
251 colander.String(),
251 title=_('Call Method'),
252 title=_('Call Method'),
252 description=_('Select if the Webhook call should be made '
253 description=_('Select if the Webhook call should be made '
253 'with POST or GET.'),
254 'with POST or GET.'),
254 default='post',
255 default='post',
255 missing='',
256 missing='',
256 widget=deform.widget.RadioChoiceWidget(
257 widget=deform.widget.RadioChoiceWidget(
257 values=[('get', 'GET'), ('post', 'POST')],
258 values=[('get', 'GET'), ('post', 'POST')],
258 inline=True
259 inline=True
259 ),
260 ),
260 )
261 )
261
262
262
263
263 class WebhookIntegrationType(IntegrationTypeBase):
264 class WebhookIntegrationType(IntegrationTypeBase):
264 key = 'webhook'
265 key = 'webhook'
265 display_name = _('Webhook')
266 display_name = _('Webhook')
266 description = _('Post json events to a Webhook endpoint')
267 description = _('Post json events to a Webhook endpoint')
267 icon = '''<?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>'''
268 icon = '''<?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>'''
268
269
269 valid_events = [
270 valid_events = [
270 events.PullRequestCloseEvent,
271 events.PullRequestCloseEvent,
271 events.PullRequestMergeEvent,
272 events.PullRequestMergeEvent,
272 events.PullRequestUpdateEvent,
273 events.PullRequestUpdateEvent,
273 events.PullRequestCommentEvent,
274 events.PullRequestCommentEvent,
274 events.PullRequestReviewEvent,
275 events.PullRequestReviewEvent,
275 events.PullRequestCreateEvent,
276 events.PullRequestCreateEvent,
276 events.RepoPushEvent,
277 events.RepoPushEvent,
277 events.RepoCreateEvent,
278 events.RepoCreateEvent,
278 ]
279 ]
279
280
280 def settings_schema(self):
281 def settings_schema(self):
281 schema = WebhookSettingsSchema()
282 schema = WebhookSettingsSchema()
282 schema.add(colander.SchemaNode(
283 schema.add(colander.SchemaNode(
283 colander.Set(),
284 colander.Set(),
284 widget=deform.widget.CheckboxChoiceWidget(
285 widget=deform.widget.CheckboxChoiceWidget(
285 values=sorted(
286 values=sorted(
286 [(e.name, e.display_name) for e in self.valid_events]
287 [(e.name, e.display_name) for e in self.valid_events]
287 )
288 )
288 ),
289 ),
289 description="Events activated for this integration",
290 description="Events activated for this integration",
290 name='events'
291 name='events'
291 ))
292 ))
292 return schema
293 return schema
293
294
294 def send_event(self, event):
295 def send_event(self, event):
295 log.debug('handling event %s with Webhook integration %s',
296 log.debug('handling event %s with Webhook integration %s',
296 event.name, self)
297 event.name, self)
297
298
298 if event.__class__ not in self.valid_events:
299 if event.__class__ not in self.valid_events:
299 log.debug('event not valid: %r' % event)
300 log.debug('event not valid: %r' % event)
300 return
301 return
301
302
302 if event.name not in self.settings['events']:
303 if event.name not in self.settings['events']:
303 log.debug('event ignored: %r' % event)
304 log.debug('event ignored: %r' % event)
304 return
305 return
305
306
306 data = event.as_dict()
307 data = event.as_dict()
307 template_url = self.settings['url']
308 template_url = self.settings['url']
308
309
309 headers = {}
310 headers = {}
310 head_key = self.settings.get('custom_header_key')
311 head_key = self.settings.get('custom_header_key')
311 head_val = self.settings.get('custom_header_val')
312 head_val = self.settings.get('custom_header_val')
312 if head_key and head_val:
313 if head_key and head_val:
313 headers = {head_key: head_val}
314 headers = {head_key: head_val}
314
315
315 handler = WebhookHandler(
316 handler = WebhookHandler(
316 template_url, self.settings['secret_token'], headers)
317 template_url, self.settings['secret_token'], headers)
317
318
318 url_calls = handler(event, data)
319 url_calls = handler(event, data)
319 log.debug('webhook: calling following urls: %s',
320 log.debug('webhook: calling following urls: %s',
320 [x[0] for x in url_calls])
321 [x[0] for x in url_calls])
321
322
322 run_task(post_to_webhook, url_calls, self.settings)
323 run_task(post_to_webhook, url_calls, self.settings)
323
324
324
325
325 @async_task(ignore_result=True, base=RequestContextTask)
326 @async_task(ignore_result=True, base=RequestContextTask)
326 def post_to_webhook(url_calls, settings):
327 def post_to_webhook(url_calls, settings):
327 """
328 """
328 Example data::
329 Example data::
329
330
330 {'actor': {'user_id': 2, 'username': u'admin'},
331 {'actor': {'user_id': 2, 'username': u'admin'},
331 'actor_ip': u'192.168.157.1',
332 'actor_ip': u'192.168.157.1',
332 'name': 'repo-push',
333 'name': 'repo-push',
333 'push': {'branches': [{'name': u'default',
334 'push': {'branches': [{'name': u'default',
334 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
335 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
335 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
336 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
336 'branch': u'default',
337 'branch': u'default',
337 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
338 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
338 'issues': [],
339 'issues': [],
339 'mentions': [],
340 'mentions': [],
340 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
341 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
341 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
342 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
342 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
343 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
343 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
344 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
344 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
345 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
345 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
346 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
346 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
347 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
347 'reviewers': [],
348 'reviewers': [],
348 'revision': 9L,
349 'revision': 9L,
349 'short_id': 'a815cc738b96',
350 'short_id': 'a815cc738b96',
350 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
351 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
351 'issues': {}},
352 'issues': {}},
352 'repo': {'extra_fields': '',
353 'repo': {'extra_fields': '',
353 'permalink_url': u'http://rc.local:8080/_7',
354 'permalink_url': u'http://rc.local:8080/_7',
354 'repo_id': 7,
355 'repo_id': 7,
355 'repo_name': u'hg-repo',
356 'repo_name': u'hg-repo',
356 'repo_type': u'hg',
357 'repo_type': u'hg',
357 'url': u'http://rc.local:8080/hg-repo'},
358 'url': u'http://rc.local:8080/hg-repo'},
358 'server_url': u'http://rc.local:8080',
359 'server_url': u'http://rc.local:8080',
359 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
360 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
360
361
361 """
362 """
362 max_retries = 3
363 max_retries = 3
363 retries = Retry(
364 retries = Retry(
364 total=max_retries,
365 total=max_retries,
365 backoff_factor=0.15,
366 backoff_factor=0.15,
366 status_forcelist=[500, 502, 503, 504])
367 status_forcelist=[500, 502, 503, 504])
367 call_headers = {
368 call_headers = {
368 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
369 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
369 rhodecode.__version__)
370 rhodecode.__version__)
370 } # updated below with custom ones, allows override
371 } # updated below with custom ones, allows override
371
372
372 for url, token, headers, data in url_calls:
373 for url, token, headers, data in url_calls:
373 req_session = requests.Session()
374 req_session = requests.Session()
374 req_session.mount( # retry max N times
375 req_session.mount( # retry max N times
375 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
376 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
376
377
377 method = settings.get('method_type') or 'post'
378 method = settings.get('method_type') or 'post'
378 call_method = getattr(req_session, method)
379 call_method = getattr(req_session, method)
379
380
380 headers = headers or {}
381 headers = headers or {}
381 call_headers.update(headers)
382 call_headers.update(headers)
382 auth = get_auth(settings)
383 auth = get_auth(settings)
383
384
384 log.debug('calling Webhook with method: %s, and auth:%s',
385 log.debug('calling Webhook with method: %s, and auth:%s',
385 call_method, auth)
386 call_method, auth)
386 if settings.get('log_data'):
387 if settings.get('log_data'):
387 log.debug('calling webhook with data: %s', data)
388 log.debug('calling webhook with data: %s', data)
388 resp = call_method(url, json={
389 resp = call_method(url, json={
389 'token': token,
390 'token': token,
390 'event': data
391 'event': data
391 }, headers=call_headers, auth=auth)
392 }, headers=call_headers, auth=auth)
392 log.debug('Got Webhook response: %s', resp)
393 log.debug('Got Webhook response: %s', resp)
393
394
394 resp.raise_for_status() # raise exception on a failed request
395 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now