OLD | NEW |
1 # Copyright 2008 Google Inc. | 1 # Copyright 2008 Google Inc. |
2 # | 2 # |
3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
4 # you may not use this file except in compliance with 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 | 5 # You may obtain a copy of the License at |
6 # | 6 # |
7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
8 # | 8 # |
9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
13 # limitations under the License. | 13 # limitations under the License. |
14 | 14 |
15 """Rietveld-BuildBucket integration module.""" | 15 """Rietveld-BuildBucket integration module.""" |
16 | 16 |
17 import datetime | 17 import datetime |
18 import json | 18 import json |
19 import logging | 19 import logging |
20 import os | 20 import os |
21 import urllib | 21 import urllib |
| 22 import uuid |
22 | 23 |
23 from google.appengine.api import app_identity | 24 from google.appengine.api import app_identity |
24 from google.appengine.api import memcache | 25 from google.appengine.api import memcache |
25 from google.appengine.api import users | 26 from google.appengine.api import users |
26 from google.appengine.ext import ndb | 27 from google.appengine.ext import ndb |
27 | 28 |
28 from django.conf import settings | 29 from django.conf import settings |
29 | 30 |
30 from codereview import common | 31 from codereview import common |
31 from codereview import models | 32 from codereview import models |
32 from codereview import net | 33 from codereview import net |
33 | 34 |
34 EPOCH = datetime.datetime.utcfromtimestamp(0) | 35 EPOCH = datetime.datetime.utcfromtimestamp(0) |
35 BUILDBUCKET_APP_ID = ( | 36 BUILDBUCKET_APP_ID = ( |
36 'cr-buildbucket-test' if common.IS_DEV else 'cr-buildbucket') | 37 'cr-buildbucket-test' if common.IS_DEV else 'cr-buildbucket') |
37 BUILDBUCKET_API_ROOT = ( | 38 BUILDBUCKET_API_ROOT = ( |
38 'https://%s.appspot.com/_ah/api/buildbucket/v1' % BUILDBUCKET_APP_ID) | 39 'https://%s.appspot.com/_ah/api/buildbucket/v1' % BUILDBUCKET_APP_ID) |
39 # See tag conventions http://cr-buildbucket.appspot.com/#docs/conventions | 40 # See the convention |
| 41 # https://chromium.googlesource.com/infra/infra/+/master/appengine/cr-buildbucke
t/doc/index.md#buildset-tag |
40 BUILDSET_TAG_FORMAT = 'patch/rietveld/{hostname}/{issue}/{patch}' | 42 BUILDSET_TAG_FORMAT = 'patch/rietveld/{hostname}/{issue}/{patch}' |
41 | 43 |
42 AUTH_SERVICE_ID = 'chrome-infra-auth' | 44 AUTH_SERVICE_ID = 'chrome-infra-auth' |
43 IMPERSONATION_TOKEN_MINT_URL = ( | 45 IMPERSONATION_TOKEN_MINT_URL = ( |
44 'https://%s.appspot.com/auth_service/api/v1/delegation/token/create' % | 46 'https://%s.appspot.com/auth_service/api/v1/delegation/token/create' % |
45 AUTH_SERVICE_ID) | 47 AUTH_SERVICE_ID) |
46 IMPERSONATION_TOKEN_CACHE_KEY_FORMAT = 'impersonation_token/v1/%s' | 48 IMPERSONATION_TOKEN_CACHE_KEY_FORMAT = 'impersonation_token/v1/%s' |
47 | 49 |
48 | 50 |
49 class BuildBucketError(Exception): | 51 class BuildBucketError(Exception): |
(...skipping 13 matching lines...) Expand all Loading... |
63 @property | 65 @property |
64 def is_from_buildbucket(self): | 66 def is_from_buildbucket(self): |
65 # Used in build_result.html template. | 67 # Used in build_result.html template. |
66 return True | 68 return True |
67 | 69 |
68 @classmethod | 70 @classmethod |
69 def convert_status_to_result(cls, build): | 71 def convert_status_to_result(cls, build): |
70 """Converts build status to TryJobResult.result. | 72 """Converts build status to TryJobResult.result. |
71 | 73 |
72 See buildbucket docs here: | 74 See buildbucket docs here: |
73 https://cr-buildbucket.appspot.com/#/docs/build | 75 https://chromium.googlesource.com/infra/infra/+/master/appengine/cr-buildbuc
ket/doc/index.md#Build |
74 """ | 76 """ |
75 status = build.get('status') | 77 status = build.get('status') |
76 if status == 'SCHEDULED': | 78 if status == 'SCHEDULED': |
77 return cls.TRYPENDING | 79 return cls.TRYPENDING |
78 | 80 |
79 if status == 'COMPLETED': | 81 if status == 'COMPLETED': |
80 if build.get('result') == 'SUCCESS': | 82 if build.get('result') == 'SUCCESS': |
81 return cls.SUCCESS | 83 return cls.SUCCESS |
82 if build.get('result') == 'FAILURE': | 84 if build.get('result') == 'FAILURE': |
83 if build.get('failure_reason') == 'BUILD_FAILURE': | 85 if build.get('failure_reason') == 'BUILD_FAILURE': |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
153 def get_builds_for_patchset_async(project, issue_id, patchset_id): | 155 def get_builds_for_patchset_async(project, issue_id, patchset_id): |
154 """Queries BuildBucket for builds associated with the patchset. | 156 """Queries BuildBucket for builds associated with the patchset. |
155 | 157 |
156 Requests for max 500 builds and does not check "next_cursor". Currently if | 158 Requests for max 500 builds and does not check "next_cursor". Currently if |
157 more than 100 builds are requested, only 100 are returned. Presumably there | 159 more than 100 builds are requested, only 100 are returned. Presumably there |
158 will be no patchsets with >100 builds. | 160 will be no patchsets with >100 builds. |
159 | 161 |
160 Returns: | 162 Returns: |
161 A list of buildbucket build dicts. | 163 A list of buildbucket build dicts. |
162 """ | 164 """ |
163 # See tag conventions http://cr-buildbucket.appspot.com/#docs/conventions . | 165 buildset_tag = get_buildset_for(project, issue_id, patchset_id) |
164 hostname = common.get_preferred_domain(project, default_to_appid=False) | |
165 if not hostname: | |
166 logging.error( | |
167 'Preferred domain name for this app is not set. ' | |
168 'See PREFERRED_DOMAIN_NAMES in settings.py: %r', hostname) | |
169 raise ndb.Return([]) | |
170 | |
171 buildset_tag = BUILDSET_TAG_FORMAT.format( | |
172 hostname=hostname, | |
173 issue=issue_id, | |
174 patch=patchset_id, | |
175 ) | |
176 params = { | 166 params = { |
177 'max_builds': 500, | 167 'max_builds': 500, |
178 'tag': 'buildset:%s' % buildset_tag, | 168 'tag': 'buildset:%s' % buildset_tag, |
179 } | 169 } |
180 | 170 |
181 logging.info( | 171 logging.info( |
182 'Fetching builds for patchset %s/%s. Buildset: %s', | 172 'Fetching builds for patchset %s/%s. Buildset: %s', |
183 issue_id, patchset_id, buildset_tag) | 173 issue_id, patchset_id, buildset_tag) |
184 try: | 174 try: |
185 resp = yield rpc_async('GET', 'search', params=params) | 175 resp = yield rpc_async('GET', 'search', params=params) |
(...skipping 19 matching lines...) Expand all Loading... |
205 try_job_result = BuildbucketTryJobResult.from_build(build) | 195 try_job_result = BuildbucketTryJobResult.from_build(build) |
206 if not try_job_result.builder: | 196 if not try_job_result.builder: |
207 logging.info( | 197 logging.info( |
208 'Build %s does not have a builder' % try_job_result.build_id) | 198 'Build %s does not have a builder' % try_job_result.build_id) |
209 continue | 199 continue |
210 results.append(try_job_result) | 200 results.append(try_job_result) |
211 raise ndb.Return(results) | 201 raise ndb.Return(results) |
212 | 202 |
213 | 203 |
214 ################################################################################ | 204 ################################################################################ |
| 205 ## Scheduling builds. |
| 206 |
| 207 |
| 208 def schedule(issue, patchset_id, builds): |
| 209 """Schedules builds on buildbucket. |
| 210 |
| 211 |builds| is a list of (master, builder) tuples, where master must not have |
| 212 '.master' prefix. |
| 213 """ |
| 214 account = models.Account.current_user_account |
| 215 assert account, 'User is not logged in; cannot schedule builds.' |
| 216 |
| 217 if not builds: |
| 218 return |
| 219 |
| 220 self_hostname = common.get_preferred_domain(issue.project) |
| 221 |
| 222 req = {'builds':[]} |
| 223 opid = uuid.uuid4() |
| 224 |
| 225 for i, (master, builder) in enumerate(builds): |
| 226 # Build definitions are similar to what CQ produces: |
| 227 # https://chrome-internal.googlesource.com/infra/infra_internal/+/c3092da989
75c7a3e083093f21f0f4130c66a51c/commit_queue/buildbucket_util.py#171 |
| 228 req['builds'].append({ |
| 229 'bucket': 'master.%s' % master, |
| 230 'parameters_json': json.dumps({ |
| 231 'builder_name': builder, |
| 232 'changes': [{ |
| 233 'author': {'email': issue.owner.email()}, |
| 234 'url': 'https://%s/%s/%s/' % ( |
| 235 self_hostname, issue.key.id(), patchset_id) |
| 236 }], |
| 237 'properties': { |
| 238 'issue': issue.key.id(), |
| 239 'master': master, |
| 240 'patch_project': issue.project, |
| 241 'patch_storage': 'rietveld', |
| 242 'patchset': patchset_id, |
| 243 'project': issue.project, |
| 244 'rietveld': self_hostname, |
| 245 }, |
| 246 }), |
| 247 'tags': [ |
| 248 'builder:%s' % builder, |
| 249 'buildset:%s' % get_buildset_for( |
| 250 issue.project, issue.key.id(), patchset_id), |
| 251 'master:%s' % master, |
| 252 'user_agent:rietveld', |
| 253 ], |
| 254 'client_operation_id': '%s:%s' % (opid, i), |
| 255 }) |
| 256 |
| 257 logging.debug( |
| 258 'Scheduling %d builds on behalf of %s', len(req['builds']), account.email) |
| 259 res = rpc_async('PUT', 'builds/batch', payload=req).get_result() |
| 260 for r in res['results']: |
| 261 error = r.get('error') |
| 262 if error: |
| 263 logging.error('Build scheduling failed. Response: %r', res) |
| 264 raise BuildBucketError('Could not schedule build(s): %r' % error) |
| 265 |
| 266 actual_builds = [r['build'] for r in res['results']] |
| 267 logging.info( |
| 268 'Scheduled buildbucket builds: %r', |
| 269 ', '.join([str(b['id']) for b in actual_builds])) |
| 270 return actual_builds |
| 271 |
| 272 |
| 273 ################################################################################ |
215 ## Buildbucket RPC. | 274 ## Buildbucket RPC. |
216 | 275 |
217 | 276 |
218 @ndb.tasklet | 277 @ndb.tasklet |
219 def _mint_delegation_token_async(): | 278 def _mint_delegation_token_async(): |
220 """Generates an access token to impersonate the current user, if any. | 279 """Generates an access token to impersonate the current user, if any. |
221 | 280 |
222 Memcaches the token. | 281 Memcaches the token. |
223 """ | 282 """ |
224 account = models.Account.current_user_account | 283 account = models.Account.current_user_account |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
319 return None | 378 return None |
320 return value | 379 return value |
321 | 380 |
322 | 381 |
323 def timestamp_to_datetime(timestamp): | 382 def timestamp_to_datetime(timestamp): |
324 if timestamp is None: | 383 if timestamp is None: |
325 return None | 384 return None |
326 if isinstance(timestamp, basestring): | 385 if isinstance(timestamp, basestring): |
327 timestamp = int(timestamp) | 386 timestamp = int(timestamp) |
328 return EPOCH + datetime.timedelta(microseconds=timestamp) | 387 return EPOCH + datetime.timedelta(microseconds=timestamp) |
| 388 |
| 389 |
| 390 def get_buildset_for(project, issue_id, patchset_id): |
| 391 # See the convention |
| 392 # https://chromium.googlesource.com/infra/infra/+/master/appengine/cr-buildbuc
ket/doc/index.md#buildset-tag |
| 393 hostname = common.get_preferred_domain(project, default_to_appid=False) |
| 394 if not hostname: |
| 395 logging.error( |
| 396 'Preferred domain name for this app is not set. ' |
| 397 'See PREFERRED_DOMAIN_NAMES in settings.py: %r', hostname) |
| 398 raise ndb.Return([]) |
| 399 |
| 400 return BUILDSET_TAG_FORMAT.format( |
| 401 hostname=hostname, |
| 402 issue=issue_id, |
| 403 patch=patchset_id, |
| 404 ) |
OLD | NEW |