OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 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 |
11 Prior adding Build to the datastore, a swarming task is created. The definition | 11 Prior adding Build to the datastore, a swarming task is created. The definition |
(...skipping 20 matching lines...) Expand all Loading... |
32 import json | 32 import json |
33 import logging | 33 import logging |
34 import string | 34 import string |
35 | 35 |
36 from components import auth | 36 from components import auth |
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 components.config import validation |
42 from google.appengine.api import app_identity | 43 from google.appengine.api import app_identity |
43 from google.appengine.ext import ndb | 44 from google.appengine.ext import ndb |
44 import webapp2 | 45 import webapp2 |
45 | 46 |
46 from proto import project_config_pb2 | 47 from proto import project_config_pb2 |
47 from . import swarmingcfg as swarmingcfg_module | 48 from . import swarmingcfg as swarmingcfg_module |
48 import config | 49 import config |
49 import errors | 50 import errors |
50 import model | 51 import model |
51 import notifications | 52 import notifications |
| 53 import protoutil |
| 54 |
52 | 55 |
53 PUBSUB_TOPIC = 'swarming' | 56 PUBSUB_TOPIC = 'swarming' |
54 BUILDER_PARAMETER = 'builder_name' | 57 BUILDER_PARAMETER = 'builder_name' |
55 PARAM_PROPERTIES = 'properties' | 58 PARAM_PROPERTIES = 'properties' |
56 PARAM_SWARMING = 'swarming' | 59 PARAM_SWARMING = 'swarming' |
57 PARAM_CHANGES = 'changes' | 60 PARAM_CHANGES = 'changes' |
58 DEFAULT_URL_FORMAT = 'https://{swarming_hostname}/user/task/{task_id}' | 61 DEFAULT_URL_FORMAT = 'https://{swarming_hostname}/user/task/{task_id}' |
59 | 62 |
60 | 63 |
61 ################################################################################ | 64 ################################################################################ |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
130 | 133 |
131 def validate_build_parameters(builder_name, params): | 134 def validate_build_parameters(builder_name, params): |
132 """Raises errors.InvalidInputError if build parameters are invalid.""" | 135 """Raises errors.InvalidInputError if build parameters are invalid.""" |
133 params = copy.deepcopy(params) | 136 params = copy.deepcopy(params) |
134 | 137 |
135 def bad(fmt, *args): | 138 def bad(fmt, *args): |
136 raise errors.InvalidInputError(fmt % args) | 139 raise errors.InvalidInputError(fmt % args) |
137 | 140 |
138 params.pop(BUILDER_PARAMETER) # already validated | 141 params.pop(BUILDER_PARAMETER) # already validated |
139 | 142 |
| 143 |
| 144 def assert_object(name, value): |
| 145 if not isinstance(value, dict): |
| 146 bad('%s parameter must be an object' % name) |
| 147 |
140 properties = params.pop(PARAM_PROPERTIES, None) | 148 properties = params.pop(PARAM_PROPERTIES, None) |
141 if properties is not None: | 149 if properties is not None: |
142 if not isinstance(properties, dict): | 150 assert_object('properties', properties) |
143 bad('properties parameter must be an object') | |
144 if properties.get('buildername', builder_name) != builder_name: | 151 if properties.get('buildername', builder_name) != builder_name: |
145 bad('inconsistent builder name') | 152 bad('inconsistent builder name') |
146 | 153 |
147 swarming = params.pop(PARAM_SWARMING, None) | 154 swarming = params.pop(PARAM_SWARMING, None) |
148 if swarming is not None: | 155 if swarming is not None: |
149 if not isinstance(swarming, dict): | 156 assert_object('swarming', swarming) |
150 bad('swarming parameter must be an object') | |
151 swarming = copy.deepcopy(swarming) | 157 swarming = copy.deepcopy(swarming) |
152 if 'recipe' in swarming: | 158 if 'recipe' in swarming: |
| 159 logging.error( |
| 160 'someone is still using deprecated swarming.recipe parameter') |
153 recipe = swarming.pop('recipe') | 161 recipe = swarming.pop('recipe') |
154 if not isinstance(recipe, dict): | 162 assert_object('swarming.recipe', recipe) |
155 bad('swarming.recipe parameter must be an object') | |
156 if 'revision' in recipe: | 163 if 'revision' in recipe: |
157 revision = recipe.pop('revision') | 164 revision = recipe.pop('revision') |
158 if not isinstance(revision, basestring): | 165 if not isinstance(revision, basestring): |
159 bad('swarming.recipe.revision parameter must be a string') | 166 bad('swarming.recipe.revision parameter must be a string') |
160 if recipe: | 167 if recipe: |
161 bad('unrecognized keys in swarming.recipe: %r', recipe) | 168 bad('unrecognized keys in swarming.recipe: %r', recipe) |
162 canary_template = swarming.pop('canary_template', None) | 169 canary_template = swarming.pop('canary_template', None) |
163 if canary_template not in (True, False, None): | 170 if canary_template not in (True, False, None): |
164 bad('swarming.canary_template parameter must true, false or null') | 171 bad('swarming.canary_template parameter must true, false or null') |
| 172 |
| 173 override_builder_cfg_data = swarming.pop('override_builder_cfg', None) |
| 174 if override_builder_cfg_data is not None: |
| 175 assert_object('swarming.override_builder_cfg', override_builder_cfg_data) |
| 176 override_builder_cfg = project_config_pb2.Swarming.Builder() |
| 177 try: |
| 178 protoutil.merge_dict( |
| 179 override_builder_cfg_data, override_builder_cfg) |
| 180 except TypeError as ex: |
| 181 bad('swarming.override_builder_cfg parameter: %s', ex) |
| 182 if override_builder_cfg.name: |
| 183 bad('swarming.override_builder_cfg cannot override builder name') |
| 184 ctx = validation.Context.raise_on_error( |
| 185 exc_type=errors.InvalidInputError, |
| 186 prefix='swarming.override_builder_cfg parameter: ') |
| 187 swarmingcfg_module.validate_builder_cfg( |
| 188 override_builder_cfg, ctx, final=False) |
| 189 |
165 if swarming: | 190 if swarming: |
166 bad('unrecognized keys in swarming param: %r', swarming.keys()) | 191 bad('unrecognized keys in swarming param: %r', swarming.keys()) |
167 | 192 |
168 changes = params.pop(PARAM_CHANGES, None) | 193 changes = params.pop(PARAM_CHANGES, None) |
169 if changes is not None: | 194 if changes is not None: |
170 if not isinstance(changes, list): | 195 if not isinstance(changes, list): |
171 bad('changes param must be an array') | 196 bad('changes param must be an array') |
172 for c in changes: # pragma: no branch | 197 for c in changes: # pragma: no branch |
173 if not isinstance(c, dict): | 198 if not isinstance(c, dict): |
174 bad('changes param must contain only objects') | 199 bad('changes param must contain only objects') |
(...skipping 21 matching lines...) Expand all Loading... |
196 if percentage == 0: | 221 if percentage == 0: |
197 return False | 222 return False |
198 identity = { | 223 identity = { |
199 'bucket': build.bucket, | 224 'bucket': build.bucket, |
200 'parametres': build.parameters, | 225 'parametres': build.parameters, |
201 } | 226 } |
202 digest = hashlib.sha1(json.dumps(identity, sort_keys=True)).digest() | 227 digest = hashlib.sha1(json.dumps(identity, sort_keys=True)).digest() |
203 return digest[0] < 256 * percentage / 100 | 228 return digest[0] < 256 * percentage / 100 |
204 | 229 |
205 | 230 |
| 231 def _prepare_builder_config(swarming_cfg, builder_cfg, swarming_param): |
| 232 """Returns final version of builder config to use for |build|. |
| 233 |
| 234 Expects arguments to be valid. |
| 235 """ |
| 236 swarming_cfg = copy.deepcopy(swarming_cfg) |
| 237 swarmingcfg_module.normalize_swarming_cfg(swarming_cfg) |
| 238 |
| 239 # Apply defaults. |
| 240 result = copy.deepcopy(swarming_cfg.builder_defaults) |
| 241 swarmingcfg_module.merge_builder(result, builder_cfg) |
| 242 |
| 243 # Apply overrides in the swarming parameter. |
| 244 override_builder_cfg_data = swarming_param.get('override_builder_cfg', {}) |
| 245 if override_builder_cfg_data: |
| 246 override_builder_cfg = project_config_pb2.Swarming.Builder() |
| 247 protoutil.merge_dict(override_builder_cfg_data, result) |
| 248 ctx = validation.Context.raise_on_error( |
| 249 exc_type=errors.InvalidInputError, |
| 250 prefix='swarming.override_buider_cfg parameter: ') |
| 251 swarmingcfg_module.merge_builder(result, override_builder_cfg) |
| 252 swarmingcfg_module.validate_builder_cfg(result, ctx) |
| 253 return result |
| 254 |
| 255 |
206 @ndb.tasklet | 256 @ndb.tasklet |
207 def create_task_def_async(project_id, swarming_cfg, builder_cfg, build): | 257 def create_task_def_async(project_id, swarming_cfg, builder_cfg, build): |
208 """Creates a swarming task definition for the |build|. | 258 """Creates a swarming task definition for the |build|. |
209 | 259 |
210 Supports build properties that are supported by Buildbot-Buildbucket | 260 Supports build properties that are supported by Buildbot-Buildbucket |
211 integration. See | 261 integration. See |
212 https://chromium.googlesource.com/chromium/tools/build/+/eff4ceb/scripts/maste
r/buildbucket/README.md#Build-parameters | 262 https://chromium.googlesource.com/chromium/tools/build/+/eff4ceb/scripts/maste
r/buildbucket/README.md#Build-parameters |
213 | 263 |
214 Raises: | 264 Raises: |
215 errors.InvalidInputError if build.parameters are invalid. | 265 errors.InvalidInputError if build.parameters are invalid. |
216 """ | 266 """ |
217 params = build.parameters or {} | 267 params = build.parameters or {} |
218 validate_build_parameters(builder_cfg.name, params) | 268 validate_build_parameters(builder_cfg.name, params) |
219 swarming_param = params.get(PARAM_SWARMING) or {} | 269 swarming_param = params.get(PARAM_SWARMING) or {} |
220 | 270 |
221 # Use canary template? | 271 # Use canary template? |
222 canary = swarming_param.get('canary_template') | 272 canary = swarming_param.get('canary_template') |
223 canary_required = bool(canary) | 273 canary_required = bool(canary) |
224 if canary is None: | 274 if canary is None: |
225 canary = should_use_canary_template( | 275 canary = should_use_canary_template( |
226 build, swarming_cfg.task_template_canary_percentage) | 276 build, swarming_cfg.task_template_canary_percentage) |
227 | 277 |
228 # Normalize/merge configs. | 278 builder_cfg = _prepare_builder_config( |
229 swarming_cfg = copy.deepcopy(swarming_cfg) | 279 swarming_cfg, builder_cfg, swarming_param) |
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 | 280 |
236 # Render task template. | 281 # Render task template. |
237 try: | 282 try: |
238 task_template, canary = yield get_task_template_async( | 283 task_template, canary = yield get_task_template_async( |
239 canary, canary_required) | 284 canary, canary_required) |
240 except CanaryTemplateNotFound as ex: | 285 except CanaryTemplateNotFound as ex: |
241 raise errors.InvalidInputError(ex.message) | 286 raise errors.InvalidInputError(ex.message) |
242 task_template_params = { | 287 task_template_params = { |
243 'bucket': build.bucket, | 288 'bucket': build.bucket, |
244 'builder': builder_cfg.name, | 289 'builder': builder_cfg.name, |
(...skipping 474 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
719 def _extend_unique(target, items): | 764 def _extend_unique(target, items): |
720 for x in items: | 765 for x in items: |
721 if x not in target: # pragma: no branch | 766 if x not in target: # pragma: no branch |
722 target.append(x) | 767 target.append(x) |
723 | 768 |
724 | 769 |
725 class TaskToken(tokens.TokenKind): | 770 class TaskToken(tokens.TokenKind): |
726 expiration_sec = 60 * 60 * 24 # 24 hours. | 771 expiration_sec = 60 * 60 * 24 # 24 hours. |
727 secret_key = auth.SecretKey('swarming_task_token', scope='local') | 772 secret_key = auth.SecretKey('swarming_task_token', scope='local') |
728 version = 1 | 773 version = 1 |
OLD | NEW |