| # 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_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", step_name="docker cleanup") |
| |
| 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): Docker 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 {}).items()): |
| 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) |