| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """This module integrates buildbucket with swarming. | 5 """This module integrates buildbucket with swarming. |
| 6 | 6 |
| 7 A bucket config may have "swarming" field that specifies how a builder | 7 A bucket config may have "swarming" field that specifies how a builder |
| 8 is mapped to a recipe. If build is scheduled for a bucket/builder | 8 is mapped to a recipe. If build is scheduled for a bucket/builder |
| 9 with swarming configuration, the integration overrides the default behavior. | 9 with swarming configuration, the integration overrides the default behavior. |
| 10 | 10 |
| 11 Prior adding Build to the datastore, a swarming task is created. The definition | 11 Prior adding Build to the datastore, a swarming task is created. The definition |
| (...skipping 20 matching lines...) Expand all Loading... |
| 32 import json | 32 import json |
| 33 import logging | 33 import logging |
| 34 import string | 34 import string |
| 35 | 35 |
| 36 from components import auth | 36 from components import auth |
| 37 from components import config as component_config | 37 from components import config as component_config |
| 38 from components import decorators | 38 from components import decorators |
| 39 from components import net | 39 from components import net |
| 40 from components import utils | 40 from components import utils |
| 41 from components.auth import tokens | 41 from components.auth import tokens |
| 42 from components.config import validation |
| 42 from google.appengine.api import app_identity | 43 from google.appengine.api import app_identity |
| 43 from google.appengine.ext import ndb | 44 from google.appengine.ext import ndb |
| 44 import webapp2 | 45 import webapp2 |
| 45 | 46 |
| 46 from proto import project_config_pb2 | 47 from proto import project_config_pb2 |
| 47 from . import swarmingcfg as swarmingcfg_module | 48 from . import swarmingcfg as swarmingcfg_module |
| 48 import config | 49 import config |
| 49 import errors | 50 import errors |
| 50 import model | 51 import model |
| 51 import notifications | 52 import notifications |
| 53 import protoutil |
| 54 |
| 52 | 55 |
| 53 PUBSUB_TOPIC = 'swarming' | 56 PUBSUB_TOPIC = 'swarming' |
| 54 BUILDER_PARAMETER = 'builder_name' | 57 BUILDER_PARAMETER = 'builder_name' |
| 55 PARAM_PROPERTIES = 'properties' | 58 PARAM_PROPERTIES = 'properties' |
| 56 PARAM_SWARMING = 'swarming' | 59 PARAM_SWARMING = 'swarming' |
| 57 PARAM_CHANGES = 'changes' | 60 PARAM_CHANGES = 'changes' |
| 58 DEFAULT_URL_FORMAT = 'https://{swarming_hostname}/user/task/{task_id}' | 61 DEFAULT_URL_FORMAT = 'https://{swarming_hostname}/user/task/{task_id}' |
| 59 | 62 |
| 60 | 63 |
| 61 ################################################################################ | 64 ################################################################################ |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 130 | 133 |
| 131 def validate_build_parameters(builder_name, params): | 134 def validate_build_parameters(builder_name, params): |
| 132 """Raises errors.InvalidInputError if build parameters are invalid.""" | 135 """Raises errors.InvalidInputError if build parameters are invalid.""" |
| 133 params = copy.deepcopy(params) | 136 params = copy.deepcopy(params) |
| 134 | 137 |
| 135 def bad(fmt, *args): | 138 def bad(fmt, *args): |
| 136 raise errors.InvalidInputError(fmt % args) | 139 raise errors.InvalidInputError(fmt % args) |
| 137 | 140 |
| 138 params.pop(BUILDER_PARAMETER) # already validated | 141 params.pop(BUILDER_PARAMETER) # already validated |
| 139 | 142 |
| 143 |
| 144 def assert_object(name, value): |
| 145 if not isinstance(value, dict): |
| 146 bad('%s parameter must be an object' % name) |
| 147 |
| 140 properties = params.pop(PARAM_PROPERTIES, None) | 148 properties = params.pop(PARAM_PROPERTIES, None) |
| 141 if properties is not None: | 149 if properties is not None: |
| 142 if not isinstance(properties, dict): | 150 assert_object('properties', properties) |
| 143 bad('properties parameter must be an object') | |
| 144 if properties.get('buildername', builder_name) != builder_name: | 151 if properties.get('buildername', builder_name) != builder_name: |
| 145 bad('inconsistent builder name') | 152 bad('inconsistent builder name') |
| 146 | 153 |
| 147 swarming = params.pop(PARAM_SWARMING, None) | 154 swarming = params.pop(PARAM_SWARMING, None) |
| 148 if swarming is not None: | 155 if swarming is not None: |
| 149 if not isinstance(swarming, dict): | 156 assert_object('swarming', swarming) |
| 150 bad('swarming parameter must be an object') | |
| 151 swarming = copy.deepcopy(swarming) | 157 swarming = copy.deepcopy(swarming) |
| 152 if 'recipe' in swarming: | 158 if 'recipe' in swarming: |
| 159 logging.error( |
| 160 'someone is still using deprecated swarming.recipe parameter') |
| 153 recipe = swarming.pop('recipe') | 161 recipe = swarming.pop('recipe') |
| 154 if not isinstance(recipe, dict): | 162 assert_object('swarming.recipe', recipe) |
| 155 bad('swarming.recipe parameter must be an object') | |
| 156 if 'revision' in recipe: | 163 if 'revision' in recipe: |
| 157 revision = recipe.pop('revision') | 164 revision = recipe.pop('revision') |
| 158 if not isinstance(revision, basestring): | 165 if not isinstance(revision, basestring): |
| 159 bad('swarming.recipe.revision parameter must be a string') | 166 bad('swarming.recipe.revision parameter must be a string') |
| 160 if recipe: | 167 if recipe: |
| 161 bad('unrecognized keys in swarming.recipe: %r', recipe) | 168 bad('unrecognized keys in swarming.recipe: %r', recipe) |
| 162 canary_template = swarming.pop('canary_template', None) | 169 canary_template = swarming.pop('canary_template', None) |
| 163 if canary_template not in (True, False, None): | 170 if canary_template not in (True, False, None): |
| 164 bad('swarming.canary_template parameter must true, false or null') | 171 bad('swarming.canary_template parameter must true, false or null') |
| 172 |
| 173 override_builder_cfg_data = swarming.pop('override_builder_cfg', None) |
| 174 if override_builder_cfg_data is not None: |
| 175 assert_object('swarming.override_builder_cfg', override_builder_cfg_data) |
| 176 override_builder_cfg = project_config_pb2.Swarming.Builder() |
| 177 try: |
| 178 protoutil.merge_dict( |
| 179 override_builder_cfg_data, override_builder_cfg) |
| 180 except TypeError as ex: |
| 181 bad('swarming.override_builder_cfg parameter: %s', ex) |
| 182 if override_builder_cfg.name: |
| 183 bad('swarming.override_builder_cfg cannot override builder name') |
| 184 ctx = validation.Context.raise_on_error( |
| 185 exc_type=errors.InvalidInputError, |
| 186 prefix='swarming.override_builder_cfg parameter: ') |
| 187 swarmingcfg_module.validate_builder_cfg( |
| 188 override_builder_cfg, ctx, final=False) |
| 189 |
| 165 if swarming: | 190 if swarming: |
| 166 bad('unrecognized keys in swarming param: %r', swarming.keys()) | 191 bad('unrecognized keys in swarming param: %r', swarming.keys()) |
| 167 | 192 |
| 168 changes = params.pop(PARAM_CHANGES, None) | 193 changes = params.pop(PARAM_CHANGES, None) |
| 169 if changes is not None: | 194 if changes is not None: |
| 170 if not isinstance(changes, list): | 195 if not isinstance(changes, list): |
| 171 bad('changes param must be an array') | 196 bad('changes param must be an array') |
| 172 for c in changes: # pragma: no branch | 197 for c in changes: # pragma: no branch |
| 173 if not isinstance(c, dict): | 198 if not isinstance(c, dict): |
| 174 bad('changes param must contain only objects') | 199 bad('changes param must contain only objects') |
| (...skipping 21 matching lines...) Expand all Loading... |
| 196 if percentage == 0: | 221 if percentage == 0: |
| 197 return False | 222 return False |
| 198 identity = { | 223 identity = { |
| 199 'bucket': build.bucket, | 224 'bucket': build.bucket, |
| 200 'parametres': build.parameters, | 225 'parametres': build.parameters, |
| 201 } | 226 } |
| 202 digest = hashlib.sha1(json.dumps(identity, sort_keys=True)).digest() | 227 digest = hashlib.sha1(json.dumps(identity, sort_keys=True)).digest() |
| 203 return digest[0] < 256 * percentage / 100 | 228 return digest[0] < 256 * percentage / 100 |
| 204 | 229 |
| 205 | 230 |
| 231 def _prepare_builder_config(swarming_cfg, builder_cfg, swarming_param): |
| 232 """Returns final version of builder config to use for |build|. |
| 233 |
| 234 Expects arguments to be valid. |
| 235 """ |
| 236 swarming_cfg = copy.deepcopy(swarming_cfg) |
| 237 swarmingcfg_module.normalize_swarming_cfg(swarming_cfg) |
| 238 |
| 239 # Apply defaults. |
| 240 result = copy.deepcopy(swarming_cfg.builder_defaults) |
| 241 swarmingcfg_module.merge_builder(result, builder_cfg) |
| 242 |
| 243 # Apply overrides in the swarming parameter. |
| 244 override_builder_cfg_data = swarming_param.get('override_builder_cfg', {}) |
| 245 if override_builder_cfg_data: |
| 246 override_builder_cfg = project_config_pb2.Swarming.Builder() |
| 247 protoutil.merge_dict(override_builder_cfg_data, result) |
| 248 ctx = validation.Context.raise_on_error( |
| 249 exc_type=errors.InvalidInputError, |
| 250 prefix='swarming.override_buider_cfg parameter: ') |
| 251 swarmingcfg_module.merge_builder(result, override_builder_cfg) |
| 252 swarmingcfg_module.validate_builder_cfg(result, ctx) |
| 253 return result |
| 254 |
| 255 |
| 206 @ndb.tasklet | 256 @ndb.tasklet |
| 207 def create_task_def_async(project_id, swarming_cfg, builder_cfg, build): | 257 def create_task_def_async(project_id, swarming_cfg, builder_cfg, build): |
| 208 """Creates a swarming task definition for the |build|. | 258 """Creates a swarming task definition for the |build|. |
| 209 | 259 |
| 210 Supports build properties that are supported by Buildbot-Buildbucket | 260 Supports build properties that are supported by Buildbot-Buildbucket |
| 211 integration. See | 261 integration. See |
| 212 https://chromium.googlesource.com/chromium/tools/build/+/eff4ceb/scripts/maste
r/buildbucket/README.md#Build-parameters | 262 https://chromium.googlesource.com/chromium/tools/build/+/eff4ceb/scripts/maste
r/buildbucket/README.md#Build-parameters |
| 213 | 263 |
| 214 Raises: | 264 Raises: |
| 215 errors.InvalidInputError if build.parameters are invalid. | 265 errors.InvalidInputError if build.parameters are invalid. |
| 216 """ | 266 """ |
| 217 params = build.parameters or {} | 267 params = build.parameters or {} |
| 218 validate_build_parameters(builder_cfg.name, params) | 268 validate_build_parameters(builder_cfg.name, params) |
| 219 swarming_param = params.get(PARAM_SWARMING) or {} | 269 swarming_param = params.get(PARAM_SWARMING) or {} |
| 220 | 270 |
| 221 # Use canary template? | 271 # Use canary template? |
| 222 canary = swarming_param.get('canary_template') | 272 canary = swarming_param.get('canary_template') |
| 223 canary_required = bool(canary) | 273 canary_required = bool(canary) |
| 224 if canary is None: | 274 if canary is None: |
| 225 canary = should_use_canary_template( | 275 canary = should_use_canary_template( |
| 226 build, swarming_cfg.task_template_canary_percentage) | 276 build, swarming_cfg.task_template_canary_percentage) |
| 227 | 277 |
| 228 # Normalize/merge configs. | 278 builder_cfg = _prepare_builder_config( |
| 229 swarming_cfg = copy.deepcopy(swarming_cfg) | 279 swarming_cfg, builder_cfg, swarming_param) |
| 230 swarmingcfg_module.normalize_swarming_cfg(swarming_cfg) | |
| 231 | |
| 232 merged = copy.deepcopy(swarming_cfg.builder_defaults) | |
| 233 swarmingcfg_module.merge_builder(merged, builder_cfg) | |
| 234 builder_cfg = merged | |
| 235 | 280 |
| 236 # Render task template. | 281 # Render task template. |
| 237 try: | 282 try: |
| 238 task_template, canary = yield get_task_template_async( | 283 task_template, canary = yield get_task_template_async( |
| 239 canary, canary_required) | 284 canary, canary_required) |
| 240 except CanaryTemplateNotFound as ex: | 285 except CanaryTemplateNotFound as ex: |
| 241 raise errors.InvalidInputError(ex.message) | 286 raise errors.InvalidInputError(ex.message) |
| 242 task_template_params = { | 287 task_template_params = { |
| 243 'bucket': build.bucket, | 288 'bucket': build.bucket, |
| 244 'builder': builder_cfg.name, | 289 'builder': builder_cfg.name, |
| (...skipping 474 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 719 def _extend_unique(target, items): | 764 def _extend_unique(target, items): |
| 720 for x in items: | 765 for x in items: |
| 721 if x not in target: # pragma: no branch | 766 if x not in target: # pragma: no branch |
| 722 target.append(x) | 767 target.append(x) |
| 723 | 768 |
| 724 | 769 |
| 725 class TaskToken(tokens.TokenKind): | 770 class TaskToken(tokens.TokenKind): |
| 726 expiration_sec = 60 * 60 * 24 # 24 hours. | 771 expiration_sec = 60 * 60 * 24 # 24 hours. |
| 727 secret_key = auth.SecretKey('swarming_task_token', scope='local') | 772 secret_key = auth.SecretKey('swarming_task_token', scope='local') |
| 728 version = 1 | 773 version = 1 |
| OLD | NEW |