Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(537)

Side by Side Diff: appengine/cr-buildbucket/swarming/swarming.py

Issue 2209583003: swarmbucket: refactor builder defaults (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: deepcopy and subctx Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
OLDNEW
« no previous file with comments | « appengine/cr-buildbucket/proto/project_config_pb2.py ('k') | appengine/cr-buildbucket/swarming/swarmingcfg.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698