github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/gubernator/github/handlers.py (about) 1 # Copyright 2016 The Kubernetes Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import cgi 16 import datetime 17 import hashlib 18 import hmac 19 import logging 20 import json 21 import traceback 22 23 import webapp2 24 from webapp2_extras import security 25 26 from google.appengine.api.runtime import memory_usage 27 from google.appengine.datastore import datastore_query 28 29 import classifier 30 import models 31 32 33 try: 34 WEBHOOK_SECRET = open('webhook_secret').read().strip() 35 except IOError: 36 logging.warning('unable to load webhook secret') 37 WEBHOOK_SECRET = 'default' 38 39 40 def make_signature(body): 41 hmac_instance = hmac.HMAC(WEBHOOK_SECRET, body, hashlib.sha1) 42 return 'sha1=' + hmac_instance.hexdigest() 43 44 45 class GithubHandler(webapp2.RequestHandler): 46 ''' 47 Handle POSTs delivered using GitHub's webhook interface. Posts are 48 authenticated with HMAC signatures and a shared secret. 49 50 Each event is saved to a database, and can trigger additional 51 processing. 52 ''' 53 def post(self): 54 event = self.request.headers.get('x-github-event') 55 signature = self.request.headers.get('x-hub-signature', '') 56 body = self.request.body 57 58 expected_signature = make_signature(body) 59 if not security.compare_hashes(signature, expected_signature): 60 logging.error('webhook failed signature check') 61 self.abort(400) 62 63 body_json = json.loads(body) 64 repo = body_json.get('repository', {}).get('full_name') 65 number = None 66 if 'pull_request' in body_json: 67 number = body_json['pull_request']['number'] 68 elif 'issue' in body_json: 69 number = body_json['issue']['number'] 70 71 parent = None 72 if number: 73 parent = models.GithubResource.make_key(repo, number) 74 75 kwargs = {} 76 timestamp = self.request.headers.get('x-timestamp') 77 if timestamp is not None: 78 kwargs['timestamp'] = datetime.datetime.strptime( 79 timestamp, '%Y-%m-%d %H:%M:%S.%f') 80 81 webhook = models.GithubWebhookRaw( 82 parent=parent, 83 repo=repo, number=number, event=event, body=body, **kwargs) 84 webhook.put() 85 86 if event == 'status': 87 status = models.GHStatus.from_json(body_json) 88 models.save_if_newer(status) 89 query = models.GHIssueDigest.find_head(repo, status.sha) 90 for issue in query.fetch(): 91 update_issue_digest(issue.repo, issue.number) 92 93 if number is not None: 94 update_issue_digest(repo, number) 95 96 97 def update_issue_digest(repo, number, always_put=False): 98 digest = models.GHIssueDigest.make(repo, number, 99 *classifier.classify_issue(repo, number)) 100 if always_put: 101 digest.put() 102 else: 103 models.save_if_newer(digest) 104 105 106 class BaseHandler(webapp2.RequestHandler): 107 def dispatch(self): 108 # Eh, this is less work than making all the debug pages escape properly. 109 # No resources allowed except for inline CSS, no iframing of content. 110 self.response.headers['Content-Security-Policy'] = \ 111 "default-src none; style-src 'unsafe-inline'; frame-ancestors none" 112 super(BaseHandler, self).dispatch() 113 114 115 class Events(BaseHandler): 116 ''' 117 Perform input/output on a series of webhook events from the datastore, for 118 debugging purposes. 119 ''' 120 def get(self): 121 cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor')) 122 repo = self.request.get('repo') 123 number = int(self.request.get('number', 0)) or None 124 count = int(self.request.get('count', 500)) 125 if repo is not None and number is not None: 126 q = models.GithubWebhookRaw.query( 127 models.GithubWebhookRaw.repo == repo, 128 models.GithubWebhookRaw.number == number) 129 else: 130 q = models.GithubWebhookRaw.query() 131 q = q.order(models.GithubWebhookRaw.timestamp) 132 events, next_cursor, more = q.fetch_page(count, start_cursor=cursor) 133 out = [] 134 for event in events: 135 out.append({'repo': event.repo, 'event': event.event, 136 'timestamp': str(event.timestamp), 137 'body': json.loads(event.body)}) 138 resp = {'next': more and next_cursor.urlsafe(), 'calls': out} 139 self.response.headers['content-type'] = 'text/json' 140 self.response.write(json.dumps(resp, indent=4, sort_keys=True)) 141 142 143 class Status(BaseHandler): 144 def get(self): 145 repo = self.request.get('repo') 146 sha = self.request.get('sha') 147 if not repo or not sha: 148 self.abort(403) 149 return 150 results = models.GHStatus.query_for_sha(repo, sha) 151 self.response.write('<table>') 152 for res in results: 153 self.response.write('<tr><td>%s<td>%s<td><a href="%s">%s</a>\n' % 154 (res.context, res.state, res.target_url, res.description)) 155 156 157 class Timeline(BaseHandler): 158 ''' 159 Render all the information in the datastore about a particular issue. 160 161 This is used for debugging and investigations. 162 ''' 163 def emit_classified(self, repo, number): 164 try: 165 self.response.write('<h3>Classifier Output</h3>') 166 ret = classifier.classify_issue(repo, number) 167 self.response.write('<ul><li>pr: %s<li>open: %s<li>involved: %s' 168 % tuple(ret[:3])) 169 self.response.write('<li>last_event_timestamp: %s' % ret[4]) 170 self.response.write('<li>payload len: %d' %len(json.dumps(ret[3]))) 171 self.response.write('<pre>%s</pre></ul>' % cgi.escape( 172 json.dumps(ret[3], indent=2, sort_keys=True))) 173 except BaseException: 174 self.response.write('<pre>%s</pre>' % traceback.format_exc()) 175 176 def emit_events(self, repo, number): 177 ancestor = models.GithubResource.make_key(repo, number) 178 events = list(models.GithubWebhookRaw.query(ancestor=ancestor)) 179 events.sort(key=lambda e: e.timestamp) 180 181 self.response.write('<h3>Distilled Events</h3>') 182 self.response.write('<pre>') 183 event_pairs = [event.to_tuple() for event in events] 184 for ev in classifier.distill_events(event_pairs): 185 self.response.write(cgi.escape('%s, %s %s\n' % ev)) 186 self.response.write('</pre>') 187 188 self.response.write('<h3>%d Raw Events</h3>' % (len(events))) 189 self.response.write('<table border=2>') 190 merged = {} 191 for event in events: 192 body_json = json.loads(event.body) 193 models.shrink(body_json) 194 if 'issue' in body_json: 195 merged.update(body_json['issue']) 196 elif 'pull_request' in body_json: 197 merged.update(body_json['pull_request']) 198 body = json.dumps(body_json, indent=2) 199 action = body_json.get('action') 200 sender = body_json.get('sender', {}).get('login') 201 self.response.write('<tr><td>%s\n' % '<td>'.join(str(x) for x in 202 [event.timestamp, event.event, action, sender, 203 '<pre>' + cgi.escape(body)])) 204 return merged 205 206 def get(self): 207 repo = self.request.get('repo') 208 number = self.request.get('number') 209 if self.request.get('format') == 'json': 210 ancestor = models.GithubResource.make_key(repo, number) 211 events = list(models.GithubWebhookRaw.query(ancestor=ancestor)) 212 self.response.headers['content-type'] = 'application/json' 213 self.response.write(json.dumps([e.body for e in events], indent=True)) 214 return 215 self.response.write( 216 '<style>td pre{max-height:200px;overflow:scroll}</style>') 217 self.response.write('<p>Memory: %s' % memory_usage().current()) 218 self.emit_classified(repo, number) 219 self.response.write('<p>Memory: %s' % memory_usage().current()) 220 if self.request.get('classify_only'): 221 return 222 merged = self.emit_events(repo, number) 223 self.response.write('<p>Memory: %s' % memory_usage().current()) 224 if 'head' in merged: 225 sha = merged['head']['sha'] 226 results = models.GHStatus.query_for_sha(repo, sha) 227 self.response.write('</table><table>') 228 for res in results: 229 self.response.write('<tr><td>%s<td>%s<td><a href="%s">%s</a>\n' 230 % (res.context, res.state, res.target_url, res.description)) 231 models.shrink(merged) 232 self.response.write('</table><pre>%s</pre>' % cgi.escape( 233 json.dumps(merged, indent=2, sort_keys=True))) 234 self.response.write('<p>Memory: %s' % memory_usage().current())