blob: 48340fd0d28b201c0921d1d086ad92fb7289a3b8 [file] [log] [blame]
# Copyright 2016 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.
"""API for interacting with the buildbucket service directly.
Instead of triggering jobs by emitting annotations then handled by the master,
this module allows slaves to directly post requests to buildbucket.
"""
import json
from recipe_engine import recipe_api
class BuildbucketApi(recipe_api.RecipeApi):
"""A module for interacting with buildbucket."""
def __init__(self, buildername, buildnumber, *args, **kwargs):
super(BuildbucketApi, self).__init__(*args, **kwargs)
self._buildername = buildername
self._buildnumber = buildnumber
self._properties = None
def get_config_defaults(self):
if self.m.platform.is_win:
return {'PLATFORM': 'win'}
return {'PLATFORM': 'default'}
def _configure_defaults(self):
"""Apply default configuration if no configuration has been set.
Ideally whoever uses this api will explicitly set the configuration by
doing `api.buildbucket.set_config('production_buildbucket')`, but to make
this module usable even in case they don't configure it, we set the default
to the production instance of buildbucket."""
# There's only two items in this module's configuration, the path to the
# buildbucket cli client binary and the buildbucket hostname, this default
# configuration will override them.
if not self.c or not self.c.complete():
self.set_config('production_buildbucket')
def _tags_for_build(self, bucket, parameters, override_tags=None):
buildbucket_info = self.properties or {}
original_tags_list = buildbucket_info.get('build', {}).get('tags', [])
original_tags = dict(t.split(':', 1) for t in original_tags_list)
new_tags = {'user_agent': 'recipe'}
if 'buildset' in original_tags:
new_tags['buildset'] = original_tags['buildset']
builder_name = parameters.get('builder_name')
if builder_name:
new_tags['builder'] = builder_name
if bucket.startswith('master.'):
new_tags['master'] = bucket[7:]
if self._buildnumber is not None:
new_tags['parent_buildnumber'] = str(self._buildnumber)
if self._buildername is not None:
new_tags['parent_buildername'] = str(self._buildername)
new_tags.update(override_tags or {})
return sorted([':'.join((x, y)) for x, y in new_tags.iteritems()])
@property
def properties(self):
"""Returns (dict-like or None): The BuildBucket properties, if present."""
if self._properties is None:
# Not cached, load and deserialize from properties.
props = self.m.properties.get('buildbucket')
if props is not None:
if isinstance(props, basestring):
props = json.loads(props)
self._properties = props
return self._properties
def put(self, builds, service_account=None, **kwargs):
"""Puts a batch of builds.
Args:
builds (list): A list of dicts, where keys are:
'bucket': (required) name of the bucket for the request.
'parameters' (dict): (required) arbitrary json-able parameters that a
build system would be able to interpret.
'tags': (optional) a dict(str->str) of tags for the build. These will
be added to those generated by this method and override them if
appropriate.
service_account (str): (optional) path to locally saved secrets for
service account to authenticate as.
Returns:
A step that as its .stdout property contains the response object as
returned by buildbucket.
"""
build_specs = []
for build in builds:
build_specs.append(json.dumps({
'bucket': build['bucket'],
'parameters_json': json.dumps(build['parameters'], sort_keys=True),
'tags': self._tags_for_build(build['bucket'], build['parameters'],
build.get('tags')),
}, sort_keys=True))
return self._call_service('put', build_specs, service_account, **kwargs)
def cancel_build(self, build_id, service_account=None, **kwargs):
return self._call_service('cancel', [build_id], service_account, **kwargs)
def get_build(self, build_id, service_account=None, **kwargs):
return self._call_service('get', [build_id], service_account, **kwargs)
def _call_service(self, command, args, service_account=None, **kwargs):
# TODO: Deploy buildbucket client using cipd.
self._configure_defaults()
step_name = kwargs.pop('name', 'buildbucket.' + command)
if service_account:
args = ['--service-account-json', service_account] + args
args = [str(self.c.buildbucket_client_path), command, '--host',
self.c.buildbucket_host] + args
return self.m.step(
step_name, args, stdout=self.m.json.output(), **kwargs)