def get_builds_for_patchset_async(project, issue_id, patchset_id): """Queries BuildBucket for builds associated with the patchset. Requests for max 500 builds and does not check "next_cursor". Currently if more than 100 builds are requested, only 100 are returned. Presumably there will be no patchsets with >100 builds. Returns: A list of buildbucket build dicts. """ # See tag conventions http://cr-buildbucket.appspot.com/#docs/conventions . hostname = common.get_preferred_domain(project, default_to_appid=False) if not hostname: logging.error( 'Preferred domain name for this app is not set. ' 'See PREFERRED_DOMAIN_NAMES in settings.py: %r', hostname) raise ndb.Return([]) buildset_tag = BUILDSET_TAG_FORMAT.format( hostname=hostname, issue=issue_id, patch=patchset_id, ) params = { 'max_builds': 500, 'tag': 'buildset:%s' % buildset_tag, } url = '%s/search' % BUILDBUCKET_API_ROOT logging.info( 'Fetching builds for patchset %s/%s. Buildset: %s', issue_id, patchset_id, buildset_tag) try: resp = yield net.json_request_async( url, params=params, scopes='https://www.googleapis.com/auth/userinfo.email') except net.NotFoundError as ex: logging.error( 'Buildbucket returned 404 unexpectedly. Body: %s', ex.response) raise if 'error' in resp: bb_error = resp.get('error', {}) raise BuildBucketError( 'BuildBucket responded with error (reason %s): %s' % ( bb_error.get('reason', 'no-reason'), bb_error.get('message', 'no-message'))) raise ndb.Return(resp.get('builds') or [])
def get_builds_for_patchset_async(project, issue_id, patchset_id): """Queries BuildBucket for builds associated with the patchset. Requests for max 500 builds and does not check "next_cursor". Currently if more than 100 builds are requested, only 100 are returned. Presumably there will be no patchsets with >100 builds. Returns: A list of buildbucket build dicts. """ # See tag conventions http://cr-buildbucket.appspot.com/#docs/conventions . hostname = common.get_preferred_domain(project, default_to_appid=False) if not hostname: logging.error( 'Preferred domain name for this app is not set. ' 'See PREFERRED_DOMAIN_NAMES in settings.py: %r', hostname) raise ndb.Return([]) buildset_tag = BUILDSET_TAG_FORMAT.format( hostname=hostname, issue=issue_id, patch=patchset_id, ) params = { 'max_builds': 500, 'tag': 'buildset:%s' % buildset_tag, } url = '%s/search' % BUILDBUCKET_API_ROOT logging.info('Fetching builds for patchset %s/%s. Buildset: %s', issue_id, patchset_id, buildset_tag) try: resp = yield net.json_request_async( url, params=params, scopes='https://www.googleapis.com/auth/userinfo.email') except net.NotFoundError as ex: logging.error('Buildbucket returned 404 unexpectedly. Body: %s', ex.response) raise if 'error' in resp: bb_error = resp.get('error', {}) raise BuildBucketError( 'BuildBucket responded with error (reason %s): %s' % (bb_error.get( 'reason', 'no-reason'), bb_error.get('message', 'no-message'))) raise ndb.Return(resp.get('builds') or [])
def _mint_delegation_token_async(): """Generates an access token to impersonate the current user, if any. Memcaches the token. """ account = models.Account.current_user_account if account is None: raise ndb.Return(None) ctx = ndb.get_context() # Get from cache. cache_key = IMPERSONATION_TOKEN_CACHE_KEY_FORMAT % account.email token = yield ctx.memcache_get(cache_key) if token: raise ndb.Return(token) # Request a new one. logging.debug('Minting a delegation token for %s', account.email) req = { 'audience': ['user:%s' % app_identity.get_service_account_name()], 'services': ['service:%s' % BUILDBUCKET_APP_ID], 'impersonate': 'user:%s' % account.email, } resp = yield net.json_request_async( IMPERSONATION_TOKEN_MINT_URL, method='POST', payload=req, scopes=net.EMAIL_SCOPE) token = resp.get('delegation_token') if not token: raise BuildBucketError( 'Could not mint a delegation token. Response: %s' % resp) # Put to cache. validity_duration_sec = resp.get('validity_duration') assert isinstance(validity_duration_sec, int) if validity_duration_sec >= 10: validity_duration_sec -= 10 # Refresh the token 10 sec in advance. yield ctx.memcache_add(cache_key, token, time=validity_duration_sec) raise ndb.Return(token)
def rpc_async(method, path, **kwargs): """Makes an authenticated request to buildbucket. Impersonates the current user if he/she is logged in. Otherwise sends an anonymous request. """ assert 'scopes' not in kwargs assert 'headers' not in kwargs url = '%s/%s' % (BUILDBUCKET_API_ROOT, path) delegation_token = yield _mint_delegation_token_async() headers = {} scopes = None if delegation_token: headers['X-Delegation-Token-V1'] = delegation_token scopes = net.EMAIL_SCOPE res = yield net.json_request_async( url, method=method, headers=headers, scopes=scopes, **kwargs) raise ndb.Return(res)
def rpc_async(method, path, **kwargs): """Makes an authenticated request to buildbucket. Impersonates the current user if he/she is logged in. Otherwise sends an anonymous request. """ assert 'scopes' not in kwargs assert 'headers' not in kwargs url = '%s/%s' % (BUILDBUCKET_API_ROOT, path) delegation_token = yield _mint_delegation_token_async() headers = {} scopes = None if delegation_token: headers['X-Delegation-Token-V1'] = delegation_token scopes = net.EMAIL_SCOPE res = yield net.json_request_async(url, method=method, headers=headers, scopes=scopes, **kwargs) raise ndb.Return(res)
def _mint_delegation_token_async(): """Generates an access token to impersonate the current user, if any. Memcaches the token. """ account = models.Account.current_user_account if account is None: raise ndb.Return(None) ctx = ndb.get_context() # Get from cache. cache_key = IMPERSONATION_TOKEN_CACHE_KEY_FORMAT % account.email token_envelope = yield ctx.memcache_get(cache_key) if token_envelope: # Randomize token expiration time to workaround the case when multiple # concurrent requests start to refresh the token at the same time. token, exp_ts, lifetime_sec = token_envelope if time.time() < exp_ts - lifetime_sec * 0.05 * random.random(): logging.info('Fetched cached delegation token: fingerprint=%s', _get_token_fingerprint(token)) raise ndb.Return(token) # Request a new one. logging.debug('Minting a delegation token for %s', account.email) req = { 'delegatedIdentity': 'user:%s' % account.email, 'audience': ['REQUESTOR'], 'services': ['service:%s' % BUILDBUCKET_APP_ID], 'validityDuration': 5 * 3600, } resp = yield net.json_request_async( IMPERSONATION_TOKEN_MINT_URL, method='POST', payload=req, scopes=net.EMAIL_SCOPE, headers={'Accept': 'application/json; charset=utf-8'}) signed_token = resp.get('token') if not signed_token: raise BuildBucketError( 'Could not mint a delegation token. Response: %s' % resp) token_struct = resp.get('delegationSubtoken') if not token_struct or not isinstance(token_struct, dict): logging.error('Bad delegation token response: %s', resp) raise BuildBucketError('Could not mint a delegation token') logging.info( 'Token server "%s" generated token (subtoken_id=%s, fingerprint=%s):\n%s', resp.get('serviceVersion'), token_struct.get('subtokenId'), _get_token_fingerprint(signed_token), json.dumps(token_struct, sort_keys=True, indent=2, separators=(',', ': '))) # Put to cache. validity_duration_sec = token_struct.get('validityDuration') assert isinstance(validity_duration_sec, (int, float)) if validity_duration_sec >= 10: validity_duration_sec -= 10 # Refresh the token 10 sec in advance. exp_ts = int(time.time() + validity_duration_sec) yield ctx.memcache_set(key=cache_key, value=(signed_token, exp_ts, validity_duration_sec), time=exp_ts) raise ndb.Return(signed_token)