| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 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 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 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 google.appengine.api import app_identity | 42 from google.appengine.api import app_identity |
| 43 from google.appengine.ext import ndb | 43 from google.appengine.ext import ndb |
| 44 import webapp2 | 44 import webapp2 |
| 45 | 45 |
| 46 from proto import project_config_pb2 | 46 from proto import project_config_pb2 |
| 47 from . import swarmingcfg as swarmingcfg_module |
| 47 import config | 48 import config |
| 48 import errors | 49 import errors |
| 49 import model | 50 import model |
| 50 import notifications | 51 import notifications |
| 51 | 52 |
| 52 PUBSUB_TOPIC = 'swarming' | 53 PUBSUB_TOPIC = 'swarming' |
| 53 BUILDER_PARAMETER = 'builder_name' | 54 BUILDER_PARAMETER = 'builder_name' |
| 54 PARAM_PROPERTIES = 'properties' | 55 PARAM_PROPERTIES = 'properties' |
| 55 PARAM_SWARMING = 'swarming' | 56 PARAM_SWARMING = 'swarming' |
| 56 PARAM_CHANGES = 'changes' | 57 PARAM_CHANGES = 'changes' |
| (...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 180 email = author.get('email') | 181 email = author.get('email') |
| 181 if not isinstance(email, basestring): | 182 if not isinstance(email, basestring): |
| 182 bad('change author email must be a string') | 183 bad('change author email must be a string') |
| 183 if not email: | 184 if not email: |
| 184 bad('change author email not specified') | 185 bad('change author email not specified') |
| 185 | 186 |
| 186 if params: | 187 if params: |
| 187 bad('unrecognized params: %r', params.keys()) | 188 bad('unrecognized params: %r', params.keys()) |
| 188 | 189 |
| 189 | 190 |
| 190 def read_properties(recipe): | |
| 191 """Parses build properties from the recipe message.""" | |
| 192 result = dict(p.split(':', 1) for p in recipe.properties) | |
| 193 for p in recipe.properties_j: | |
| 194 k, v = p.split(':', 1) | |
| 195 result[k] = json.loads(v) | |
| 196 return result | |
| 197 | |
| 198 | |
| 199 def merge_recipe(r1, r2): | |
| 200 """Merges two Recipe messages. Values in r2 overwrite values in r1.""" | |
| 201 if not r1: # pragma: no branch | |
| 202 return r2 # pragma: no cover | |
| 203 props = read_properties(r1) | |
| 204 props.update(read_properties(r2)) | |
| 205 recipe = project_config_pb2.Swarming.Recipe( | |
| 206 repository=r2.repository or r1.repository, | |
| 207 name=r2.name or r1.name, | |
| 208 ) | |
| 209 return recipe, props | |
| 210 | |
| 211 | |
| 212 def should_use_canary_template(build, percentage): # pragma: no cover | 191 def should_use_canary_template(build, percentage): # pragma: no cover |
| 213 """Returns True if a canary template should be used for the |build|. | 192 """Returns True if a canary template should be used for the |build|. |
| 214 | 193 |
| 215 This function is determinstic. | 194 This function is determinstic. |
| 216 """ | 195 """ |
| 217 if percentage == 0: | 196 if percentage == 0: |
| 218 return False | 197 return False |
| 219 identity = { | 198 identity = { |
| 220 'bucket': build.bucket, | 199 'bucket': build.bucket, |
| 221 'parametres': build.parameters, | 200 'parametres': build.parameters, |
| (...skipping 17 matching lines...) Expand all Loading... |
| 239 validate_build_parameters(builder_cfg.name, params) | 218 validate_build_parameters(builder_cfg.name, params) |
| 240 swarming_param = params.get(PARAM_SWARMING) or {} | 219 swarming_param = params.get(PARAM_SWARMING) or {} |
| 241 | 220 |
| 242 # Use canary template? | 221 # Use canary template? |
| 243 canary = swarming_param.get('canary_template') | 222 canary = swarming_param.get('canary_template') |
| 244 canary_required = bool(canary) | 223 canary_required = bool(canary) |
| 245 if canary is None: | 224 if canary is None: |
| 246 canary = should_use_canary_template( | 225 canary = should_use_canary_template( |
| 247 build, swarming_cfg.task_template_canary_percentage) | 226 build, swarming_cfg.task_template_canary_percentage) |
| 248 | 227 |
| 228 # Normalize/merge configs. |
| 229 swarming_cfg = copy.deepcopy(swarming_cfg) |
| 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 |
| 249 # Render task template. | 236 # Render task template. |
| 250 try: | 237 try: |
| 251 task_template, canary = yield get_task_template_async( | 238 task_template, canary = yield get_task_template_async( |
| 252 canary, canary_required) | 239 canary, canary_required) |
| 253 except CanaryTemplateNotFound as ex: | 240 except CanaryTemplateNotFound as ex: |
| 254 raise errors.InvalidInputError(ex.message) | 241 raise errors.InvalidInputError(ex.message) |
| 255 task_template_params = { | 242 task_template_params = { |
| 256 'bucket': build.bucket, | 243 'bucket': build.bucket, |
| 257 'builder': builder_cfg.name, | 244 'builder': builder_cfg.name, |
| 258 'project': project_id, | 245 'project': project_id, |
| 259 } | 246 } |
| 260 | 247 |
| 261 is_recipe = ( | 248 is_recipe = builder_cfg.HasField('recipe') |
| 262 builder_cfg.HasField('recipe') or swarming_cfg.HasField('common_recipe')) | |
| 263 if is_recipe: # pragma: no branch | 249 if is_recipe: # pragma: no branch |
| 264 recipe, build_properties = merge_recipe( | 250 build_properties = swarmingcfg_module.read_properties(builder_cfg.recipe) |
| 265 swarming_cfg.common_recipe, builder_cfg.recipe) | 251 recipe_revision = swarming_param.get('recipe', {}).get('revision') or '' |
| 266 revision = swarming_param.get('recipe', {}).get('revision') or '' | |
| 267 | 252 |
| 268 build_properties['buildername'] = builder_cfg.name | 253 build_properties['buildername'] = builder_cfg.name |
| 269 | 254 |
| 270 changes = params.get(PARAM_CHANGES) | 255 changes = params.get(PARAM_CHANGES) |
| 271 if changes: # pragma: no branch | 256 if changes: # pragma: no branch |
| 272 # Buildbucket-Buildbot integration passes repo_url of the first change in | 257 # Buildbucket-Buildbot integration passes repo_url of the first change in |
| 273 # build parameter "changes" as "repository" attribute of SourceStamp. | 258 # build parameter "changes" as "repository" attribute of SourceStamp. |
| 274 # https://chromium.googlesource.com/chromium/tools/build/+/2c6023d/scripts
/master/buildbucket/changestore.py#140 | 259 # https://chromium.googlesource.com/chromium/tools/build/+/2c6023d/scripts
/master/buildbucket/changestore.py#140 |
| 275 # Buildbot passes repository of the build source stamp as "repository" | 260 # Buildbot passes repository of the build source stamp as "repository" |
| 276 # build property. Recipes, in partiular bot_update recipe module, rely on | 261 # build property. Recipes, in partiular bot_update recipe module, rely on |
| 277 # "repository" property and it is an almost sane property to support in | 262 # "repository" property and it is an almost sane property to support in |
| 278 # swarmbucket. | 263 # swarmbucket. |
| 279 repo_url = changes[0].get('repo_url') | 264 repo_url = changes[0].get('repo_url') |
| 280 if repo_url: # pragma: no branch | 265 if repo_url: # pragma: no branch |
| 281 build_properties['repository'] = repo_url | 266 build_properties['repository'] = repo_url |
| 282 | 267 |
| 283 # Buildbot-Buildbucket integration converts emails in changes to blamelist | 268 # Buildbot-Buildbucket integration converts emails in changes to blamelist |
| 284 # property. | 269 # property. |
| 285 emails = [c.get('author', {}).get('email') for c in changes] | 270 emails = [c.get('author', {}).get('email') for c in changes] |
| 286 build_properties['blamelist'] = filter(None, emails) | 271 build_properties['blamelist'] = filter(None, emails) |
| 287 | 272 |
| 288 # Properties specified in build parameters must override any values derived | 273 # Properties specified in build parameters must override any values derived |
| 289 # by swarmbucket. | 274 # by swarmbucket. |
| 290 build_properties.update(build.parameters.get(PARAM_PROPERTIES) or {}) | 275 build_properties.update(build.parameters.get(PARAM_PROPERTIES) or {}) |
| 291 | 276 |
| 292 task_template_params.update({ | 277 task_template_params.update({ |
| 293 'repository': recipe.repository, | 278 'repository': builder_cfg.recipe.repository, |
| 294 'revision': revision, | 279 'revision': recipe_revision, |
| 295 'recipe': recipe.name, | 280 'recipe': builder_cfg.recipe.name, |
| 296 'properties_json': json.dumps(build_properties, sort_keys=True), | 281 'properties_json': json.dumps(build_properties, sort_keys=True), |
| 297 }) | 282 }) |
| 298 | 283 |
| 299 task_template_params = { | 284 task_template_params = { |
| 300 k: v or '' for k, v in task_template_params.iteritems()} | 285 k: v or '' for k, v in task_template_params.iteritems()} |
| 301 task = format_obj(task_template, task_template_params) | 286 task = format_obj(task_template, task_template_params) |
| 302 | 287 |
| 303 if builder_cfg.priority > 0: # pragma: no branch | 288 if builder_cfg.priority > 0: # pragma: no branch |
| 304 task['priority'] = builder_cfg.priority | 289 task['priority'] = builder_cfg.priority |
| 305 | 290 |
| 306 build_tags = dict(t.split(':', 1) for t in build.tags) | 291 build_tags = dict(t.split(':', 1) for t in build.tags) |
| 307 build_tags['builder'] = builder_cfg.name | 292 build_tags['builder'] = builder_cfg.name |
| 308 build.tags = sorted('%s:%s' % (k, v) for k, v in build_tags.iteritems()) | 293 build.tags = sorted('%s:%s' % (k, v) for k, v in build_tags.iteritems()) |
| 309 | 294 |
| 310 swarming_tags = task.setdefault('tags', []) | 295 swarming_tags = task.setdefault('tags', []) |
| 311 _extend_unique(swarming_tags, [ | 296 _extend_unique(swarming_tags, [ |
| 312 'buildbucket_hostname:%s' % app_identity.get_default_version_hostname(), | 297 'buildbucket_hostname:%s' % app_identity.get_default_version_hostname(), |
| 313 'buildbucket_bucket:%s' % build.bucket, | 298 'buildbucket_bucket:%s' % build.bucket, |
| 314 'buildbucket_template_canary:%s' % str(canary).lower(), | 299 'buildbucket_template_canary:%s' % str(canary).lower(), |
| 315 ]) | 300 ]) |
| 316 if is_recipe: # pragma: no branch | 301 if is_recipe: # pragma: no branch |
| 317 _extend_unique(swarming_tags, [ | 302 _extend_unique(swarming_tags, [ |
| 318 'recipe_repository:%s' % recipe.repository, | 303 'recipe_repository:%s' % builder_cfg.recipe.repository, |
| 319 'recipe_revision:%s' % revision, | 304 'recipe_revision:%s' % recipe_revision, |
| 320 'recipe_name:%s' % recipe.name, | 305 'recipe_name:%s' % builder_cfg.recipe.name, |
| 321 ]) | 306 ]) |
| 322 _extend_unique(swarming_tags, swarming_cfg.common_swarming_tags) | |
| 323 _extend_unique(swarming_tags, builder_cfg.swarming_tags) | 307 _extend_unique(swarming_tags, builder_cfg.swarming_tags) |
| 324 _extend_unique(swarming_tags, build.tags) | 308 _extend_unique(swarming_tags, build.tags) |
| 325 swarming_tags.sort() | 309 swarming_tags.sort() |
| 326 | 310 |
| 327 task_properties = task.setdefault('properties', {}) | 311 task_properties = task.setdefault('properties', {}) |
| 328 task_properties['dimensions'] = _prepare_dimensions( | 312 task_properties['dimensions'] = _to_swarming_dimensions( |
| 329 task_properties.get('dimensions', []), | 313 swarmingcfg_module.merge_dimensions( |
| 330 swarming_cfg.common_dimensions, | 314 builder_cfg.dimensions, |
| 331 builder_cfg.dimensions | 315 task_properties.get('dimensions', []), |
| 332 ) | 316 )) |
| 333 | 317 |
| 334 if swarming_cfg.common_execution_timeout_secs > 0: | |
| 335 task_properties['execution_timeout_secs'] = ( | |
| 336 swarming_cfg.common_execution_timeout_secs) | |
| 337 if builder_cfg.execution_timeout_secs > 0: | 318 if builder_cfg.execution_timeout_secs > 0: |
| 338 task_properties['execution_timeout_secs'] = ( | 319 task_properties['execution_timeout_secs'] = ( |
| 339 builder_cfg.execution_timeout_secs) | 320 builder_cfg.execution_timeout_secs) |
| 340 | 321 |
| 341 task['pubsub_topic'] = ( | 322 task['pubsub_topic'] = ( |
| 342 'projects/%s/topics/%s' % | 323 'projects/%s/topics/%s' % |
| 343 (app_identity.get_application_id(), PUBSUB_TOPIC)) | 324 (app_identity.get_application_id(), PUBSUB_TOPIC)) |
| 344 task['pubsub_auth_token'] = TaskToken.generate() | 325 task['pubsub_auth_token'] = TaskToken.generate() |
| 345 task['pubsub_userdata'] = json.dumps({ | 326 task['pubsub_userdata'] = json.dumps({ |
| 346 'created_ts': utils.datetime_to_timestamp(utils.utcnow()), | 327 'created_ts': utils.datetime_to_timestamp(utils.utcnow()), |
| 347 'swarming_hostname': swarming_cfg.hostname, | 328 'swarming_hostname': swarming_cfg.hostname, |
| 348 }, sort_keys=True) | 329 }, sort_keys=True) |
| 349 raise ndb.Return(task) | 330 raise ndb.Return(task) |
| 350 | 331 |
| 351 | 332 |
| 352 def _prepare_dimensions(global_dims, bucket_cfg_dims, builder_cfg_dims): | 333 def _to_swarming_dimensions(dims): |
| 353 """Computes final task dimensions. | 334 """Converts dimensions from buildbucket format to swarming format.""" |
| 354 | 335 return [ |
| 355 Configs must have valid format. | 336 {'key': key, 'value': value} |
| 356 """ | 337 for key, value in |
| 357 # Make them all lists of pairs. | 338 (s.split(':', 1) for s in dims) |
| 358 global_dims = [(d['key'], d['value']) for d in global_dims] | 339 ] |
| 359 parse_cfg_dim = lambda d: d.split(':', 1) | |
| 360 bucket_dims = map(parse_cfg_dim, bucket_cfg_dims) | |
| 361 builder_dims = map(parse_cfg_dim, builder_cfg_dims) | |
| 362 | |
| 363 # Note: dimensions must be unique. | |
| 364 # Overwrite global dimensions by bucket-level dimensions, | |
| 365 # then by builder-level dimensions. | |
| 366 merged = {} | |
| 367 for ds in (global_dims, bucket_dims, builder_dims): | |
| 368 merged.update(ds) | |
| 369 return sorted({'key': k, 'value': v} for k, v in merged.iteritems() if v) | |
| 370 | 340 |
| 371 | 341 |
| 372 @ndb.tasklet | 342 @ndb.tasklet |
| 373 def create_task_async(build): | 343 def create_task_async(build): |
| 374 """Creates a swarming task for the build and mutates the build. | 344 """Creates a swarming task for the build and mutates the build. |
| 375 | 345 |
| 376 May be called only if is_for_swarming(build) == True. | 346 May be called only if is_for_swarming(build) == True. |
| 377 """ | 347 """ |
| 378 if build.lease_key: | 348 if build.lease_key: |
| 379 raise errors.InvalidInputError( | 349 raise errors.InvalidInputError( |
| (...skipping 369 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 749 def _extend_unique(target, items): | 719 def _extend_unique(target, items): |
| 750 for x in items: | 720 for x in items: |
| 751 if x not in target: # pragma: no branch | 721 if x not in target: # pragma: no branch |
| 752 target.append(x) | 722 target.append(x) |
| 753 | 723 |
| 754 | 724 |
| 755 class TaskToken(tokens.TokenKind): | 725 class TaskToken(tokens.TokenKind): |
| 756 expiration_sec = 60 * 60 * 24 # 24 hours. | 726 expiration_sec = 60 * 60 * 24 # 24 hours. |
| 757 secret_key = auth.SecretKey('swarming_task_token', scope='local') | 727 secret_key = auth.SecretKey('swarming_task_token', scope='local') |
| 758 version = 1 | 728 version = 1 |
| OLD | NEW |