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

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: swarmbucket: refactor builder defaults 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 json 5 import json
6 import re 6 import re
7 7
8 from proto import project_config_pb2 8 from proto import project_config_pb2
9 9
10 10
11 DIMENSION_KEY_RGX = re.compile(r'^[a-zA-Z\_\-]+$') 11 DIMENSION_KEY_RGX = re.compile(r'^[a-zA-Z\_\-]+$')
12 NO_PROPERTY = object()
13
14
15 def copy_msg(msg):
16 """Clones a protobuf message."""
Vadim Sh. 2016/08/03 20:58:11 how does it handle fields that are protos themselv
nodir 2016/08/03 21:38:11 removed in favor of copy.deepcopy because this thi
17 copy = type(msg)()
18 copy.CopyFrom(msg)
19 return copy
20
21
22 def read_properties(recipe):
23 """Parses build properties from the recipe message.
24
25 Expects the message to be valid.
26
27 Uses NO_PROPERTY for empty values.
28 """
29 result = dict(p.split(':', 1) for p in recipe.properties)
30 for p in recipe.properties_j:
31 k, v = p.split(':', 1)
32 if not v:
Vadim Sh. 2016/08/03 20:58:12 I find it a bit sad that 'properties: "a:"' and 'p
nodir 2016/08/03 21:38:11 unfortunately no, because properties_j: "a:\"b\""
33 parsed = NO_PROPERTY
34 else:
35 parsed = json.loads(v)
36 result[k] = parsed
37 return result
38
39
40 def merge_recipe(r1, r2):
41 """Merges Recipe message r2 into r1.
42
43 Expects messages to be valid.
44
45 All properties are converted to properties_j.
46 """
47 props = read_properties(r1)
48 props.update(read_properties(r2))
49
50 r1.MergeFrom(r2)
51 r1.properties[:] = []
52 r1.properties_j[:] = [
53 '%s:%s' % (k, json.dumps(v))
54 for k, v in sorted(props.iteritems())
55 if v is not NO_PROPERTY
56 ]
57
58
59 def merge_dimensions(d1, d2):
60 """Merges dimensions. Values in d2 overwrite values in d1.
61
62 Expects dimensions to be valid.
63
64 If a dimensions value in d2 is empty, it is excluded from the result.
65 """
66 parse = lambda d: dict(a.split(':', 1) for a in d)
67 dims = parse(d1)
68 dims.update(parse(d2))
69 return ['%s:%s' % (k, v) for k, v in sorted(dims.iteritems()) if v]
70
71
72 def merge_builder(b1, b2):
73 """Merges Builder message b2 into b1. Expects messages to be valid."""
74 dims = merge_dimensions(b1.dimensions, b2.dimensions)
75 recipe = None
76 if b1.HasField('recipe') or b2.HasField('recipe'): # pragma: no branch
77 recipe = copy_msg(b1.recipe)
78 merge_recipe(recipe, b2.recipe)
79
80 b1.MergeFrom(b2)
81 b1.dimensions[:] = dims
82 if recipe: # pragma: no branch
83 b1.recipe.CopyFrom(recipe)
84
85
86 # TODO(nodir): remove this function once all confgis are converted
87 def normalize_swarming_cfg(cfg):
88 """Converts deprecated fields into new ones.
89
90 Does not check for presence of both new and deprecated fields, because
91 configs will be migrated shortly after this change is deployed.
92 """
93 if cfg.HasField('builder_defaults'): # pragma: no cover
94 return
95 defs = cfg.builder_defaults
96 defs.swarming_tags[:] = cfg.common_swarming_tags
97 defs.dimensions[:] = cfg.common_dimensions
98 defs.recipe.CopyFrom(cfg.common_recipe)
99 defs.execution_timeout_secs = cfg.common_execution_timeout_secs
12 100
13 101
14 def validate_tag(tag, ctx): 102 def validate_tag(tag, ctx):
15 # a valid swarming tag is a string that contains ":" 103 # a valid swarming tag is a string that contains ":"
16 if ':' not in tag: 104 if ':' not in tag:
17 ctx.error('does not have ":": %s', tag) 105 ctx.error('does not have ":": %s', tag)
18 name = tag.split(':', 1)[0] 106 name = tag.split(':', 1)[0]
19 if name.lower() == 'builder': 107 if name.lower() == 'builder':
20 ctx.error( 108 ctx.error(
21 'do not specify builder tag; ' 109 'do not specify builder tag; '
(...skipping 15 matching lines...) Expand all
37 if not DIMENSION_KEY_RGX.match(key): 125 if not DIMENSION_KEY_RGX.match(key):
38 ctx.error( 126 ctx.error(
39 'key "%s" does not match pattern "%s"', 127 'key "%s" does not match pattern "%s"',
40 key, DIMENSION_KEY_RGX.pattern) 128 key, DIMENSION_KEY_RGX.pattern)
41 if key in known_keys: 129 if key in known_keys:
42 ctx.error('duplicate key %s', key) 130 ctx.error('duplicate key %s', key)
43 else: 131 else:
44 known_keys.add(key) 132 known_keys.add(key)
45 133
46 134
47 def validate_recipe_cfg(recipe, common_recipe, ctx): 135 def validate_recipe_cfg(recipe, ctx, final=True):
48 common_recipe = common_recipe or project_config_pb2.Swarming.Recipe() 136 """Validates a Recipe message.
49 if not (recipe.name or common_recipe.name): 137
138 If final is False, does not validate for completeness.
139 """
140 if final and not recipe.name:
50 ctx.error('name unspecified') 141 ctx.error('name unspecified')
51 if not (recipe.repository or common_recipe.repository): 142 if final and not recipe.repository:
52 ctx.error('repository unspecified') 143 ctx.error('repository unspecified')
53 validate_recipe_properties(recipe.properties, recipe.properties_j, ctx) 144 validate_recipe_properties(recipe.properties, recipe.properties_j, ctx)
54 145
55 146
56 def validate_recipe_properties(properties, properties_j, ctx): 147 def validate_recipe_properties(properties, properties_j, ctx):
57 keys = set() 148 keys = set()
58 149
59 def validate_key(key): 150 def validate_key(key):
60 if not key: 151 if not key:
61 ctx.error('key not specified') 152 ctx.error('key not specified')
(...skipping 14 matching lines...) Expand all
76 keys.add(key) 167 keys.add(key)
77 168
78 for i, p in enumerate(properties_j): 169 for i, p in enumerate(properties_j):
79 with ctx.prefix('properties_j #%d: ', i + 1): 170 with ctx.prefix('properties_j #%d: ', i + 1):
80 if ':' not in p: 171 if ':' not in p:
81 ctx.error('does not have colon') 172 ctx.error('does not have colon')
82 else: 173 else:
83 key, value = p.split(':', 1) 174 key, value = p.split(':', 1)
84 validate_key(key) 175 validate_key(key)
85 keys.add(key) 176 keys.add(key)
86 try: 177 if value:
87 json.loads(value) 178 try:
88 except ValueError as ex: 179 json.loads(value)
89 ctx.error(ex) 180 except ValueError as ex:
181 ctx.error(ex)
90 182
91 183
92 def validate_builder_cfg( 184 def validate_builder_cfg(builder, ctx, final=True):
93 builder, ctx, bucket_has_pool_dim=False, common_recipe=None): 185 """Validates a Builder message.
94 if not builder.name: 186
187 If final is False, does not validate for completeness.
188 """
189 if final and not builder.name:
95 ctx.error('name unspecified') 190 ctx.error('name unspecified')
96 191
97 for i, t in enumerate(builder.swarming_tags): 192 for i, t in enumerate(builder.swarming_tags):
98 with ctx.prefix('tag #%d: ', i + 1): 193 with ctx.prefix('tag #%d: ', i + 1):
99 validate_tag(t, ctx) 194 validate_tag(t, ctx)
100 195
101 validate_dimensions('dimension', builder.dimensions, ctx) 196 validate_dimensions('dimension', builder.dimensions, ctx)
102 if not bucket_has_pool_dim and not has_pool_dimension(builder.dimensions): 197 if final and not has_pool_dimension(builder.dimensions):
103 ctx.error( 198 ctx.error('has no "pool" dimension')
104 'has no "pool" dimension. '
105 'Either define it in the builder or in "common_dimensions"')
106 199
107 if not builder.HasField('recipe') and not common_recipe: 200 with ctx.prefix('recipe: '):
108 ctx.error('recipe unspecified') 201 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 202
113 if builder.priority > 200: 203 if builder.priority > 200:
114 ctx.error('priority must be in [0, 200] range; got %d', builder.priority) 204 ctx.error('priority must be in [0, 200] range; got %d', builder.priority)
115 205
116 206
117 def validate_cfg(swarming, ctx): 207 def validate_cfg(swarming, ctx):
208 swarming = copy_msg(swarming)
209 normalize_swarming_cfg(swarming)
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 = ctx.proxy(ctx)
Vadim Sh. 2016/08/03 20:58:11 huh? what is 'proxy'?
nodir 2016/08/03 21:38:11 ugh, I forgot that validation context is in a diff
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 = ctx.proxy(ctx)
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_msg(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