blob: 30aee71187ae395d8d01cb797ffd1000e237431b [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This serves as a handler for PubSub push for builds."""
import base64
import json
import logging
import re
import urlparse
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
from google.protobuf.field_mask_pb2 import FieldMask
from buildbucket_proto.build_pb2 import Build
from gae_libs import appengine_util
from gae_libs.handlers.base_handler import BaseHandler
from gae_libs.handlers.base_handler import Permission
from common.waterfall.buildbucket_client import GetV2Build
from model.isolated_target import IsolatedTarget
_PROP_NAME_REGEX = re.compile(
r'swarm_hashes_(?P<ref>.*)\(at\)\{\#(?P<cp>[0-9]+)\}'
r'(?P<suffix>(_with(out)?_patch))?')
class CompletedBuildPubsubIngestor(BaseHandler):
"""Adds isolate targets to the index when pubsub notifies of completed build.
"""
PERMISSION_LEVEL = Permission.ANYONE # Protected with login:admin.
def HandlePost(self):
build_id = None
status = None
project = None
try:
envelope = json.loads(self.request.body)
build_id = envelope['message']['attributes']['build_id']
version = envelope['message']['attributes'].get('version')
if version and version != 'v1':
logging.info('Ignoring versions other than v1')
return
build = json.loads(base64.b64decode(envelope['message']['data']))['build']
status = build['status']
project = build['project']
except (ValueError, KeyError) as e:
# Ignore requests with invalid message.
logging.debug('build_id: %r', build_id)
logging.error('Unexpected PubSub message format: %s', e.message)
logging.debug('Post body: %s', self.request.body)
return
if status == 'COMPLETED':
_HandlePossibleCodeCoverageBuild(int(build_id)) # TODO: revert.
if project == 'chromium':
return _IngestProto(int(build_id))
# We don't care about pending or non-chromium builds, so we accept the
# notification by returning 200, and prevent pubsub from retrying it.
def _HandlePossibleCodeCoverageBuild(build_id): # pragma: no cover
"""Schedules a taskqueue task to process the code coverage data."""
# https://cloud.google.com/appengine/docs/standard/python/taskqueue/push/creating-tasks#target
try:
taskqueue.add(
name='coveragedata-%s' % build_id, # Avoid duplicate tasks.
url='/coverage/task/process-data/build/%s' % build_id,
target='code-coverage-backend', # Always use the default version.
queue_name='code-coverage-process-data')
except (taskqueue.TombstonedTaskError, taskqueue.TaskAlreadyExistsError):
logging.warning('Build %s was already scheduled to be processed', build_id)
def _DecodeSwarmingHashesPropertyName(prop):
"""Extracts ref, commit position and patch status from property name.
Args:
prop(str): The property name is expected to be in the following format:
swarm_hashes_<ref>(at){#<commit_position}<optional suffix>
"""
matches = _PROP_NAME_REGEX.match(prop)
with_patch = matches.group('suffix') == '_with_patch'
return matches.group('ref'), int(matches.group('cp')), with_patch
def _IngestProto(build_id):
"""Process a build described in a proto, i.e. buildbucket v2 api format."""
assert build_id
build = GetV2Build(
build_id,
fields=FieldMask(
paths=['id', 'output.properties', 'input', 'status', 'builder']))
if not build:
return BaseHandler.CreateError(
'Could not retrieve build #%d from buildbucket, retry' % build_id, 404)
# Sanity check.
assert build_id == build.id
properties_struct = build.output.properties
commit = build.input.gitiles_commit
patches = build.input.gerrit_changes
# Convert the Struct to standard dict, to use .get, .iteritems etc.
properties = dict(properties_struct.items())
swarm_hashes_properties = {}
for k, v in properties.iteritems():
if _PROP_NAME_REGEX.match(k):
swarm_hashes_properties[k] = v
if not swarm_hashes_properties:
logging.debug('Build %d does not have swarm_hashes property', build_id)
return
master_name = properties.get('target_mastername',
properties.get('mastername'))
if not master_name:
logging.error('Build %d does not have expected "mastername" property',
build_id)
return
luci_project = build.builder.project
luci_bucket = build.builder.bucket
luci_builder = properties.get('target_buildername') or build.builder.builder
if commit.host:
gitiles_host = commit.host
gitiles_project = commit.project
gitiles_ref = commit.ref or 'refs/heads/master'
else:
# Non-ci build, use 'repository' property instead to get base revision
# information.
repo_url = urlparse.urlparse(properties.get('repository', ''))
gitiles_host = repo_url.hostname or ''
gitiles_project = repo_url.path or ''
# Trim "/" prefix so that "/chromium/src" becomes
# "chromium/src", also remove ".git" suffix if present.
if gitiles_project.startswith('/'): # pragma: no branch
gitiles_project = gitiles_project[1:]
if gitiles_project.endswith('.git'): # pragma: no branch
gitiles_project = gitiles_project[:-len('.git')]
gitiles_ref = properties.get('gitiles_ref', 'refs/heads/master')
gerrit_patch = None
if len(patches) > 0:
gerrit_patch = '/'.join(
map(str, [patches[0].host, patches[0].change, patches[0].patchset]))
entities = []
for prop_name, swarm_hashes in swarm_hashes_properties.iteritems():
ref, commit_position, with_patch = _DecodeSwarmingHashesPropertyName(
prop_name)
for target_name, isolated_hash in swarm_hashes.items():
entities.append(
IsolatedTarget.Create(
build_id=build_id,
luci_project=luci_project,
bucket=luci_bucket,
master_name=master_name,
builder_name=luci_builder,
gitiles_host=gitiles_host,
gitiles_project=gitiles_project,
gitiles_ref=gitiles_ref or ref,
gerrit_patch=gerrit_patch if with_patch else '',
target_name=target_name,
isolated_hash=isolated_hash,
commit_position=commit_position))
result = [key.pairs() for key in ndb.put_multi(entities)]
return {'data': {'created_rows': result}}