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())