blob: a226e977c4ada54db524806604262a39fccc6860 [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", f"docker {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",
f"--file={dockerfile}",
]
args.extend(docker_args)
if cache_from:
args.append(f"--cache-from={cache_from}")
for tag in tags:
args.append(f"--tag={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=f"docker build {name}")
@contextmanager
def create(
self,
tag,
cmd_args=(),
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.
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.
name (str) - Optional name of the created container.
"""
self("pull", tag, step_name=f"pull docker image {tag}")
args = ["create"]
if name:
args.extend(["--name", name])
args.append(tag)
args.extend(cmd_args)
try:
container_id = self(
*args, stdout=self.m.raw_io.output_text()
).stdout.strip()
yield container_id
finally:
self(
"rm",
"-fv",
container_id,
step_name=f"remove {container_id} container",
)
def start(self, name, attach=False, **kwargs):
args = ["start", name]
if attach:
args.append("--attach")
return self(*args, **kwargs)
def exec(self, name, cmd_args=(), env=None, cwd=None, **kwargs):
"""Execute a command in a running container.
Args:
name (str): Name of the docker container.
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.
env (dict[str]str) : dict of env variables.
cwd (str): Path to the working directory for the container.
"""
args = ["exec", name]
if env:
for k, v in sorted((env or {}).items()):
args.extend(["--env", f"{k}={v}"])
if cwd:
args.extend(
[
"-w",
cwd,
]
)
args.extend(cmd_args)
return self(*args, **kwargs)
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(f"copy file from container {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", f"{name}:{item}", target_dir, step_name=f"copy {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",
f"type=bind,source={host_dir},destination={docker_dir}",
]
)
if env:
for k, v in sorted((env or {}).items()):
args.extend(["--env", f"{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",
f"{self._luci_context}:/tmp/luci_context:ro",
# 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)