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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: appengine/config_service/validation.py
diff --git a/appengine/config_service/validation.py b/appengine/config_service/validation.py
index 0d80feffb21623324d3742427eb5fb328750bcb5..fc792e6819bbb630da739d2c57d68c6b94d71109 100644
--- a/appengine/config_service/validation.py
+++ b/appengine/config_service/validation.py
@@ -10,6 +10,7 @@ import urlparse
from google.appengine.ext import ndb
+from components import auth
from components import config
from components import gitiles
from components import net
@@ -18,6 +19,7 @@ from components.config import validation
from proto import project_config_pb2
from proto import service_config_pb2
import common
+import services
import storage
@@ -50,44 +52,66 @@ def validate_url(url, ctx):
def validate_pattern(pattern, literal_validator, ctx):
+ if not isinstance(pattern, basestring):
+ ctx.error('pattern must be a string: %r', pattern)
+ return
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.
- literal_validator(pattern, ctx)
+ kind = 'text'
+ value = pattern
+ else:
+ kind, value = pattern.split(':', 2)
+
+ if kind == 'text':
+ literal_validator(value, ctx)
return
- pattern_type, pattern_text = pattern.split(':', 2)
- if pattern_type != 'regex':
- ctx.error('unknown pattern type: %s', pattern_type)
+ if kind == 'regex':
+ try:
+ re.compile(value)
+ except re.error as ex:
+ ctx.error('invalid regular expression "%s": %s', value, ex)
return
- try:
- re.compile(pattern_text)
- except re.error as ex:
- ctx.error('invalid regular expression "%s": %s', pattern_text, ex)
+ ctx.error('unknown pattern kind: %s', kind)
+
+
+def check_id_sorted(iterable, list_name, ctx):
+ """Emits a warning if the iterable is not sorted by id."""
+ prev = None
+ for item in iterable:
+ if not item.id:
+ continue
+ if prev is not None and item.id < prev:
+ ctx.warning(
+ '%s are not sorted by id. First offending id: %s', list_name, item.id)
+ return
+ prev = item.id
-@validation.self_rule(
- common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg)
-def validate_validation_cfg(cfg, ctx):
- for i, rule in enumerate(cfg.rules):
- with ctx.prefix('Rule #%d: ', i + 1):
- with ctx.prefix('config_set: '):
- validate_pattern(rule.config_set, validate_config_set, ctx)
- with ctx.prefix('path: '):
- validate_pattern(rule.path, validate_path, ctx)
- with ctx.prefix('url: '):
- validate_url(rule.url, ctx)
+
+def validate_id(id, rgx, known_ids, ctx):
+ if not id:
+ ctx.error('id is not specified')
+ return
+ if not rgx.match(id):
+ ctx.error('id "%s" does not match %s regex', id, rgx.pattern)
+ return
+ if id in known_ids:
+ ctx.error('id is not unique')
+ else:
+ known_ids.add(id)
def validate_config_set_location(loc, ctx, allow_relative_url=False):
if not loc:
ctx.error('not specified')
return
- if loc.storage_type == service_config_pb2.ConfigSetLocation.UNSET:
+ if allow_relative_url and is_url_relative(loc.url):
+ if loc.storage_type != service_config_pb2.ConfigSetLocation.UNSET:
+ ctx.error('storage_type must not be set if relative url is used')
+ 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.
ctx.error('storage_type is not set')
else:
assert loc.storage_type == service_config_pb2.ConfigSetLocation.GITILES
- if allow_relative_url and is_url_relative(loc.url):
- # It is relative. Avoid calling gitiles.Location.parse.
- return
try:
gitiles.Location.parse(loc.url)
except ValueError as ex:
@@ -98,27 +122,71 @@ def validate_config_set_location(loc, ctx, allow_relative_url=False):
common.PROJECT_REGISTRY_FILENAME, service_config_pb2.ProjectsCfg)
def validate_project_registry(cfg, ctx):
project_ids = set()
- unsorted_id = None
for i, project in enumerate(cfg.projects):
with ctx.prefix('Project %s: ', project.id or ('#%d' % (i + 1))):
- if not project.id:
- ctx.error('id is not specified')
- else:
- if project.id in project_ids:
- ctx.error('id is not unique')
- else:
- project_ids.add(project.id)
- if not unsorted_id and i > 0:
- if cfg.projects[i - 1].id and project.id < cfg.projects[i - 1].id:
- unsorted_id = project.id
+ validate_id(project.id, config.common.PROJECT_ID_RGX, project_ids, ctx)
with ctx.prefix('config_location: '):
validate_config_set_location(project.config_location, ctx)
+ check_id_sorted(cfg.projects, 'Projects', ctx)
+
+
+def validate_email(email, ctx):
+ try:
+ 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.
+ except ValueError as ex:
+ ctx.error('invalid email: "%s"', email)
- if unsorted_id:
- ctx.warning(
- 'Project list is not sorted by id. First offending id: %s',
- unsorted_id)
+@validation.self_rule(
+ common.SERVICES_REGISTRY_FILENAME, service_config_pb2.ServicesCfg)
+def validate_services_cfg(cfg, ctx):
+ service_ids = set()
+ for i, service in enumerate(cfg.services):
+ with ctx.prefix('Service %s: ', service.id or ('#%d' % (i + 1))):
+ validate_id(service.id, config.common.SERVICE_ID_RGX, service_ids, ctx)
+ if service.config_location and service.config_location.url:
+ with ctx.prefix('config_location: '):
+ validate_config_set_location(
+ service.config_location, ctx, allow_relative_url=True)
+ for owner in service.owners:
+ validate_email(owner, ctx)
+
+ if service.metadata_url:
+ with ctx.prefix('metadata_url: '):
+ validate_url(service.metadata_url, ctx)
+
+ check_id_sorted(cfg.services, 'Services', ctx)
+
+
+def validate_service_dynamic_metadata_blob(metadata, ctx):
+ """Validates JSON-encoded ServiceDynamicMetadata"""
+ if not isinstance(metadata, dict):
+ ctx.error('Service dynamic metadata must be an object')
+ return
+
+ validation = metadata.get('validation')
+ if validation is None:
+ return
+
+ with ctx.prefix('validation: '):
+ if not isinstance(validation, dict):
+ ctx.error('must be an object')
+ return
+ with ctx.prefix('url: '):
+ validate_url(validation.get('url'), ctx)
+ patterns = validation.get('patterns')
+ if not isinstance(patterns, list):
+ ctx.error('patterns must be a list')
+ return
+ for i, p in enumerate(patterns):
+ with ctx.prefix('pattern #%d: ', i + 1):
+ if not isinstance(p, dict):
+ ctx.error('must be an object')
+ continue
+ with ctx.prefix('config_set: '):
+ validate_pattern(p.get('config_set'), validate_config_set, ctx)
+ with ctx.prefix('path: '):
+ validate_pattern(p.get('path'), validate_path, ctx)
@validation.self_rule(common.ACL_FILENAME, service_config_pb2.AclCfg)
@@ -184,8 +252,29 @@ def validate_refs_cfg(cfg, ctx):
@ndb.tasklet
-def _endpoint_validate_async(url, config_set, path, content, ctx):
+def _validate_by_service_async(service, config_set, path, content, ctx):
"""Validates a config with an external service."""
+ try:
+ metadata = yield services.get_metadata_async(service.id)
+ except services.DynamicMetadataError as ex:
+ logging.error('Could not load dynamic metadata for %s: %s', service.id, ex)
+ return
+
+ assert metadata and metadata.validation
+ url = metadata.validation.url
+ if not url:
+ return
+
+ match = False
+ for p in metadata.validation.patterns:
+ # TODO(nodir): optimize if necessary.
+ if (validation.compile_pattern(p.config_set)(config_set) and
+ validation.compile_pattern(p.path)(path)):
+ match = True
+ break
+ if not match:
+ return
+
res = None
def report_error(text):
@@ -245,17 +334,11 @@ def validate_config_async(config_set, path, content, ctx=None):
# defined using validation.self_rule.
validation.validate(config_set, path, content, ctx=ctx)
- validation_cfg = yield storage.get_self_config_async(
- common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg)
- # Be paranoid, check yourself.
- validate_validation_cfg(validation_cfg, validation.Context.raise_on_error())
-
+ all_services = yield services.get_services_async()
futures = []
- for rule in validation_cfg.rules:
- if (_pattern_match(rule.config_set, config_set) and
- _pattern_match(rule.path, path)):
- futures.append(
- _endpoint_validate_async(rule.url, config_set, path, content, ctx))
+ for service in all_services:
+ futures.append(
+ _validate_by_service_async(service, config_set, path, content, ctx))
yield futures
raise ndb.Return(ctx.result())
@@ -265,16 +348,6 @@ def validate_config(*args, **kwargs):
return validate_config_async(*args, **kwargs).get_result()
-def _pattern_match(pattern, value):
- # Assume pattern is valid.
- if ':' not in pattern:
- return pattern == value
- else:
- kind, pattern = pattern.split(':', 2)
- assert kind == 'regex'
- return bool(re.match('^%s$' % pattern, value))
-
-
def is_url_relative(url):
parsed = urlparse.urlparse(url)
return bool(not parsed.scheme and not parsed.netloc and parsed.path)

Powered by Google App Engine
This is Rietveld 408576698