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

Unified Diff: appengine/cr-buildbucket/swarming/swarmingcfg.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 side-by-side diff with in-line comments
Download patch
Index: appengine/cr-buildbucket/swarming/swarmingcfg.py
diff --git a/appengine/cr-buildbucket/swarming/swarmingcfg.py b/appengine/cr-buildbucket/swarming/swarmingcfg.py
index 324151cea03a0e6dbb9b5551453daf6ff99283cd..7e7cd68b8538a6a499fab22652c6ccf97f56e2c3 100644
--- a/appengine/cr-buildbucket/swarming/swarmingcfg.py
+++ b/appengine/cr-buildbucket/swarming/swarmingcfg.py
@@ -2,13 +2,97 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import copy
import json
import re
+from components.config import validation
+
from proto import project_config_pb2
DIMENSION_KEY_RGX = re.compile(r'^[a-zA-Z\_\-]+$')
+NO_PROPERTY = object()
+
+
+def read_properties(recipe):
+ """Parses build properties from the recipe message.
+
+ Expects the message to be valid.
+
+ Uses NO_PROPERTY for empty values.
+ """
+ result = dict(p.split(':', 1) for p in recipe.properties)
+ for p in recipe.properties_j:
+ k, v = p.split(':', 1)
+ if not v:
+ parsed = NO_PROPERTY
+ else:
+ parsed = json.loads(v)
+ result[k] = parsed
+ return result
+
+
+def merge_recipe(r1, r2):
+ """Merges Recipe message r2 into r1.
+
+ Expects messages to be valid.
+
+ All properties are converted to properties_j.
+ """
+ props = read_properties(r1)
+ props.update(read_properties(r2))
+
+ r1.MergeFrom(r2)
+ r1.properties[:] = []
+ r1.properties_j[:] = [
+ '%s:%s' % (k, json.dumps(v))
+ for k, v in sorted(props.iteritems())
+ if v is not NO_PROPERTY
+ ]
+
+
+def merge_dimensions(d1, d2):
+ """Merges dimensions. Values in d2 overwrite values in d1.
+
+ Expects dimensions to be valid.
+
+ If a dimensions value in d2 is empty, it is excluded from the result.
+ """
+ parse = lambda d: dict(a.split(':', 1) for a in d)
+ dims = parse(d1)
+ dims.update(parse(d2))
+ return ['%s:%s' % (k, v) for k, v in sorted(dims.iteritems()) if v]
+
+
+def merge_builder(b1, b2):
+ """Merges Builder message b2 into b1. Expects messages to be valid."""
+ dims = merge_dimensions(b1.dimensions, b2.dimensions)
+ recipe = None
+ if b1.HasField('recipe') or b2.HasField('recipe'): # pragma: no branch
+ recipe = copy.deepcopy(b1.recipe)
+ merge_recipe(recipe, b2.recipe)
+
+ b1.MergeFrom(b2)
+ b1.dimensions[:] = dims
+ if recipe: # pragma: no branch
+ b1.recipe.CopyFrom(recipe)
+
+
+# TODO(nodir): remove this function once all confgis are converted
+def normalize_swarming_cfg(cfg):
+ """Converts deprecated fields into new ones.
+
+ Does not check for presence of both new and deprecated fields, because
+ configs will be migrated shortly after this change is deployed.
+ """
+ if cfg.HasField('builder_defaults'): # pragma: no cover
+ return
+ defs = cfg.builder_defaults
+ defs.swarming_tags[:] = cfg.common_swarming_tags
+ defs.dimensions[:] = cfg.common_dimensions
+ defs.recipe.CopyFrom(cfg.common_recipe)
+ defs.execution_timeout_secs = cfg.common_execution_timeout_secs
def validate_tag(tag, ctx):
@@ -44,11 +128,14 @@ def validate_dimensions(field_name, dimensions, ctx):
known_keys.add(key)
-def validate_recipe_cfg(recipe, common_recipe, ctx):
- common_recipe = common_recipe or project_config_pb2.Swarming.Recipe()
- if not (recipe.name or common_recipe.name):
+def validate_recipe_cfg(recipe, ctx, final=True):
+ """Validates a Recipe message.
+
+ If final is False, does not validate for completeness.
+ """
+ if final and not recipe.name:
ctx.error('name unspecified')
- if not (recipe.repository or common_recipe.repository):
+ if final and not recipe.repository:
ctx.error('repository unspecified')
validate_recipe_properties(recipe.properties, recipe.properties_j, ctx)
@@ -83,15 +170,19 @@ def validate_recipe_properties(properties, properties_j, ctx):
key, value = p.split(':', 1)
validate_key(key)
keys.add(key)
- try:
- json.loads(value)
- except ValueError as ex:
- ctx.error(ex)
+ if value:
+ try:
+ json.loads(value)
+ except ValueError as ex:
+ ctx.error(ex)
-def validate_builder_cfg(
- builder, ctx, bucket_has_pool_dim=False, common_recipe=None):
- if not builder.name:
+def validate_builder_cfg(builder, ctx, final=True):
+ """Validates a Builder message.
+
+ If final is False, does not validate for completeness.
+ """
+ if final and not builder.name:
ctx.error('name unspecified')
for i, t in enumerate(builder.swarming_tags):
@@ -99,47 +190,48 @@ def validate_builder_cfg(
validate_tag(t, ctx)
validate_dimensions('dimension', builder.dimensions, ctx)
- if not bucket_has_pool_dim and not has_pool_dimension(builder.dimensions):
- ctx.error(
- 'has no "pool" dimension. '
- 'Either define it in the builder or in "common_dimensions"')
+ if final and not has_pool_dimension(builder.dimensions):
+ ctx.error('has no "pool" dimension')
- if not builder.HasField('recipe') and not common_recipe:
- ctx.error('recipe unspecified')
- else:
- with ctx.prefix('recipe: '):
- validate_recipe_cfg(builder.recipe, common_recipe, ctx)
+ with ctx.prefix('recipe: '):
+ validate_recipe_cfg(builder.recipe, ctx, final=final)
if builder.priority > 200:
ctx.error('priority must be in [0, 200] range; got %d', builder.priority)
def validate_cfg(swarming, ctx):
- if not swarming.hostname:
- ctx.error('hostname unspecified')
- for i, t in enumerate(swarming.common_swarming_tags):
- with ctx.prefix('common tag #%d: ', i + 1):
- validate_tag(t, ctx)
+ swarming = copy.deepcopy(swarming)
+ normalize_swarming_cfg(swarming)
- validate_dimensions('common dimension', swarming.common_dimensions, ctx)
- has_pool_dim = has_pool_dimension(swarming.common_dimensions)
+ def make_subctx():
+ return validation.Context(
+ on_message=lambda msg: ctx.msg(msg.severity, '%s', msg.text))
+ if not swarming.hostname:
+ ctx.error('hostname unspecified')
if swarming.task_template_canary_percentage > 100:
ctx.error('task_template_canary_percentage must must be in [0, 100]')
- common_recipe = None
- if swarming.HasField('common_recipe'):
- common_recipe = swarming.common_recipe
- with ctx.prefix('common_recipe: '):
- validate_recipe_properties(
- swarming.common_recipe.properties,
- swarming.common_recipe.properties_j, ctx)
+ with ctx.prefix('builder_defaults: '):
+ if swarming.builder_defaults.name:
+ ctx.error('do not specify default name')
+ subctx = make_subctx()
+ validate_builder_cfg(swarming.builder_defaults, subctx, final=False)
+ builder_defaults_has_errors = subctx.result().has_errors
for i, b in enumerate(swarming.builders):
with ctx.prefix('builder %s: ' % (b.name or '#%s' % (i + 1))):
- validate_builder_cfg(
- b, ctx, bucket_has_pool_dim=has_pool_dim,
- common_recipe=common_recipe)
+ # Validate b before merging, otherwise merging will fail.
+ subctx = make_subctx()
+ validate_builder_cfg(b, subctx, final=False)
+ if subctx.result().has_errors or builder_defaults_has_errors:
+ # Do no try to merge invalid configs.
+ continue
+
+ merged = copy.deepcopy(swarming.builder_defaults)
+ merge_builder(merged, b)
+ validate_builder_cfg(merged, ctx)
def has_pool_dimension(dimensions):
« no previous file with comments | « appengine/cr-buildbucket/swarming/swarming.py ('k') | appengine/cr-buildbucket/swarming/test/swarmingcfg_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698