| # 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) |