blob: 97a99ada02eb6ead76cbac5d5a8487a689e657ab [file] [log] [blame]
# Copyright 2019 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.
from recipe_engine import recipe_api
from contextlib import contextmanager
class DockerApi(recipe_api.RecipeApi):
"""Provides steps to connect and run Docker images."""
def __init__(self, luci_context, *args, **kwargs):
super(DockerApi, self).__init__(*args, **kwargs)
self._luci_context = luci_context
def __call__(self, *args, **kwargs):
"""Executes specified docker command.
Please make sure to use api.docker.login method before if specified command
requires authentication.
Args:
args: arguments passed to the 'docker' command including subcommand name,
e.g. api.docker('push', 'my_image:latest').
kwargs: arguments passed down to api.step module.
"""
cmd = ['docker']
step_name = kwargs.pop('step_name', 'docker %s' % args[0])
return self.m.step(step_name, cmd + list(args), **kwargs)
def build(self, dockerfile, tags=(), build_args=(), cache_from=None):
"""Build docker image based on dockerfile.
E.g.with api.context(cwd=working_dir):
api.docker.build(dockerfile,
[ 'gcr.io/goma-fuchsia/fuchsia_linux/clang-r101' ],
'gcr.io/goma-fuchsia/fuchsia_linux/clang-r101')
will build docker image from 'dockerfile', using
'gcr.io/goma-fuchsia/fuchsia_linux/clang-r101' image as
cache and tag it with 'gcr.io/goma-fuchsia/fuchsia_linux/clang-r101'
Please note, due to limitation of docker, dockerfile must located
within the working_dir or its subdirectories.
Args:
* dockerfile (str) - The path to the dockerfile.
* tags (list(str)) - The list of tags need to be put on built image.
* build_args (list(str)) - The list of build args.
* cache_from (str) - The tag to the cache image. It's optional.
"""
args = [
'build',
'--file=%s' % dockerfile,
]
if cache_from:
args.append('--cache-from=%s' % cache_from)
for tag in tags:
args.append('--tag=%s' % tag)
for build_arg in build_args:
args.extend(['--build-arg', build_arg])
args.append(self.m.context.cwd)
name = tags[0] if tags else dockerfile
self(*args, step_name='docker build %s' % name)
@contextmanager
def create(self, tag, name=''):
"""Make context wrapping of creating docker container
and returns the name of the created container.
E.g. with create(tag, name='optional-name') as container:
api.docker.copy(container, ['path/inside/container'], api.path)
Args:
* tag (str) - The tag of the docker image.
* name (str) - Optional name of the created container.
"""
try:
self('pull', tag, step_name='pull docker image %s' % tag)
args = ['create']
if name:
args.extend(['--name', name])
args.extend([tag])
container_id = self(*args, stdout=self.m.raw_io.output()).stdout.strip()
yield container_id
finally:
self(
'rm',
'-fv',
container_id,
step_name='remove %s container' % container_id)
def copy(self, name, file_list, target_dir):
"""Copy files from a docker container.
E.g. copy(
'gomatools',
['/opt/goma/bin/setup_cmd'],
api.path['cleanup'])
will copy file '/opt/goma/bin/setup_cmd' from container 'gomatools' and
save it to api.path['cleanup'] directory.
Args:
* name (str) - The name of the docker container.
* file_list (list(str)) - The list of files need to be copied.
* target_dir (Path) - The target path.
"""
with self.m.step.nest('copy file from container %s' % name):
target_dir = str(target_dir)
if not target_dir:
target_dir = './'
if target_dir[-1] != '/':
target_dir = target_dir + '/'
for item in file_list:
self(
'cp',
'%s:%s' % (name, item),
target_dir,
step_name='copy %s' % item)
def cleanup(self):
"""Clean up storage resources used by docker."""
self('system', 'prune', '-f', step_name='docker cleanup')
@property
def version(self):
"""Returns Docker version installed or None if failed to detect."""
step = self(
'version',
stdout=self.m.raw_io.output(),
step_test_data=(
lambda: self.m.raw_io.test_api.stream_output('Version: 1.2.3')))
for line in step.stdout.splitlines():
line = line.strip().lower()
if line.startswith('version: '):
version = line[len('version: '):]
step.presentation.step_text = version
return version
step.presentation.step_text = 'unknown'
return None
def run(self,
server,
project,
image,
cmd_args=(),
dir_mapping=None,
env=None,
inherit_luci_context=False,
**kwargs):
"""Run a command in a Docker image as the current user:group.
Args:
server (str): Docker registry server.
project (str): Docekr registry project.
image (str): Name of the image to run.
cmd_args (seq[str]): Used to specify command to run in an image as a list of
arguments. If not specified, then the default command embedded into
the image is executed.
dir_mapping (seq[tuple]): List of tuples (host_dir, docker_dir) mapping host
directories to directories in a Docker container. Directories are
mapped as read-write.
env (dict[str]str) : dict of env variables.
inherit_luci_context (bool): Inherit current LUCI Context (including auth).
CAUTION: removes network isolation between the container and the
docker host. Read more https://docs.docker.com/network/host/.
"""
args = ['run']
if dir_mapping:
for host_dir, docker_dir in dir_mapping:
# Ensure that host paths exist, otherwise they will be created by the docker
# command, which makes them owned by root and thus hard to remove/modify.
if not self.m.path.exists(host_dir):
self.m.file.ensure_directory('host dir', host_dir)
args.extend(['--volume', '%s:%s' % (host_dir, docker_dir)])
if env:
for k, v in sorted((env or {}).iteritems()):
args.extend(['--env', '%s=%s' % (k, v)])
if inherit_luci_context:
assert self.m.platform.is_linux, 'supported only on Linux'
if not self._luci_context: # pragma: no cover
raise self.m.step.InfraFailure('$LUCI_CONTEXT is not set or empty')
args.extend([
# Map the temp file to /tmp/luci_context inside the container.
'--volume',
'%s:/tmp/luci_context:ro' % self._luci_context,
# Set LUCI_CONTEXT variable pointing to /tmp/luci_context.
'--env',
'LUCI_CONTEXT=/tmp/luci_context',
# Remove network isolation, so the container can talk to auth server.
'--network',
'host',
])
args.append('%s/%s/%s' % (server, project, image))
args.extend(cmd_args)
return self(*args, **kwargs)