blob: 20fc6644da889c42df482ca860a39790b133e186 [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().__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=(), docker_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,
]
args.extend(docker_args)
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_text()
).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", "-a", step_name="docker cleanup")
def run(
self,
image,
cmd_args=(),
dir_mapping=None,
env=None,
cwd=None,
inherit_luci_context=False,
**kwargs,
):
"""Run a command in a Docker image as the current user:group.
Args:
image (str): Name of the image to run, including version tag.
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.
cwd (str): Path to the working directory for the container.
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(
[
"--mount",
"type=bind,source=%s,destination=%s" % (host_dir, docker_dir),
]
)
if env:
for k, v in sorted((env or {}).items()):
args.extend(["--env", "%s=%s" % (k, v)])
if cwd:
args.extend(
[
"-w",
cwd,
]
)
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(image)
args.extend(cmd_args)
return self(*args, **kwargs)