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

Side by Side 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 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 import copy
5 import json 6 import json
6 import re 7 import re
7 8
9 from components.config import validation
10
8 from proto import project_config_pb2 11 from proto import project_config_pb2
9 12
10 13
11 DIMENSION_KEY_RGX = re.compile(r'^[a-zA-Z\_\-]+$') 14 DIMENSION_KEY_RGX = re.compile(r'^[a-zA-Z\_\-]+$')
15 NO_PROPERTY = object()
16
17
18 def read_properties(recipe):
19 """Parses build properties from the recipe message.
20
21 Expects the message to be valid.
22
23 Uses NO_PROPERTY for empty values.
24 """
25 result = dict(p.split(':', 1) for p in recipe.properties)
26 for p in recipe.properties_j:
27 k, v = p.split(':', 1)
28 if not v:
29 parsed = NO_PROPERTY
30 else:
31 parsed = json.loads(v)
32 result[k] = parsed
33 return result
34
35
36 def merge_recipe(r1, r2):
37 """Merges Recipe message r2 into r1.
38
39 Expects messages to be valid.
40
41 All properties are converted to properties_j.
42 """
43 props = read_properties(r1)
44 props.update(read_properties(r2))
45
46 r1.MergeFrom(r2)
47 r1.properties[:] = []
48 r1.properties_j[:] = [
49 '%s:%s' % (k, json.dumps(v))
50 for k, v in sorted(props.iteritems())
51 if v is not NO_PROPERTY
52 ]
53
54
55 def merge_dimensions(d1, d2):
56 """Merges dimensions. Values in d2 overwrite values in d1.
57
58 Expects dimensions to be valid.
59
60 If a dimensions value in d2 is empty, it is excluded from the result.
61 """
62 parse = lambda d: dict(a.split(':', 1) for a in d)
63 dims = parse(d1)
64 dims.update(parse(d2))
65 return ['%s:%s' % (k, v) for k, v in sorted(dims.iteritems()) if v]
66
67
68 def merge_builder(b1, b2):
69 """Merges Builder message b2 into b1. Expects messages to be valid."""
70 dims = merge_dimensions(b1.dimensions, b2.dimensions)
71 recipe = None
72 if b1.HasField('recipe') or b2.HasField('recipe'): # pragma: no branch
73 recipe = copy.deepcopy(b1.recipe)
74 merge_recipe(recipe, b2.recipe)
75
76 b1.MergeFrom(b2)
77 b1.dimensions[:] = dims
78 if recipe: # pragma: no branch
79 b1.recipe.CopyFrom(recipe)
80
81
82 # TODO(nodir): remove this function once all confgis are converted
83 def normalize_swarming_cfg(cfg):
84 """Converts deprecated fields into new ones.
85
86 Does not check for presence of both new and deprecated fields, because
87 configs will be migrated shortly after this change is deployed.
88 """
89 if cfg.HasField('builder_defaults'): # pragma: no cover
90 return
91 defs = cfg.builder_defaults
92 defs.swarming_tags[:] = cfg.common_swarming_tags
93 defs.dimensions[:] = cfg.common_dimensions
94 defs.recipe.CopyFrom(cfg.common_recipe)
95 defs.execution_timeout_secs = cfg.common_execution_timeout_secs
12 96
13 97
14 def validate_tag(tag, ctx): 98 def validate_tag(tag, ctx):
15 # a valid swarming tag is a string that contains ":" 99 # a valid swarming tag is a string that contains ":"
16 if ':' not in tag: 100 if ':' not in tag:
17 ctx.error('does not have ":": %s', tag) 101 ctx.error('does not have ":": %s', tag)
18 name = tag.split(':', 1)[0] 102 name = tag.split(':', 1)[0]
19 if name.lower() == 'builder': 103 if name.lower() == 'builder':
20 ctx.error( 104 ctx.error(
21 'do not specify builder tag; ' 105 'do not specify builder tag; '
(...skipping 15 matching lines...) Expand all
37 if not DIMENSION_KEY_RGX.match(key): 121 if not DIMENSION_KEY_RGX.match(key):
38 ctx.error( 122 ctx.error(
39 'key "%s" does not match pattern "%s"', 123 'key "%s" does not match pattern "%s"',
40 key, DIMENSION_KEY_RGX.pattern) 124 key, DIMENSION_KEY_RGX.pattern)
41 if key in known_keys: 125 if key in known_keys:
42 ctx.error('duplicate key %s', key) 126 ctx.error('duplicate key %s', key)
43 else: 127 else:
44 known_keys.add(key) 128 known_keys.add(key)
45 129
46 130
47 def validate_recipe_cfg(recipe, common_recipe, ctx): 131 def validate_recipe_cfg(recipe, ctx, final=True):
48 common_recipe = common_recipe or project_config_pb2.Swarming.Recipe() 132 """Validates a Recipe message.
49 if not (recipe.name or common_recipe.name): 133
134 If final is False, does not validate for completeness.
135 """
136 if final and not recipe.name:
50 ctx.error('name unspecified') 137 ctx.error('name unspecified')
51 if not (recipe.repository or common_recipe.repository): 138 if final and not recipe.repository:
52 ctx.error('repository unspecified') 139 ctx.error('repository unspecified')
53 validate_recipe_properties(recipe.properties, recipe.properties_j, ctx) 140 validate_recipe_properties(recipe.properties, recipe.properties_j, ctx)
54 141
55 142
56 def validate_recipe_properties(properties, properties_j, ctx): 143 def validate_recipe_properties(properties, properties_j, ctx):
57 keys = set() 144 keys = set()
58 145
59 def validate_key(key): 146 def validate_key(key):
60 if not key: 147 if not key:
61 ctx.error('key not specified') 148 ctx.error('key not specified')
(...skipping 14 matching lines...) Expand all
76 keys.add(key) 163 keys.add(key)
77 164
78 for i, p in enumerate(properties_j): 165 for i, p in enumerate(properties_j):
79 with ctx.prefix('properties_j #%d: ', i + 1): 166 with ctx.prefix('properties_j #%d: ', i + 1):
80 if ':' not in p: 167 if ':' not in p:
81 ctx.error('does not have colon') 168 ctx.error('does not have colon')
82 else: 169 else:
83 key, value = p.split(':', 1) 170 key, value = p.split(':', 1)
84 validate_key(key) 171 validate_key(key)
85 keys.add(key) 172 keys.add(key)
86 try: 173 if value:
87 json.loads(value) 174 try:
88 except ValueError as ex: 175 json.loads(value)
89 ctx.error(ex) 176 except ValueError as ex:
177 ctx.error(ex)
90 178
91 179
92 def validate_builder_cfg( 180 def validate_builder_cfg(builder, ctx, final=True):
93 builder, ctx, bucket_has_pool_dim=False, common_recipe=None): 181 """Validates a Builder message.
94 if not builder.name: 182
183 If final is False, does not validate for completeness.
184 """
185 if final and not builder.name:
95 ctx.error('name unspecified') 186 ctx.error('name unspecified')
96 187
97 for i, t in enumerate(builder.swarming_tags): 188 for i, t in enumerate(builder.swarming_tags):
98 with ctx.prefix('tag #%d: ', i + 1): 189 with ctx.prefix('tag #%d: ', i + 1):
99 validate_tag(t, ctx) 190 validate_tag(t, ctx)
100 191
101 validate_dimensions('dimension', builder.dimensions, ctx) 192 validate_dimensions('dimension', builder.dimensions, ctx)
102 if not bucket_has_pool_dim and not has_pool_dimension(builder.dimensions): 193 if final and not has_pool_dimension(builder.dimensions):
103 ctx.error( 194 ctx.error('has no "pool" dimension')
104 'has no "pool" dimension. '
105 'Either define it in the builder or in "common_dimensions"')
106 195
107 if not builder.HasField('recipe') and not common_recipe: 196 with ctx.prefix('recipe: '):
108 ctx.error('recipe unspecified') 197 validate_recipe_cfg(builder.recipe, ctx, final=final)
109 else:
110 with ctx.prefix('recipe: '):
111 validate_recipe_cfg(builder.recipe, common_recipe, ctx)
112 198
113 if builder.priority > 200: 199 if builder.priority > 200:
114 ctx.error('priority must be in [0, 200] range; got %d', builder.priority) 200 ctx.error('priority must be in [0, 200] range; got %d', builder.priority)
115 201
116 202
117 def validate_cfg(swarming, ctx): 203 def validate_cfg(swarming, ctx):
204 swarming = copy.deepcopy(swarming)
205 normalize_swarming_cfg(swarming)
206
207 def make_subctx():
208 return validation.Context(
209 on_message=lambda msg: ctx.msg(msg.severity, '%s', msg.text))
210
118 if not swarming.hostname: 211 if not swarming.hostname:
119 ctx.error('hostname unspecified') 212 ctx.error('hostname unspecified')
120 for i, t in enumerate(swarming.common_swarming_tags):
121 with ctx.prefix('common tag #%d: ', i + 1):
122 validate_tag(t, ctx)
123
124 validate_dimensions('common dimension', swarming.common_dimensions, ctx)
125 has_pool_dim = has_pool_dimension(swarming.common_dimensions)
126
127 if swarming.task_template_canary_percentage > 100: 213 if swarming.task_template_canary_percentage > 100:
128 ctx.error('task_template_canary_percentage must must be in [0, 100]') 214 ctx.error('task_template_canary_percentage must must be in [0, 100]')
129 215
130 common_recipe = None 216 with ctx.prefix('builder_defaults: '):
131 if swarming.HasField('common_recipe'): 217 if swarming.builder_defaults.name:
132 common_recipe = swarming.common_recipe 218 ctx.error('do not specify default name')
133 with ctx.prefix('common_recipe: '): 219 subctx = make_subctx()
134 validate_recipe_properties( 220 validate_builder_cfg(swarming.builder_defaults, subctx, final=False)
135 swarming.common_recipe.properties, 221 builder_defaults_has_errors = subctx.result().has_errors
136 swarming.common_recipe.properties_j, ctx)
137 222
138 for i, b in enumerate(swarming.builders): 223 for i, b in enumerate(swarming.builders):
139 with ctx.prefix('builder %s: ' % (b.name or '#%s' % (i + 1))): 224 with ctx.prefix('builder %s: ' % (b.name or '#%s' % (i + 1))):
140 validate_builder_cfg( 225 # Validate b before merging, otherwise merging will fail.
141 b, ctx, bucket_has_pool_dim=has_pool_dim, 226 subctx = make_subctx()
142 common_recipe=common_recipe) 227 validate_builder_cfg(b, subctx, final=False)
228 if subctx.result().has_errors or builder_defaults_has_errors:
229 # Do no try to merge invalid configs.
230 continue
231
232 merged = copy.deepcopy(swarming.builder_defaults)
233 merge_builder(merged, b)
234 validate_builder_cfg(merged, ctx)
143 235
144 236
145 def has_pool_dimension(dimensions): 237 def has_pool_dimension(dimensions):
146 return any(d.startswith('pool:') for d in dimensions) 238 return any(d.startswith('pool:') for d in dimensions)
OLDNEW
« 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