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