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

Side by Side Diff: appengine/config_service/validation.py

Issue 1221643020: config services: services.cfg and validation (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: Created 5 years, 5 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 2015 The Swarming Authors. All rights reserved. 1 # Copyright 2015 The Swarming Authors. All rights reserved.
2 # Use of this source code is governed by the Apache v2.0 license that can be 2 # Use of this source code is governed by the Apache v2.0 license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import base64 5 import base64
6 import logging 6 import logging
7 import posixpath 7 import posixpath
8 import re 8 import re
9 import urlparse 9 import urlparse
10 10
11 from google.appengine.ext import ndb 11 from google.appengine.ext import ndb
12 12
13 from components import auth
13 from components import config 14 from components import config
14 from components import gitiles 15 from components import gitiles
15 from components import net 16 from components import net
16 from components.config import validation 17 from components.config import validation
17 18
18 from proto import project_config_pb2 19 from proto import project_config_pb2
19 from proto import service_config_pb2 20 from proto import service_config_pb2
20 import common 21 import common
22 import services
21 import storage 23 import storage
22 24
23 25
24 def validate_config_set(config_set, ctx=None): 26 def validate_config_set(config_set, ctx=None):
25 ctx = ctx or validation.Context.raise_on_error() 27 ctx = ctx or validation.Context.raise_on_error()
26 if not any(r.match(config_set) for r in config.ALL_CONFIG_SET_RGX): 28 if not any(r.match(config_set) for r in config.ALL_CONFIG_SET_RGX):
27 ctx.error('invalid config set: %s', config_set) 29 ctx.error('invalid config set: %s', config_set)
28 30
29 31
30 def validate_path(path, ctx=None): 32 def validate_path(path, ctx=None):
(...skipping 12 matching lines...) Expand all
43 ctx.error('not specified') 45 ctx.error('not specified')
44 return 46 return
45 parsed = urlparse.urlparse(url) 47 parsed = urlparse.urlparse(url)
46 if not parsed.netloc: 48 if not parsed.netloc:
47 ctx.error('hostname not specified') 49 ctx.error('hostname not specified')
48 if parsed.scheme != 'https': 50 if parsed.scheme != 'https':
49 ctx.error('scheme must be "https"') 51 ctx.error('scheme must be "https"')
50 52
51 53
52 def validate_pattern(pattern, literal_validator, ctx): 54 def validate_pattern(pattern, literal_validator, ctx):
55 if not isinstance(pattern, basestring):
56 ctx.error('pattern must be a string: %r', pattern)
57 return
53 if ':' not in pattern: 58 if ':' not in pattern:
Sergiy Byelozyorov 2015/07/07 09:10:47 a lot of this code is duplicated in compile_patter
nodir 2015/07/07 15:54:18 Done.
54 literal_validator(pattern, ctx) 59 kind = 'text'
60 value = pattern
61 else:
62 kind, value = pattern.split(':', 2)
63
64 if kind == 'text':
65 literal_validator(value, ctx)
55 return 66 return
56 67
57 pattern_type, pattern_text = pattern.split(':', 2) 68 if kind == 'regex':
58 if pattern_type != 'regex': 69 try:
59 ctx.error('unknown pattern type: %s', pattern_type) 70 re.compile(value)
71 except re.error as ex:
72 ctx.error('invalid regular expression "%s": %s', value, ex)
60 return 73 return
61 try: 74
62 re.compile(pattern_text) 75 ctx.error('unknown pattern kind: %s', kind)
63 except re.error as ex:
64 ctx.error('invalid regular expression "%s": %s', pattern_text, ex)
65 76
66 77
67 @validation.self_rule( 78 def check_id_sorted(iterable, list_name, ctx):
68 common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg) 79 """Emits a warning if the iterable is not sorted by id."""
69 def validate_validation_cfg(cfg, ctx): 80 prev = None
70 for i, rule in enumerate(cfg.rules): 81 for item in iterable:
71 with ctx.prefix('Rule #%d: ', i + 1): 82 if not item.id:
72 with ctx.prefix('config_set: '): 83 continue
73 validate_pattern(rule.config_set, validate_config_set, ctx) 84 if prev is not None and item.id < prev:
74 with ctx.prefix('path: '): 85 ctx.warning(
75 validate_pattern(rule.path, validate_path, ctx) 86 '%s are not sorted by id. First offending id: %s', list_name, item.id)
76 with ctx.prefix('url: '): 87 return
77 validate_url(rule.url, ctx) 88 prev = item.id
89
90
91 def validate_id(id, rgx, known_ids, ctx):
92 if not id:
93 ctx.error('id is not specified')
94 return
95 if not rgx.match(id):
96 ctx.error('id "%s" does not match %s regex', id, rgx.pattern)
97 return
98 if id in known_ids:
99 ctx.error('id is not unique')
100 else:
101 known_ids.add(id)
78 102
79 103
80 def validate_config_set_location(loc, ctx, allow_relative_url=False): 104 def validate_config_set_location(loc, ctx, allow_relative_url=False):
81 if not loc: 105 if not loc:
82 ctx.error('not specified') 106 ctx.error('not specified')
83 return 107 return
84 if loc.storage_type == service_config_pb2.ConfigSetLocation.UNSET: 108 if allow_relative_url and is_url_relative(loc.url):
109 if loc.storage_type != service_config_pb2.ConfigSetLocation.UNSET:
110 ctx.error('storage_type must not be set if relative url is used')
111 elif loc.storage_type == service_config_pb2.ConfigSetLocation.UNSET:
Sergiy Byelozyorov 2015/07/07 09:10:47 I would also add elif not allow_relative_url and
nodir 2015/07/07 15:54:18 gitiles.Location.parse ensures that the url is not
Sergiy Byelozyorov 2015/07/07 23:04:38 Acknowledged.
85 ctx.error('storage_type is not set') 112 ctx.error('storage_type is not set')
86 else: 113 else:
87 assert loc.storage_type == service_config_pb2.ConfigSetLocation.GITILES 114 assert loc.storage_type == service_config_pb2.ConfigSetLocation.GITILES
88 if allow_relative_url and is_url_relative(loc.url):
89 # It is relative. Avoid calling gitiles.Location.parse.
90 return
91 try: 115 try:
92 gitiles.Location.parse(loc.url) 116 gitiles.Location.parse(loc.url)
93 except ValueError as ex: 117 except ValueError as ex:
94 ctx.error(ex.message) 118 ctx.error(ex.message)
95 119
96 120
97 @validation.self_rule( 121 @validation.self_rule(
98 common.PROJECT_REGISTRY_FILENAME, service_config_pb2.ProjectsCfg) 122 common.PROJECT_REGISTRY_FILENAME, service_config_pb2.ProjectsCfg)
99 def validate_project_registry(cfg, ctx): 123 def validate_project_registry(cfg, ctx):
100 project_ids = set() 124 project_ids = set()
101 unsorted_id = None
102 for i, project in enumerate(cfg.projects): 125 for i, project in enumerate(cfg.projects):
103 with ctx.prefix('Project %s: ', project.id or ('#%d' % (i + 1))): 126 with ctx.prefix('Project %s: ', project.id or ('#%d' % (i + 1))):
104 if not project.id: 127 validate_id(project.id, config.common.PROJECT_ID_RGX, project_ids, ctx)
105 ctx.error('id is not specified')
106 else:
107 if project.id in project_ids:
108 ctx.error('id is not unique')
109 else:
110 project_ids.add(project.id)
111 if not unsorted_id and i > 0:
112 if cfg.projects[i - 1].id and project.id < cfg.projects[i - 1].id:
113 unsorted_id = project.id
114 with ctx.prefix('config_location: '): 128 with ctx.prefix('config_location: '):
115 validate_config_set_location(project.config_location, ctx) 129 validate_config_set_location(project.config_location, ctx)
130 check_id_sorted(cfg.projects, 'Projects', ctx)
116 131
117 132
118 if unsorted_id: 133 def validate_email(email, ctx):
119 ctx.warning( 134 try:
120 'Project list is not sorted by id. First offending id: %s', 135 auth.Identity('user', email)
Sergiy Byelozyorov 2015/07/07 09:10:47 I think this is a misuse of the auth.Identity.__ne
nodir 2015/07/07 15:54:18 This is exactly what I did in a previous CL. Vadim
Sergiy Byelozyorov 2015/07/07 23:04:38 Acknowledged.
121 unsorted_id) 136 except ValueError as ex:
137 ctx.error('invalid email: "%s"', email)
138
139
140 @validation.self_rule(
141 common.SERVICES_REGISTRY_FILENAME, service_config_pb2.ServicesCfg)
142 def validate_services_cfg(cfg, ctx):
143 service_ids = set()
144 for i, service in enumerate(cfg.services):
145 with ctx.prefix('Service %s: ', service.id or ('#%d' % (i + 1))):
146 validate_id(service.id, config.common.SERVICE_ID_RGX, service_ids, ctx)
147 if service.config_location and service.config_location.url:
148 with ctx.prefix('config_location: '):
149 validate_config_set_location(
150 service.config_location, ctx, allow_relative_url=True)
151 for owner in service.owners:
152 validate_email(owner, ctx)
153
154 if service.metadata_url:
155 with ctx.prefix('metadata_url: '):
156 validate_url(service.metadata_url, ctx)
157
158 check_id_sorted(cfg.services, 'Services', ctx)
159
160
161 def validate_service_dynamic_metadata_blob(metadata, ctx):
162 """Validates JSON-encoded ServiceDynamicMetadata"""
163 if not isinstance(metadata, dict):
164 ctx.error('Service dynamic metadata must be an object')
165 return
166
167 validation = metadata.get('validation')
168 if validation is None:
169 return
170
171 with ctx.prefix('validation: '):
172 if not isinstance(validation, dict):
173 ctx.error('must be an object')
174 return
175 with ctx.prefix('url: '):
176 validate_url(validation.get('url'), ctx)
177 patterns = validation.get('patterns')
178 if not isinstance(patterns, list):
179 ctx.error('patterns must be a list')
180 return
181 for i, p in enumerate(patterns):
182 with ctx.prefix('pattern #%d: ', i + 1):
183 if not isinstance(p, dict):
184 ctx.error('must be an object')
185 continue
186 with ctx.prefix('config_set: '):
187 validate_pattern(p.get('config_set'), validate_config_set, ctx)
188 with ctx.prefix('path: '):
189 validate_pattern(p.get('path'), validate_path, ctx)
122 190
123 191
124 @validation.self_rule(common.ACL_FILENAME, service_config_pb2.AclCfg) 192 @validation.self_rule(common.ACL_FILENAME, service_config_pb2.AclCfg)
125 def validate_acl_cfg(_cfg, _ctx): 193 def validate_acl_cfg(_cfg, _ctx):
126 # A valid protobuf message is enough. 194 # A valid protobuf message is enough.
127 pass 195 pass
128 196
129 @validation.self_rule(common.IMPORT_FILENAME, service_config_pb2.ImportCfg) 197 @validation.self_rule(common.IMPORT_FILENAME, service_config_pb2.ImportCfg)
130 def validate_import_cfg(_cfg, _ctx): 198 def validate_import_cfg(_cfg, _ctx):
131 # A valid protobuf message is enough. 199 # A valid protobuf message is enough.
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
177 ctx.error('name does not start with "refs/": %s', ref.name) 245 ctx.error('name does not start with "refs/": %s', ref.name)
178 elif ref.name in refs: 246 elif ref.name in refs:
179 ctx.error('duplicate ref: %s', ref.name) 247 ctx.error('duplicate ref: %s', ref.name)
180 else: 248 else:
181 refs.add(ref.name) 249 refs.add(ref.name)
182 if ref.config_path: 250 if ref.config_path:
183 validate_path(ref.config_path, ctx) 251 validate_path(ref.config_path, ctx)
184 252
185 253
186 @ndb.tasklet 254 @ndb.tasklet
187 def _endpoint_validate_async(url, config_set, path, content, ctx): 255 def _validate_by_service_async(service, config_set, path, content, ctx):
188 """Validates a config with an external service.""" 256 """Validates a config with an external service."""
257 try:
258 metadata = yield services.get_metadata_async(service.id)
259 except services.DynamicMetadataError as ex:
260 logging.error('Could not load dynamic metadata for %s: %s', service.id, ex)
261 return
262
263 assert metadata and metadata.validation
264 url = metadata.validation.url
265 if not url:
266 return
267
268 match = False
269 for p in metadata.validation.patterns:
270 # TODO(nodir): optimize if necessary.
271 if (validation.compile_pattern(p.config_set)(config_set) and
272 validation.compile_pattern(p.path)(path)):
273 match = True
274 break
275 if not match:
276 return
277
189 res = None 278 res = None
190 279
191 def report_error(text): 280 def report_error(text):
192 text = ( 281 text = (
193 'Error during external validation: %s\n' 282 'Error during external validation: %s\n'
194 'url: %s\n' 283 'url: %s\n'
195 'config_set: %s\n' 284 'config_set: %s\n'
196 'path: %s\n' 285 'path: %s\n'
197 'response: %r') % (text, url, config_set, path, res) 286 'response: %r') % (text, url, config_set, path, res)
198 logging.error(text) 287 logging.error(text)
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
238 327
239 Returns: 328 Returns:
240 components.config.validation_context.Result. 329 components.config.validation_context.Result.
241 """ 330 """
242 ctx = ctx or validation.Context() 331 ctx = ctx or validation.Context()
243 332
244 # Check the config against built-in validators, 333 # Check the config against built-in validators,
245 # defined using validation.self_rule. 334 # defined using validation.self_rule.
246 validation.validate(config_set, path, content, ctx=ctx) 335 validation.validate(config_set, path, content, ctx=ctx)
247 336
248 validation_cfg = yield storage.get_self_config_async( 337 all_services = yield services.get_services_async()
249 common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg)
250 # Be paranoid, check yourself.
251 validate_validation_cfg(validation_cfg, validation.Context.raise_on_error())
252
253 futures = [] 338 futures = []
254 for rule in validation_cfg.rules: 339 for service in all_services:
255 if (_pattern_match(rule.config_set, config_set) and 340 futures.append(
256 _pattern_match(rule.path, path)): 341 _validate_by_service_async(service, config_set, path, content, ctx))
257 futures.append(
258 _endpoint_validate_async(rule.url, config_set, path, content, ctx))
259 yield futures 342 yield futures
260 raise ndb.Return(ctx.result()) 343 raise ndb.Return(ctx.result())
261 344
262 345
263 def validate_config(*args, **kwargs): 346 def validate_config(*args, **kwargs):
264 """Blocking version of validate_async.""" 347 """Blocking version of validate_async."""
265 return validate_config_async(*args, **kwargs).get_result() 348 return validate_config_async(*args, **kwargs).get_result()
266 349
267 350
268 def _pattern_match(pattern, value):
269 # Assume pattern is valid.
270 if ':' not in pattern:
271 return pattern == value
272 else:
273 kind, pattern = pattern.split(':', 2)
274 assert kind == 'regex'
275 return bool(re.match('^%s$' % pattern, value))
276
277
278 def is_url_relative(url): 351 def is_url_relative(url):
279 parsed = urlparse.urlparse(url) 352 parsed = urlparse.urlparse(url)
280 return bool(not parsed.scheme and not parsed.netloc and parsed.path) 353 return bool(not parsed.scheme and not parsed.netloc and parsed.path)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698