github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/gubernator/view_pr.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 datetime 16 import json 17 import logging 18 import os 19 import time 20 21 import filters 22 import gcs_async 23 import github.models as ghm 24 import pull_request 25 import view_base 26 27 28 @view_base.memcache_memoize('pr-details://', expires=60 * 3) 29 def pr_builds(path): 30 """Return {job: [(build, {started.json}, {finished.json})]} for each job under gcs path.""" 31 jobs_dirs_fut = gcs_async.listdirs(path) 32 33 def base(path): 34 return os.path.basename(os.path.dirname(path)) 35 36 jobs_futures = [(job, gcs_async.listdirs(job)) for job in jobs_dirs_fut.get_result()] 37 futures = [] 38 39 for job, builds_fut in jobs_futures: 40 for build in builds_fut.get_result(): 41 futures.append([ 42 base(job), 43 base(build), 44 gcs_async.read('/%sstarted.json' % build), 45 gcs_async.read('/%sfinished.json' % build)]) 46 47 futures.sort(key=lambda (job, build, s, f): (job, view_base.pad_numbers(build)), reverse=True) 48 49 jobs = {} 50 for job, build, started_fut, finished_fut in futures: 51 started = started_fut.get_result() 52 finished = finished_fut.get_result() 53 if started is not None: 54 started = json.loads(started) 55 if finished is not None: 56 finished = json.loads(finished) 57 jobs.setdefault(job, []).append((build, started, finished)) 58 59 return jobs 60 61 62 def pr_path(org, repo, pr, default_org, default_repo, pull_prefix): 63 """Builds the correct gs://prefix/maybe_kubernetes/maybe_repo_org/pr.""" 64 if org == default_org and repo == default_repo: 65 return '%s/%s' % (pull_prefix, pr) 66 if org == default_org: 67 return '%s/%s/%s' % (pull_prefix, repo, pr) 68 return '%s/%s_%s/%s' % (pull_prefix, org, repo, pr) 69 70 71 def org_repo(path, default_org, default_repo): 72 """Converts /maybe_org/maybe_repo into (org, repo).""" 73 parts = path.split('/')[1:] 74 if len(parts) == 2: 75 org, repo = parts 76 elif len(parts) == 1: 77 org = default_org 78 repo = parts[0] 79 else: 80 org = default_org 81 repo = default_repo 82 return org, repo 83 84 85 class PRHandler(view_base.BaseHandler): 86 """Show a list of test runs for a PR.""" 87 def get(self, path, pr): 88 # pylint: disable=too-many-locals 89 org, repo = org_repo(path=path, 90 default_org=self.app.config['default_org'], 91 default_repo=self.app.config['default_repo'], 92 ) 93 path = pr_path(org=org, repo=repo, pr=pr, 94 pull_prefix=self.app.config['external_services'][org]['gcs_pull_prefix'], 95 default_org=self.app.config['default_org'], 96 default_repo=self.app.config['default_repo'], 97 ) 98 builds = pr_builds(path) 99 # TODO(fejta): assume all builds are monotonically increasing. 100 for bs in builds.itervalues(): 101 if any(len(b) > 8 for b, _, _ in bs): 102 bs.sort(key=lambda (b, s, f): -(s or {}).get('timestamp', 0)) 103 if pr == 'batch': # truncate batch results to last day 104 cutoff = time.time() - 60 * 60 * 24 105 builds = {} 106 for job, job_builds in builds.iteritems(): 107 builds[job] = [ 108 (b, s, f) for b, s, f in job_builds 109 if not s or s.get('timestamp') > cutoff 110 ] 111 112 max_builds, headings, rows = pull_request.builds_to_table(builds) 113 digest = ghm.GHIssueDigest.get('%s/%s' % (org, repo), pr) 114 self.render( 115 'pr.html', 116 dict( 117 pr=pr, 118 digest=digest, 119 max_builds=max_builds, 120 header=headings, 121 org=org, 122 repo=repo, 123 rows=rows, 124 path=path, 125 ) 126 ) 127 128 129 def get_acks(login, prs): 130 acks = {} 131 result = ghm.GHUserState.make_key(login).get() 132 if result: 133 acks = result.acks 134 if prs: 135 # clear acks for PRs that user is no longer involved in. 136 stale = set(acks) - set(pr.key.id() for pr in prs) 137 if stale: 138 for key in stale: 139 result.acks.pop(key) 140 result.put() 141 return acks 142 143 144 class PRDashboard(view_base.BaseHandler): 145 def get(self, user=None): 146 # pylint: disable=singleton-comparison 147 login = self.session.get('user') 148 if not user: 149 user = login 150 if not user: 151 self.redirect('/github_auth/pr') 152 return 153 logging.debug('user=%s', user) 154 elif user == 'all': 155 user = None 156 qs = [ghm.GHIssueDigest.is_pr == True] 157 if not self.request.get('all', False): 158 qs.append(ghm.GHIssueDigest.is_open == True) 159 if user: 160 qs.append(ghm.GHIssueDigest.involved == user) 161 prs = list(ghm.GHIssueDigest.query(*qs)) 162 prs.sort(key=lambda x: x.updated_at, reverse=True) 163 164 acks = None 165 if login and user == login: # user getting their own page 166 acks = get_acks(login, prs) 167 168 fmt = self.request.get('format', 'html') 169 if fmt == 'json': 170 self.response.headers['Content-Type'] = 'application/json' 171 def serial(obj): 172 if isinstance(obj, datetime.datetime): 173 return obj.isoformat() 174 elif isinstance(obj, ghm.GHIssueDigest): 175 # pylint: disable=protected-access 176 keys = ['repo', 'number'] + list(obj._values) 177 return {k: getattr(obj, k) for k in keys} 178 raise TypeError 179 self.response.write(json.dumps(prs, sort_keys=True, default=serial)) 180 elif fmt == 'html': 181 if user: 182 def acked(p): 183 if 'lgtm' in p.payload.get('labels', {}): 184 return True # LGTM is an implicit Ack 185 if acks is None: 186 return False 187 return filters.do_get_latest(p.payload, user) <= acks.get(p.key.id(), 0) 188 cats = [ 189 ('Needs Attention', lambda p: user in p.payload['attn'] and not acked(p), ''), 190 ('Approvable', lambda p: user in p.payload.get('approvers', []), 191 'is:open is:pr ("additional approvers: {0}" ' + 192 'OR "additional approver: {0}")'.format(user)), 193 ('Incoming', lambda p: user != p.payload['author'] and 194 user in p.payload['assignees'], 195 'is:open is:pr user:kubernetes assignee:%s' % user), 196 ('Outgoing', lambda p: user == p.payload['author'], 197 'is:open is:pr user:kubernetes author:%s' % user), 198 ] 199 else: 200 cats = [('Open Kubernetes PRs', lambda x: True, 201 'is:open is:pr user:kubernetes')] 202 203 self.render('pr_dashboard.html', dict( 204 prs=prs, cats=cats, user=user, login=login, acks=acks)) 205 else: 206 self.abort(406) 207 208 def post(self): 209 login = self.session.get('user') 210 if not login: 211 self.abort(403) 212 state = ghm.GHUserState.make_key(login).get() 213 if state is None: 214 state = ghm.GHUserState.make(login) 215 body = json.loads(self.request.body) 216 if body['command'] == 'ack': 217 delta = {'%s %s' % (body['repo'], body['number']): body['latest']} 218 state.acks.update(delta) 219 state.put() 220 elif body['command'] == 'ack-clear': 221 state.acks = {} 222 state.put() 223 else: 224 self.abort(400) 225 226 227 class PRBuildLogHandler(view_base.BaseHandler): 228 def get(self, path): 229 org, _ = org_repo(path=path, 230 default_org=self.app.config['default_org'], 231 default_repo=self.app.config['default_repo'], 232 ) 233 self.redirect('https://storage.googleapis.com/%s/%s' % ( 234 self.app.config['external_services'][org]['gcs_pull_prefix'], path 235 ))