Index: appengine/swarming/cipd.py |
diff --git a/appengine/swarming/cipd.py b/appengine/swarming/cipd.py |
index d04a087800194dca5f81e7745c39a40cc395ec40..7c8734e57a4abf5a2b06372701a8554592877515 100644 |
--- a/appengine/swarming/cipd.py |
+++ b/appengine/swarming/cipd.py |
@@ -4,6 +4,8 @@ |
"""CIPD-specific code is concentrated here.""" |
+import contextlib |
+import logging |
import re |
# Regular expressions below are copied from |
@@ -26,13 +28,99 @@ TAG_MAX_LEN = 400 |
# os can be "linux", "mac" or "windows" and arch can be "386", "amd64" or |
# "armv6l". |
PARAM_PLATFORM = '${platform}' |
+PARAM_PLATFORM_ESC = re.escape(PARAM_PLATFORM) |
# OS version parameter defines major and minor version of the OS distribution. |
# It is useful if package depends on .dll/.so libraries provided by the OS. |
# Example values: "ubuntu14_04", "mac10_9", "win6_1". |
PARAM_OS_VER = '${os_ver}' |
+PARAM_OS_VER_ESC = re.escape(PARAM_OS_VER) |
ALL_PARAMS = (PARAM_PLATFORM, PARAM_OS_VER) |
+@contextlib.contextmanager |
+def pin_check_fn(platform, os_ver): |
+ """Yields a function that verifies that an input CipdPackage could have been |
+ plausibly expanded, via pinning, to another CipdPackage. Repeated invocations |
+ of the function will retain knowledge of any resolved name template paramters |
+ like ${platform} and ${os_ver}. |
+ |
+ Args: |
+ platform - a pre-defined expansion of ${platform}, or None to learn from the |
+ first valid checked CipdPackage containing ${platform}. |
+ os_ver - a pre-defined expansion of ${os_ver}, or None to learn from the |
+ first valid checked CipdPackage containing ${os_ver}. |
+ |
+ Args of yielded function: |
+ original - a CipdPackage which may contain template params like ${platform} |
+ expanded - a CipdPackage which is nominally an expansion of original. |
+ |
+ CipdPackage is a duck-typed object which has three string properties: |
+ 'package_name', 'path' and 'version'. |
+ |
+ Yielded function raises: |
+ ValueError if expanded is not a valid derivation of original. |
+ |
+ Example: |
+ with pin_check_fn(None, None) as check: |
+ check(CipdPackage('', '${platform}', 'ref'), |
+ CipdPackage('', 'windows-amd64', 'deadbeef'*5)) |
+ check(CipdPackage('', '${platform}', 'ref'), |
+ CipdPackage('', 'linux-amd64', 'deadbeef'*5)) ## will raise ValueError |
+ """ |
+ plat_ref = [platform] |
+ os_ver_ref = [os_ver] |
+ def _check_fn(original, expanded): |
+ if original.path != expanded.path: |
+ logging.warn('Mismatched path: %r v %r', original.path, expanded.path) |
+ raise ValueError('Mismatched path') |
+ |
+ def sub_param(regex, param_esc, param_re, param_const): |
+ # This is validated at task creation time as well, but just to make sure. |
+ if regex.count(param_esc) > 1: |
+ logging.warn('Duplicate template param %r: %r', param_esc, regex) |
+ raise ValueError('%s occurs more than once in name.' % param_esc) |
+ |
+ ret = False |
+ if param_const is None: |
+ ret = param_esc in regex |
+ if ret: |
+ regex = regex.replace(param_esc, param_re, 1) |
+ else: |
+ regex = regex.replace(param_esc, param_const, 1) |
+ return regex, ret |
+ |
+ name_regex = re.escape(original.package_name) |
+ name_regex, scan_plat = sub_param( |
+ name_regex, PARAM_PLATFORM_ESC, r'(?P<platform>\w+-[a-z0-9]+)', |
+ plat_ref[0]) |
+ name_regex, scan_os_ver = sub_param( |
+ name_regex, PARAM_OS_VER_ESC, r'(?P<os_ver>[_a-z0-9]+)', |
+ os_ver_ref[0]) |
+ |
+ match = re.match(name_regex, expanded.package_name) |
+ if not match: |
+ logging.warn('Mismatched package_name: %r | %r v %r', |
+ original.package_name, name_regex, expanded.package_name) |
+ raise ValueError('Mismatched package_name') |
+ |
+ if is_valid_instance_id(original.version): |
+ if original.version != expanded.version: |
+ logging.warn('Mismatched pins: %r v %r', original.version, |
+ expanded.version) |
+ raise ValueError('Mismatched pins') |
+ else: |
+ if not is_valid_instance_id(expanded.version): |
+ logging.warn('Pin not a pin: %r', expanded.version) |
+ raise ValueError('Pin value is not a pin') |
+ |
+ if scan_plat: |
+ plat_ref[0] = re.escape(match.group('platform')) |
+ if scan_os_ver: |
+ os_ver_ref[0] = re.escape(match.group('os_ver')) |
+ |
+ yield _check_fn |
+ |
+ |
def is_valid_package_name(package_name): |
"""Returns True if |package_name| is a valid CIPD package name.""" |
return bool(PACKAGE_NAME_RE.match(package_name)) |
@@ -43,13 +131,15 @@ def is_valid_package_name_template(template): |
# Render known parameters first. |
for p in ALL_PARAMS: |
template = template.replace(p, 'x') |
+ if template.count(p) > 1: |
+ return False |
return is_valid_package_name(template) |
def is_valid_version(version): |
"""Returns True if |version| is a valid CIPD package version.""" |
return bool( |
- INSTANCE_ID_RE.match(version) or |
+ is_valid_instance_id(version) or |
is_valid_tag(version) or |
REF_RE.match(version) |
) |
@@ -63,6 +153,11 @@ def is_valid_tag(tag): |
return bool(TAG_KEY_RE.match(tag.split(':', 1)[0])) |
+def is_valid_instance_id(version): |
+ """Returns True if |version| is an insance_id.""" |
+ return bool(INSTANCE_ID_RE.match(version)) |
+ |
+ |
def is_pinned_version(version): |
"""Returns True if |version| is pinned.""" |
- return bool(INSTANCE_ID_RE.match(version)) or is_valid_tag(version) |
+ return is_valid_instance_id(version) or is_valid_tag(version) |