blob: 0a3382280e7af21de049749d399e55e90bb426dd [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility functions for build tags, colon-delimeted key-value pairs.
Many short functions are annotated with "pragma: no cover" because they are
covered by other modules.
"""
import errors
import re
from go.chromium.org.luci.buildbucket.proto import common_pb2
DELIMITER = ':'
BUILDER_KEY = 'builder'
BUILD_ADDRESS_KEY = 'build_address'
BUILDSET_KEY = 'buildset'
GITILES_REF_KEY = 'gitiles_ref'
RESERVED_KEYS = {
BUILD_ADDRESS_KEY,
}
BLACKLIST_KEYS_FOR_APPEND = {
BUILD_ADDRESS_KEY,
BUILDER_KEY,
BUILDSET_KEY,
}
BUILDSET_MAX_LENGTH = 1024
# Gitiles commit buildset pattern. Example:
# ('commit/gitiles/chromium.googlesource.com/infra/luci/luci-go/+/'
# 'b7a757f457487cd5cfe2dae83f65c5bc10e288b7')
RE_BUILDSET_GITILES_COMMIT = re.compile(
r'^commit/gitiles/([^/]+)/(.+?)/\+/([a-f0-9]{40})$'
)
# Gerrit CL buildset pattern. Example:
# patch/gerrit/chromium-review.googlesource.com/677784/5
RE_BUILDSET_GERRIT_CL = re.compile(r'^patch/gerrit/([^/]+)/(\d+)/(\d+)$')
# Tags keys hidden in v2 responses.
HIDDEN_TAG_KEYS = {
BUILDER_KEY,
BUILD_ADDRESS_KEY,
GITILES_REF_KEY,
}
def builder_tag(builder): # pragma: no cover
return unparse(BUILDER_KEY, builder)
def build_address_tag(builder_id, number): # pragma: no cover
"""Returns a build_address tag."""
return unparse(BUILD_ADDRESS_KEY, build_address(builder_id, number))
def parse(tag): # pragma: no cover
"""Returns tuple (key, value) from the tag."""
if DELIMITER not in tag:
raise ValueError('tag must have ":"')
return tag.split(DELIMITER, 1)
def unparse(key, value): # pragma: no cover
# """Returns a tag string from a key-value pair."""
return '%s%s%s' % (key, DELIMITER, value)
def build_address(builder_id, number): # pragma: no cover
"""Returns value for build_address tag."""
return 'luci.%s.%s/%s/%d' % (
builder_id.project, builder_id.bucket, builder_id.builder, number
)
def gitiles_commit_buildset(gitiles_commit): # pragma: no cover
return 'commit/gitiles/%s/%s/+/%s' % (
gitiles_commit.host, gitiles_commit.project, gitiles_commit.id
)
def gerrit_change_buildset(gerrit_change): # pragma: no cover
return 'patch/gerrit/%s/%d/%d' % (
gerrit_change.host, gerrit_change.change, gerrit_change.patchset
)
def parse_gitiles_commit_buildset(buildset): # pragma: no cover
# Example:
# commit/gitiles/chromium.googlesource.com/chromium/src/+/
# 4fa74ef7511f4167d15a5a6d464df06e41ffbd70
m = RE_BUILDSET_GITILES_COMMIT.match(buildset)
if not m:
return None
return common_pb2.GitilesCommit(
host=m.group(1),
project=m.group(2),
id=m.group(3),
)
def parse_gerrit_change_buildset(buildset): # pragma: no cover
# Example: patch/gerrit/chromium-review.googlesource.com/677784/5
m = RE_BUILDSET_GERRIT_CL.match(buildset)
if not m:
return None
return common_pb2.GerritChange(
host=m.group(1),
change=int(m.group(2)),
patchset=int(m.group(3)),
)
def git_commit_buildset(commit_hash): # pragma: no cover
return 'commit/git/' + commit_hash
def validate_tags(tags, mode, builder=None):
"""Validates build tags.
mode must be a string, one of:
'new': tags are for a new build.
'append': tags are to be appended to an existing build.
'search': tags to search by.
builder is the value of model.Build.proto.builder.builder.
If specified, tags "builder:<v>" must have v equal to the builder.
Relevant only in 'new' mode.
"""
assert mode in ('new', 'append', 'search'), mode
if tags is None:
return
if not isinstance(tags, list):
raise errors.InvalidInputError('tags must be a list')
seen_builder_tag = None
seen_gitiles_commit = False
for t in tags: # pragma: no branch
if not isinstance(t, basestring):
raise errors.InvalidInputError(
'Invalid tag "%s": must be a string' % (t,)
)
if ':' not in t:
raise errors.InvalidInputError(
'Invalid tag "%s": does not contain ":"' % t
)
if t[0] == ':':
raise errors.InvalidInputError('Invalid tag "%s": starts with ":"' % t)
k, v = t.split(':', 1)
if mode == 'append' and k in BLACKLIST_KEYS_FOR_APPEND:
raise errors.InvalidInputError(
'Tag "%s" cannot be added to an existing build' % k
)
if k == BUILDSET_KEY:
try:
validate_buildset(v)
except errors.InvalidInputError as ex:
raise errors.InvalidInputError('Invalid tag "%s": %s' % (t, ex))
if RE_BUILDSET_GITILES_COMMIT.match(v): # pragma: no branch
if seen_gitiles_commit:
raise errors.InvalidInputError(
'More than one commits/gitiles buildset'
)
seen_gitiles_commit = True
if k == BUILDER_KEY:
if mode == 'new': # pragma: no branch
if builder is not None and v != builder:
raise errors.InvalidInputError(
'Tag "%s" conflicts with builder name "%s"' % (t, builder)
)
if seen_builder_tag is None:
seen_builder_tag = t
elif t != seen_builder_tag: # pragma: no branch
raise errors.InvalidInputError(
'Tag "%s" conflicts with tag "%s"' % (t, seen_builder_tag)
)
if mode != 'search' and k in RESERVED_KEYS:
raise errors.InvalidInputError('Tag "%s" is reserved' % k)
def validate_buildset(bs):
"""Raises errors.InvalidInputError if the buildset is invalid."""
if len(BUILDSET_KEY) + len(DELIMITER) + len(bs) > BUILDSET_MAX_LENGTH:
raise errors.InvalidInputError('too long')
# Verify that a buildset with a known prefix is well formed.
if bs.startswith('commit/gitiles/'):
m = RE_BUILDSET_GITILES_COMMIT.match(bs)
if not m:
raise errors.InvalidInputError(
'does not match regex "%s"' % (RE_BUILDSET_GITILES_COMMIT.pattern)
)
project = m.group(2)
if project.startswith('a/'):
raise errors.InvalidInputError('gitiles project must not start with "a/"')
if project.endswith('.git'):
raise errors.InvalidInputError('gitiles project must not end with ".git"')
elif bs.startswith('patch/gerrit/'):
if not RE_BUILDSET_GERRIT_CL.match(bs):
raise errors.InvalidInputError(
'does not match regex "%s"' % RE_BUILDSET_GERRIT_CL.pattern
)