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 = swarmingcfg_module.copy_msg(swarming_cfg) |
| 230 swarmingcfg_module.normalize_swarming_cfg(swarming_cfg) |
| 231 |
| 232 merged = swarmingcfg_module.copy_msg(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 |