| # 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) |