You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
15 KiB
Python
456 lines
15 KiB
Python
# Copyright 2017 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.
|
|
|
|
"""API for interacting with CIPD.
|
|
|
|
Depends on 'cipd' binary available in PATH:
|
|
https://godoc.org/go.chromium.org/luci/cipd/client/cmd/cipd
|
|
|
|
WARNING: There is an alternative cipd recipe_module in recipes-py.git:
|
|
https://codesearch.chromium.org/chromium/infra/recipes-py/recipe_modules/cipd/
|
|
|
|
Consider using that one instead.
|
|
TODO(crbug.com/875523): Delete this module.
|
|
"""
|
|
|
|
import re
|
|
|
|
from collections import namedtuple
|
|
|
|
from recipe_engine import recipe_api
|
|
from recipe_engine.config_types import Path
|
|
|
|
|
|
def check_type(name, var, expect):
|
|
if not isinstance(var, expect): # pragma: no cover
|
|
raise TypeError('%s is not %s: %r (%s)' % (
|
|
name, type(expect).__name__, var, type(var).__name__))
|
|
|
|
|
|
def check_list_type(name, var, expect_inner):
|
|
check_type(name, var, list)
|
|
for i, itm in enumerate(var):
|
|
check_type('%s[%d]' % (name, i), itm, expect_inner)
|
|
|
|
|
|
def check_dict_type(name, var, expect_key, expect_value):
|
|
check_type(name, var, dict)
|
|
for key, value in var.items():
|
|
check_type('%s: key' % name, key, expect_key)
|
|
check_type('%s[%s]' % (name, key), value, expect_value)
|
|
|
|
|
|
class PackageDefinition(object):
|
|
DIR = namedtuple('DIR', ['path', 'exclusions'])
|
|
|
|
def __init__(self, package_name, package_root, install_mode=None):
|
|
"""Build a new PackageDefinition.
|
|
|
|
Args:
|
|
package_name (str) - the name of the CIPD package
|
|
package_root (Path) - the path on the current filesystem that all files
|
|
will be relative to. e.g. if your root is /.../foo, and you add the
|
|
file /.../foo/bar/baz.json, the final cipd package will contain
|
|
'bar/baz.json'.
|
|
install_mode (None|'copy'|'symlink') - the mechanism that the cipd client
|
|
should use when installing this package. If None, defaults to the
|
|
platform default ('copy' on windows, 'symlink' on everything else).
|
|
"""
|
|
check_type('package_name', package_name, str)
|
|
check_type('package_root', package_root, Path)
|
|
check_type('install_mode', install_mode, (type(None), str))
|
|
if install_mode not in (None, 'copy', 'symlink'):
|
|
raise ValueError('invalid value for install_mode: %r' % install_mode)
|
|
self.package_name = package_name
|
|
self.package_root = package_root
|
|
self.install_mode = install_mode
|
|
|
|
self.dirs = [] # list(DIR)
|
|
self.files = [] # list(Path)
|
|
self.version_file = None # str?
|
|
|
|
def _rel_path(self, path):
|
|
"""Returns a forward-slash-delimited version of `path` which is relative to
|
|
the package root. Will raise ValueError if path is not inside the root."""
|
|
if path == self.package_root:
|
|
return '.'
|
|
if not self.package_root.is_parent_of(path):
|
|
raise ValueError(
|
|
'path %r is not the package root %r and not a child thereof' %
|
|
(path, self.package_root))
|
|
# we know that root has the same base and some prefix of path
|
|
return '/'.join(path.pieces[len(self.package_root.pieces):])
|
|
|
|
def add_dir(self, dir_path, exclusions=None):
|
|
"""Recursively add a directory to the package.
|
|
|
|
Args:
|
|
dir_path (Path) - A path on the current filesystem under the
|
|
package_root to a directory which should be recursively included.
|
|
exclusions (list(str)) - A list of regexps to exclude when scanning the
|
|
given directory. These will be tested against the forward-slash path
|
|
to the file relative to `dir_path`.
|
|
|
|
Raises:
|
|
ValueError - dir_path is not a subdirectory of the package root.
|
|
re.error - one of the exclusions is not a valid regex.
|
|
"""
|
|
check_type('dir_path', dir_path, Path)
|
|
exclusions = exclusions or []
|
|
check_list_type('exclusions', exclusions, str)
|
|
self.dirs.append(self.DIR(self._rel_path(dir_path), exclusions))
|
|
|
|
def add_file(self, file_path):
|
|
"""Add a single file to the package.
|
|
|
|
Args:
|
|
file_path (Path) - A path on the current filesystem to the file you
|
|
wish to include.
|
|
|
|
Raises:
|
|
ValueError - file_path is not a subdirectory of the package root.
|
|
"""
|
|
check_type('file_path', file_path, Path)
|
|
self.files.append(self._rel_path(file_path))
|
|
|
|
def add_version_file(self, ver_file_rel):
|
|
"""Instruct the cipd client to place a version file in this location when
|
|
unpacking the package.
|
|
|
|
Version files are JSON which look like: {
|
|
"package_name": "infra/tools/cipd/android-amd64",
|
|
"instance_id": "433bfdf86c0bb82d1eee2d1a0473d3709c25d2c4"
|
|
}
|
|
|
|
The convention is to pick a location like '.versions/<name>.cipd_version'
|
|
so that a given cipd installation root might have a .versions folder full
|
|
of these files, one per package. This file allows executables contained
|
|
in the package to look for and report this file, allowing them to display
|
|
version information about themselves. <name> could be the name of the
|
|
binary tool, like 'cipd' in the example above.
|
|
|
|
A version file may be specifed exactly once per package.
|
|
|
|
Args:
|
|
ver_file_rel (str) - A path string relative to the installation root.
|
|
Should be specified in posix style (forward/slashes).
|
|
"""
|
|
check_type('ver_file_rel', ver_file_rel, str)
|
|
if self.version_file is not None:
|
|
raise ValueError('add_version_file() may only be used once.')
|
|
self.version_file = ver_file_rel
|
|
|
|
def to_jsonish(self):
|
|
"""Returns a JSON representation of this PackageDefinition."""
|
|
return {
|
|
'package': self.package_name,
|
|
'root': str(self.package_root),
|
|
'install_mode': self.install_mode or '',
|
|
'data': [
|
|
{'file': str(f)}
|
|
for f in self.files
|
|
]+[
|
|
{'dir': str(d.path), 'exclude': d.exclusions}
|
|
for d in self.dirs
|
|
]+([{'version_file': self.version_file}] if self.version_file else [])
|
|
}
|
|
|
|
|
|
class CIPDApi(recipe_api.RecipeApi):
|
|
"""CIPDApi provides basic support for CIPD.
|
|
|
|
This assumes that `cipd` (or `cipd.exe` or `cipd.bat` on windows) has been
|
|
installed somewhere in $PATH. This will be true if you use depot_tools, or if
|
|
your recipe is running inside of chrome-infrastructure's systems (buildbot,
|
|
swarming).
|
|
"""
|
|
PackageDefinition = PackageDefinition
|
|
|
|
# Map for architecture mapping. First key is platform module arch, second is
|
|
# platform module bits.
|
|
_SUFFIX_ARCH_MAP = {
|
|
'intel': {
|
|
32: '386',
|
|
64: 'amd64',
|
|
},
|
|
'mips': {
|
|
64: 'mips64',
|
|
},
|
|
'arm': {
|
|
32: 'armv6',
|
|
64: 'arm64',
|
|
},
|
|
}
|
|
|
|
# pylint: disable=attribute-defined-outside-init
|
|
def initialize(self):
|
|
self._cipd_credentials = None
|
|
|
|
def set_service_account_credentials(self, path):
|
|
self._cipd_credentials = path
|
|
|
|
@property
|
|
def executable(self):
|
|
return 'cipd' + ('.bat' if self.m.platform.is_win else '')
|
|
|
|
@property
|
|
def default_bot_service_account_credentials(self):
|
|
# Path to a service account credentials to use to talk to CIPD backend.
|
|
# Deployed by Puppet.
|
|
if self.m.platform.is_win:
|
|
return 'C:\\creds\\service_accounts\\service-account-cipd-builder.json'
|
|
else:
|
|
return '/creds/service_accounts/service-account-cipd-builder.json'
|
|
|
|
def platform_suffix(self, name=None, arch=None, bits=None):
|
|
"""Use to get full package name that is platform indepdent.
|
|
|
|
Example:
|
|
>>> 'my/package/%s' % api.cipd.platform_suffix()
|
|
'my/package/linux-amd64'
|
|
|
|
Optional platform bits and architecture may be supplied to generate CIPD
|
|
suffixes for other platforms. If any are omitted, the current platform
|
|
parameters will be used.
|
|
"""
|
|
name = name or self.m.platform.name
|
|
arch = arch or self.m.platform.arch
|
|
bits = bits or self.m.platform.bits
|
|
|
|
arch_map = self._SUFFIX_ARCH_MAP.get(arch)
|
|
if not arch_map:
|
|
raise KeyError('No architecture mapped for %r.' % (arch,))
|
|
arch_str = arch_map.get(bits)
|
|
if not arch_str:
|
|
raise KeyError('No architecture mapped for %r with %r bits.' % (
|
|
arch, bits))
|
|
|
|
return '%s-%s' % (
|
|
name.replace('win', 'windows'),
|
|
arch_str)
|
|
|
|
def build(self, input_dir, output_package, package_name, install_mode=None):
|
|
"""Builds, but does not upload, a cipd package from a directory.
|
|
|
|
Args:
|
|
input_dir (Path) - the directory to build the package from.
|
|
output_package (Path) - the file to write the package to.
|
|
package_name (str) - the name of the cipd package as it would appear when
|
|
uploaded to the cipd package server.
|
|
install_mode (None|'copy'|'symlink') - the mechanism that the cipd client
|
|
should use when installing this package. If None, defaults to the
|
|
platform default ('copy' on windows, 'symlink' on everything else).
|
|
"""
|
|
assert not install_mode or install_mode in ['copy', 'symlink']
|
|
return self.m.step(
|
|
'build %s' % self.m.path.basename(package_name),
|
|
[
|
|
self.executable,
|
|
'pkg-build',
|
|
'-in', input_dir,
|
|
'-name', package_name,
|
|
'-out', output_package,
|
|
'-json-output', self.m.json.output(),
|
|
] + (
|
|
['-install-mode', install_mode] if install_mode else []
|
|
),
|
|
step_test_data=lambda: self.test_api.example_build(package_name)
|
|
)
|
|
|
|
def register(self, package_name, package_path, refs=None, tags=None):
|
|
cmd = [
|
|
self.executable,
|
|
'pkg-register', package_path,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
if refs:
|
|
for ref in refs:
|
|
cmd.extend(['-ref', ref])
|
|
if tags:
|
|
for tag, value in sorted(tags.items()):
|
|
cmd.extend(['-tag', '%s:%s' % (tag, value)])
|
|
return self.m.step(
|
|
'register %s' % package_name,
|
|
cmd,
|
|
step_test_data=lambda: self.test_api.example_register(package_name)
|
|
)
|
|
|
|
def _create(self, pkg_name, pkg_def_file_or_placeholder,
|
|
refs=None, tags=None):
|
|
refs = refs or []
|
|
tags = tags or {}
|
|
check_list_type('refs', refs, str)
|
|
check_dict_type('tags', tags, str, str)
|
|
cmd = [
|
|
self.executable,
|
|
'create',
|
|
'-pkg-def', pkg_def_file_or_placeholder,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
for ref in refs:
|
|
cmd.extend(['-ref', ref])
|
|
for tag, value in sorted(tags.items()):
|
|
cmd.extend(['-tag', '%s:%s' % (tag, value)])
|
|
result = self.m.step(
|
|
'create %s' % pkg_name, cmd,
|
|
step_test_data=lambda: self.test_api.m.json.output({
|
|
'result': self.test_api.make_pin(pkg_name),
|
|
}))
|
|
ret_data = result.json.output['result']
|
|
result.presentation.links[ret_data['instance_id']] = (
|
|
'https://chrome-infra-packages.appspot.com/p/%(package)s/+/%(instance_id)s' % ret_data)
|
|
return ret_data
|
|
|
|
def create_from_yaml(self, pkg_def, refs=None, tags=None):
|
|
"""Builds and uploads a package based on on-disk YAML package definition
|
|
file.
|
|
|
|
This builds and uploads the package in one step.
|
|
|
|
Args:
|
|
pkg_def (Path) - The path to the yaml file.
|
|
refs (list(str)) - A list of ref names to set for the package instance.
|
|
tags (dict(str, str)) - A map of tag name -> value to set for the package
|
|
instance.
|
|
|
|
Returns the JSON 'result' section, e.g.: {
|
|
"package": "infra/tools/cipd/android-amd64",
|
|
"instance_id": "433bfdf86c0bb82d1eee2d1a0473d3709c25d2c4"
|
|
}
|
|
"""
|
|
check_type('pkg_def', pkg_def, Path)
|
|
return self._create(self.m.path.basename(pkg_def), pkg_def, refs, tags)
|
|
|
|
def create_from_pkg(self, pkg_def, refs=None, tags=None):
|
|
"""Builds and uploads a package based on a PackageDefinition object.
|
|
|
|
This builds and uploads the package in one step.
|
|
|
|
Args:
|
|
pkg_def (PackageDefinition) - The description of the package we want to
|
|
create.
|
|
refs (list(str)) - A list of ref names to set for the package instance.
|
|
tags (dict(str, str)) - A map of tag name -> value to set for the package
|
|
instance.
|
|
|
|
Returns the JSON 'result' section, e.g.: {
|
|
"package": "infra/tools/cipd/android-amd64",
|
|
"instance_id": "433bfdf86c0bb82d1eee2d1a0473d3709c25d2c4"
|
|
}
|
|
"""
|
|
check_type('pkg_def', pkg_def, PackageDefinition)
|
|
return self._create(
|
|
pkg_def.package_name, self.m.json.input(pkg_def.to_jsonish()), refs, tags)
|
|
|
|
|
|
def ensure(self, root, packages):
|
|
"""Ensures that packages are installed in a given root dir.
|
|
|
|
packages must be a mapping from package name to its version, where
|
|
* name must be for right platform (see also ``platform_suffix``),
|
|
* version could be either instance_id, or ref, or unique tag.
|
|
|
|
If installing a package requires credentials, call
|
|
``set_service_account_credentials`` before calling this function.
|
|
"""
|
|
package_list = ['%s %s' % (name, version)
|
|
for name, version in sorted(packages.items())]
|
|
ensure_file = self.m.raw_io.input('\n'.join(package_list))
|
|
cmd = [
|
|
self.executable,
|
|
'ensure',
|
|
'-root', root,
|
|
'-ensure-file', ensure_file,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
self.m.step(
|
|
'ensure_installed', cmd,
|
|
step_test_data=lambda: self.test_api.example_ensure(packages)
|
|
)
|
|
|
|
def set_tag(self, package_name, version, tags):
|
|
cmd = [
|
|
self.executable,
|
|
'set-tag', package_name,
|
|
'-version', version,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
for tag, value in sorted(tags.items()):
|
|
cmd.extend(['-tag', '%s:%s' % (tag, value)])
|
|
|
|
return self.m.step(
|
|
'cipd set-tag %s' % package_name,
|
|
cmd,
|
|
step_test_data=lambda: self.test_api.example_set_tag(
|
|
package_name, version
|
|
)
|
|
)
|
|
|
|
def set_ref(self, package_name, version, refs):
|
|
cmd = [
|
|
self.executable,
|
|
'set-ref', package_name,
|
|
'-version', version,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
for r in refs:
|
|
cmd.extend(['-ref', r])
|
|
|
|
return self.m.step(
|
|
'cipd set-ref %s' % package_name,
|
|
cmd,
|
|
step_test_data=lambda: self.test_api.example_set_ref(
|
|
package_name, version
|
|
)
|
|
)
|
|
|
|
def search(self, package_name, tag):
|
|
assert ':' in tag, 'tag must be in a form "k:v"'
|
|
|
|
cmd = [
|
|
self.executable,
|
|
'search', package_name,
|
|
'-tag', tag,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
|
|
return self.m.step(
|
|
'cipd search %s %s' % (package_name, tag),
|
|
cmd,
|
|
step_test_data=lambda: self.test_api.example_search(package_name)
|
|
)
|
|
|
|
def describe(self, package_name, version,
|
|
test_data_refs=None, test_data_tags=None):
|
|
cmd = [
|
|
self.executable,
|
|
'describe', package_name,
|
|
'-version', version,
|
|
'-json-output', self.m.json.output(),
|
|
]
|
|
if self._cipd_credentials:
|
|
cmd.extend(['-service-account-json', self._cipd_credentials])
|
|
|
|
return self.m.step(
|
|
'cipd describe %s' % package_name,
|
|
cmd,
|
|
step_test_data=lambda: self.test_api.example_describe(
|
|
package_name, version,
|
|
test_data_refs=test_data_refs,
|
|
test_data_tags=test_data_tags
|
|
)
|
|
)
|