# HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1698762595 -3600 # Tue Oct 31 15:29:55 2023 +0100 # Node ID 73e4d97e8d2312501020b8b5cc4e84c0fef4c852 # Parent 675704728b79365b8a9a53ff35af5d91f35facab ✨ Replace docker-py by python_on_whales diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,7 +53,7 @@ image: $TEMP_IMAGE script: - python3 -m pip install mypy types-PyYAML types-psycopg2 types-python-dateutil - types-requests build twine + types-requests build twine ".[docker]" - mypy odoo_scripts tests rules: - if: $CI_COMMIT_TAG == null diff --git a/NEWS.rst b/NEWS.rst --- a/NEWS.rst +++ b/NEWS.rst @@ -2,6 +2,13 @@ History ======= +21.0.0 +------ + +Use python_on_whales rather than Docker-py to use buildkit when building images. + +https://github.com/docker/docker-py/issues/2230 + 20.14.0 ------- diff --git a/odoo_scripts/docker_black.py b/odoo_scripts/docker_black.py --- a/odoo_scripts/docker_black.py +++ b/odoo_scripts/docker_black.py @@ -9,12 +9,12 @@ from .config import ODOO_11, Config from .docker_build import add_build_options, build_local_image, get_build_options -from .docker_client import DockerClient +from .docker_client import DockerClient, get_volumes from .parsing import apply, basic_parser -__version__ = "2.1.0" +__version__ = "2.2.0" __date__ = "2022-03-03" -__updated__ = "2023-03-31" +__updated__ = "2023-10-31" _logger = logging.getLogger(__name__) _BLACK_DEST = "black" @@ -68,21 +68,7 @@ build_local_image(build_options) pwd = os.environ["PWD"] - path = os.path.abspath(os.path.join(pwd, directory)) - volumes = { - pwd: { - "bind": pwd, - "mode": "rw" if write else "ro", - } - } - # also bind any symbolic link, but only for the given directory or its parent. - # all symbolic link could be scanned for but that might take too much time - for potential_link in (path, os.path.dirname(path)): - if os.path.islink(potential_link): - volumes[potential_link] = { - "bind": os.path.realpath(potential_link), - "mode": "rw" if write else "ro", - } + volumes, path = get_volumes(pwd, directory, write) command = ["black", "."] # The line was in a configuration file but not in modules in that Odoo 11 modules @@ -105,8 +91,7 @@ "user": os.getuid(), "command": ["-c", ";".join(command)], "volumes": volumes, - "working_dir": path, - "tty": True, + "workdir": path, }, False, ) diff --git a/odoo_scripts/docker_build.py b/odoo_scripts/docker_build.py --- a/odoo_scripts/docker_build.py +++ b/odoo_scripts/docker_build.py @@ -3,7 +3,6 @@ """ import argparse import datetime -import json import logging import os import signal @@ -11,19 +10,21 @@ from subprocess import call, check_output from typing import List, Optional +from python_on_whales import docker + from .config import ODOO_7, ODOO_8, ODOO_10, ODOO_11, ODOO_13, ODOO_14, ODOO_15, Config from .docker_build_clean import clean from .docker_build_copy import Configuration as CopyConfiguration from .docker_build_copy import add_build_copy_options, copy, get_build_copy_options -from .docker_client import DockerClient +from .docker_client import capture_docker_exit_code from .parsing import apply, basic_parser from .storage import get_orus_api_token _logger = logging.getLogger(__name__) -__version__ = "1.5.0" +__version__ = "1.6.0" __date__ = "2018-04-04" -__updated__ = "2023-10-17" +__updated__ = "2023-10-31" def add_build_options(parser: argparse.ArgumentParser): @@ -237,46 +238,26 @@ dockerfile = debug_dockerfile # TODO remove temp image - docker_client = DockerClient.client pull = not nmspc.no_pull _logger.debug("Docker Pull %s", pull) - builder = docker_client.api.build( - path=".", - rm=True, - pull=pull, - buildargs=buildargs, - tag=repository, - dockerfile=dockerfile, - ) - for line in builder: - d = json.loads(line.decode("utf-8")) - if "stream" in d: - for line in d["stream"].rstrip().split("\n"): - _logger.info(line) - if "errorDetail" in d: - _logger.fatal(d["errorDetail"]) - return 1 + full_tags = [repository] if nmspc.local else [f"{repository}:{tag}" for tag in tags] + with capture_docker_exit_code() as exit_code: + docker.image.build( + context_path=".", + pull=pull, + build_args=buildargs, + tags=full_tags, + file=dockerfile, + ) if dev: call(["rm", dockerfile]) - # retag - for tag in tags: - if tag != "tip": - _logger.info("Docker with tag %s", tag) - docker_client.images.get(repository).tag(repository, tag=tag) - if push: + if push and not exit_code: _logger.info("Docker push %s", repository) - for line in docker_client.images.push(repository, stream=True, decode=True): - _logger.debug(line) - for tag in tags: - _logger.info("Docker push %s:%s", repository, tag) - for line in docker_client.images.push( - repository, tag=tag, stream=True, decode=True - ): - _logger.debug(line) + docker.push(full_tags) # XXX call cleanup more intelligently signal_handler(0, None) - return 0 + return int(exit_code) def build_local_image(build_options: List[str], force: bool = False) -> int: @@ -287,7 +268,7 @@ c = Config() image = c.local_image_name - image_list = DockerClient.client.images.list(name=image) + image_list = docker.image.list(image) if not image_list: _logger.info("Image %s does not exist", image) else: diff --git a/odoo_scripts/docker_build_copy.py b/odoo_scripts/docker_build_copy.py --- a/odoo_scripts/docker_build_copy.py +++ b/odoo_scripts/docker_build_copy.py @@ -441,8 +441,7 @@ # TODO changer pour prendre la version du python du super # projet. Probablement pas renseigné où que ce soit. return [(package_name, tag.decode("UTF-8"))] - else: - _logger.debug("No tag found") + _logger.debug("No tag found") else: return [ ( diff --git a/odoo_scripts/docker_build_doc.py b/odoo_scripts/docker_build_doc.py --- a/odoo_scripts/docker_build_doc.py +++ b/odoo_scripts/docker_build_doc.py @@ -88,7 +88,7 @@ "user": os.getuid(), "command": ["html"], "volumes": volumes, - "working_dir": path, + "workdir": path, }, False, ) diff --git a/odoo_scripts/docker_client.py b/odoo_scripts/docker_client.py --- a/odoo_scripts/docker_client.py +++ b/odoo_scripts/docker_client.py @@ -1,17 +1,16 @@ """Class that encapsulate the docker client and provide common methods""" import atexit +import functools import logging import os import re -import tarfile +import sys from functools import partial from multiprocessing import Process -from typing import Any, Dict, List, Mapping, Optional, Pattern, Tuple +from typing import Any, Callable, Dict, List, Mapping, Optional, Pattern, Tuple, Union -import docker # type: ignore[import] -import dockerpty # type: ignore[import] -from docker.models.containers import Container # type: ignore[import] -from docker.types import Mount # type: ignore[import] +from python_on_whales import Container, Volume, docker +from python_on_whales.exceptions import DockerException from .config import ODOO_7, ODOO_8, ODOO_9, ODOO_10, Configuration @@ -19,16 +18,27 @@ # TODO detect that user is member of docker group +Mount = List[str] + + +def mount( + target: str, source: str, mount_type: Optional[str] = None, readonly: bool = False +) -> Mount: + result = [f"target={target}", f"source={source}"] + if mount_type is not None: + result.append(f"type={mount_type}") + if readonly: + result.append("readonly") + return result + class DockerClient: """Provide additional abstraction and extra methods of the docker client API""" - client = docker.from_env() - @classmethod def create_volume( cls, volume_name: str, extra_labels: Optional[Dict[str, str]] = None - ) -> Tuple[docker.models.volumes.Volume, bool]: + ) -> Tuple[Volume, bool]: """Return the volume passed in parameter, creating it if it does not exist. :param volume_name: name of the volume to create :param extra_labels: extra labels to put on the volume (only on @@ -36,7 +46,7 @@ :return: a tuple, the first one is the volume, the second is a boolean indicating that the volume has just been created (true if created) """ - volumes = cls.client.volumes.list(filters={"name": volume_name}) + volumes = docker.volume.list(filters={"name": volume_name}) if volumes: _logger.debug("Volume %s already exists", volume_name) return volumes[0], False @@ -44,28 +54,12 @@ labels = dict(odoo_scripts="") if extra_labels: labels.update(extra_labels) - return cls.client.volumes.create(name=volume_name, labels=labels), True - - @staticmethod - def put_file(src, container, dst): - """put a file into a container""" - # copied from https://stackoverflow.com/questions/46390309/how-to-copy-a-file-from-host-to-container-using-docker-py-docker-sdk # noqa: E501 # pylint: disable=line-too-long - os.chdir(os.path.dirname(src)) - srcname = os.path.basename(src) - # TODO put that in a temporary directory - tar = tarfile.open(src + ".tar", mode="w") - try: - tar.add(srcname) - finally: - tar.close() - - data = open(src + ".tar", "rb").read() - container.put_archive(os.path.dirname(dst), data) + return docker.volume.create(volume_name, labels=labels), True @classmethod def exec( cls, - container: docker.models.containers.Container, + container: Container, command: List[str], user: str, interactive: bool = False, @@ -77,24 +71,48 @@ :param user: User to execute command as. :param interactive: True if the container needs to be interactive """ - if interactive: - dockerpty.exec_command(cls.client.api, container.id, command) - # it would be nice to return the exec command return code, but - # there is no way to do that apparently - result = 0 - else: - run_result = container.exec_run(command, user=user) - result = run_result.exit_code - print(run_result.output.decode().rstrip()) - return result + with capture_docker_exit_code() as exit_code: + container.execute(command, user=user, interactive=interactive, tty=True) + return int(exit_code) @staticmethod - def _replace_then_print(replaces: List[Tuple[Pattern[str], str]], log_line: str): + def _replace_then_print( + replaces: List[Tuple[Pattern[str], str]], log_line: str, file=sys.stdout + ): # apply all the replacement of the path inside the image to the real path for pattern, value in replaces: log_line = pattern.sub(value, log_line) # print to play nicely with pycharm that will linkify the result - print(log_line) + print(log_line, file=file) + + @classmethod + def launch( + cls, + repository: str, + tag: str, + run_kwargs: Dict[str, Any], + pull: bool = True, + display_log: bool = True, + ) -> Container: + container, stop_remove_registered = cls._create( + repository, tag, run_kwargs, pull + ) + _logger.debug("Starting container %s", container.name) + container.start() + if display_log: + process = Process(target=DockerClient.print_log, args=(container,)) + process.start() + + def stop_remove_wait_process(): + stop_remove(container) + if process and process.is_alive(): + _logger.info("Waiting for log process to finish") + process.join() + + atexit.register(stop_remove_wait_process) + atexit.unregister(stop_remove_registered) + + return container @classmethod def run( @@ -104,14 +122,41 @@ run_kwargs: Dict[str, Any], pull: bool = True, target_source_dict: Optional[Mapping[str, str]] = None, - display_log: bool = True, - ): - """Run a container with the provided args""" + ) -> int: + container, stop_remove_registered = cls._create( + repository, tag, run_kwargs, pull + ) + _logger.debug("Starting container %s", container.name) + container.start() + + atexit.unregister(stop_remove_registered) + remove_registered = functools.partial(remove, container) + atexit.register(remove_registered) + + # TODO semble trop tard replaces: List[Tuple[Pattern[str], str]] = ( [(re.compile(key), value) for key, value in target_source_dict.items()] if target_source_dict else [] ) + cls.print_log(container, replaces) + + _logger.debug("Waiting for container %s", container.name) + exit_code = docker.wait(container) + _logger.debug("Removing container %s", container.name) + container.remove(volumes=True) + atexit.unregister(remove_registered) + return exit_code + + @classmethod + def _create( + cls, + repository: str, + tag: str, + run_kwargs: Dict[str, Any], + pull: bool = True, + ) -> Tuple[Container, Callable]: + """Run a container with the provided args""" image = f"{repository}:{tag}" @@ -119,13 +164,13 @@ _logger.debug("Pulling %s", image) # TODO find a better way to avoid error when pulling without a connection try: - cls.client.images.pull(repository, tag) - except docker.errors.APIError: + docker.image.pull(image) + except DockerException: _logger.warning( - "No connection to server, using existing version, if any" + "Issue when pulling, using existing version, if any", exc_info=True ) - container = None + container: Union[None, Container] = None # XXX When pressing ctrl-c during the container creating, the container # is not removed as there is no return of the value of the Container @@ -134,77 +179,15 @@ # - maybe use some kind of time frame to identify the container # - warn the user of a potential container leak by using signals - def remove(): - """Remove the container""" - if container: - _logger.debug("Removing container %s", container.name) - container.remove() - - def stop_remove(): - """Stop then remove the container.""" - if container: - _logger.debug("Stopping container %s", container.name) - container.stop() - _logger.debug("Waiting for container %s", container.name) - container.wait() - remove() - - atexit.register(stop_remove) + stop_remove_registered = functools.partial(stop_remove, container) + atexit.register(stop_remove_registered) # split create and start so in case of SIGTERM, container has fewer chances # of being None - kwargs = dict(run_kwargs, detach=True) _logger.debug("Create container with image %s", image) - container = cls.client.containers.create(image, **kwargs) + container = docker.container.create(image, **run_kwargs) _logger.debug("Created container %s", container.name) - _logger.debug("Starting container %s", container.name) - container.start() - if run_kwargs.get("detach", False): - if display_log: - process = Process(target=DockerClient.print_log, args=(container,)) - process.start() - - def stop_remove_wait_process(): - stop_remove() - if process and process.is_alive(): - _logger.info("Waiting for log process to finish") - process.join() - - atexit.register(stop_remove_wait_process) - atexit.unregister(stop_remove) - - return container - - atexit.unregister(stop_remove) - atexit.register(remove) - - if kwargs.get("tty", False): - line = b"" - for log in container.logs(stream=True, follow=True): - # if there is a tty, send byte by byte, so add them together before - # printing - # works correctly because prettier sends \r\n on each line - if log == b"\r": - cls._replace_then_print(replaces, line.decode()) - line = b"" - elif log == b"\n": - pass - else: - line += log - if line: - cls._replace_then_print(replaces, line.decode()) - else: - cls.print_log(container, replaces) - - _logger.debug("Waiting for container %s", container.name) - result = container.wait() - - if result.get("Error", None): - _logger.warning(result.decode()) - _logger.debug("Removing container %s", container.name) - container.remove(v=True) - atexit.unregister(remove) - return result["StatusCode"] + return container, stop_remove_registered @classmethod def print_log( @@ -215,22 +198,25 @@ """Print the logs of a container, eventually replacing some of its output.""" if replaces is None: replaces = [] - for log in container.logs(stream=True, follow=True): - # \r is needed when using a process that also outputs the logs - log = log.decode().rstrip() + "\r" - cls._replace_then_print(replaces, log) + logs = docker.container.logs(container, follow=True, stream=True) + if logs is not None and not isinstance(logs, str): + for channel, log in logs: + # \r is needed when using a process that also outputs the logs + log_str = log.decode().rstrip() + "\r" + cls._replace_then_print(replaces, log_str, getattr(sys, channel)) def get_odoo_base_path(config: Configuration) -> str: """Return value of env ODOO_BASE_PATH in the local image name.""" - image = DockerClient.client.images.list(config.local_image_name) + image = docker.image.list(config.local_image_name) if not image: raise Exception( "Image %s is not built, needed to be inspected", config.local_image_name ) - for env in image[0].attrs["Config"]["Env"]: - if env.startswith("ODOO_BASE_PATH="): - return env[15:] + if isinstance(image[0].config.env, list): + for env in image[0].config.env: + if env.startswith("ODOO_BASE_PATH="): + return env[15:] raise Exception("Missing ODOO_BASE_PATH in the image") @@ -251,7 +237,7 @@ for module in modules: full_target_path = os.path.join(target_path, os.path.basename(module)) full_project_path = os.path.realpath(os.path.join(project_path, module)) - mount_list.append(Mount(full_target_path, full_project_path, "bind")) + mount_list.append(mount(full_target_path, full_project_path, "bind")) mount_dict[full_target_path] = full_project_path _logger.debug("python packages: %s", ",".join(config.python_packages)) @@ -261,7 +247,7 @@ full_project_path = os.path.realpath( os.path.join(project_path, python_package[0], dir_[0]) ) - mount_list.append(Mount(full_target_path, full_project_path, "bind")) + mount_list.append(mount(full_target_path, full_project_path, "bind")) mount_dict[full_target_path] = full_project_path return mount_list, mount_dict @@ -281,12 +267,12 @@ for root, dirs, _files in os.walk("songs"): for directory in dirs: mounts.append( - Mount( + mount( os.path.dirname(get_odoo_base_path(config)) + f"/{root}/{directory}", os.path.join(project_path, root, directory), "bind", - read_only=True, + True, ) ) _logger.debug("mounts len: %s", len(mounts)) @@ -298,7 +284,7 @@ mounts: List[Mount] = [] full_source_path = os.path.realpath(source_path) if config.odoo_type in (ODOO_7, ODOO_8): - mounts.append(Mount("/opt/odoo/sources/odoo", source_path, "bind")) + mounts.append(mount("/opt/odoo/sources/odoo", source_path, "bind")) else: install_path = ( "/usr/local/lib/python2.7/dist-packages/odoo" @@ -308,7 +294,7 @@ for entry in os.listdir(os.path.join(full_source_path, "odoo")): if entry != "addons": mounts.append( - Mount( + mount( install_path + "/" + entry, os.path.join(full_source_path, "odoo", entry), "bind", @@ -317,7 +303,7 @@ for entry in os.listdir(os.path.join(full_source_path, "odoo", "addons")): # for directory in dirs: mounts.append( - Mount( + mount( install_path + "/addons/" + entry, os.path.join(full_source_path, "odoo", "addons", entry), "bind", @@ -326,7 +312,7 @@ # iterate on all the modules, avoids for entry in os.listdir(os.path.join(full_source_path, "addons")): mounts.append( - Mount( + mount( install_path + "/addons/" + entry, os.path.join(full_source_path, "addons", entry), "bind", @@ -340,7 +326,6 @@ def __init__( self, - docker_client: docker.client.DockerClient, name: str, repository: str, tag: str, @@ -352,7 +337,6 @@ force_pull: bool = False, ): """ - :param docker_client: instance of docker client :param name: name for the container :param repository: repository to use for the image :param tag: repository tag to use for the image @@ -360,7 +344,6 @@ :param stream: if not attached, indicate to stream output or not """ super().__init__() - self.docker_cli = docker_client self.name = name self.repository: str = repository self.tag = tag @@ -371,11 +354,9 @@ self.stream = stream self.stop_at_exit = stop_at_exit # Retrieve the container if it already exists - self._container: Optional[docker.models.containers.Container] - try: - self._container = self.docker_cli.containers.get(self.name) - except docker.errors.NotFound: - self._container = None + self._container: Optional[Container] = None + if docker.container.exists(self.name): + self._container = docker.container.list(filters={"name": self.name})[0] self._log_process: Optional[Process] = None self.stop_remove_if_exists = stop_remove_if_exists self.force_pull: bool = force_pull @@ -398,7 +379,7 @@ self._start() if self.stop_at_exit: atexit.register(partial(self.stop_and_remove, False)) - if self.attach: + if self.attach and self._container is not None: DockerClient.print_log(self._container) elif self.stream: process = Process(target=DockerClient.print_log, args=(self._container,)) @@ -415,7 +396,7 @@ ) def _start(self) -> None: - self._container = self.docker_cli.containers.create(**self._create_param()) + self._container = docker.container.create(**self._create_param()) _logger.info("Starting container %s", self.name) self._container.start() @@ -424,7 +405,7 @@ try: self.stop(expect_running) # call stop func self.remove() # call remove func - except docker.errors.NotFound as exception: + except DockerException as exception: _logger.info("Stop and remove container, got: %s", exception) raise exception @@ -437,8 +418,8 @@ elif self._container is not None: _logger.info("Stopping %s container", self.name) self._container.stop() - self._container.wait() - except docker.errors.NotFound as exception: + docker.container.wait(self._container) + except DockerException as exception: _logger.error("Stopping container %s, got: %s", self.name, exception) raise self.join_log_process() @@ -466,9 +447,9 @@ def pull_image(self): """Pulls image if needed or forced.""" - if self.force_pull or not self.docker_cli.images.list(self.base_image): + if self.force_pull or not docker.image.list(self.base_image): _logger.info("Pulling %s", self.base_image) - self.docker_cli.images.pull(repository=self.repository, tag=self.tag) + docker.image.pull(repository=self.repository, tag=self.tag) @property def is_running(self) -> bool: @@ -477,8 +458,7 @@ """ if self._container is None: return False - result = self.docker_cli.containers.list(filters={"id": self._container.id}) - return result and result[0].status == "running" + return bool(self._container.state.running) @property def is_exited(self) -> bool: @@ -488,25 +468,86 @@ """ if self._container is None: return True - result = self.docker_cli.containers.list(filters={"id": self._container.id}) - return result and result[0].status == "exited" + return self._container.state.status in [None, "exited"] def get_volumes(base_dir, directory: str, write: bool = False): """return volumes to bind""" path = os.path.abspath(os.path.join(base_dir, directory)) - volumes = { - base_dir: { - "bind": base_dir, - "mode": "rw" if write else "ro", - } - } + volumes = [ + ( + base_dir, + base_dir, + "rw" if write else "ro", + ) + ] # also bind any symbolic link, but only for the given directory or its parent. # all symbolic link could be scanned for but that might take too much time for potential_link in (path, os.path.dirname(path)): if os.path.islink(potential_link): - volumes[potential_link] = { - "bind": os.path.realpath(potential_link), - "mode": "rw" if write else "ro", - } + volumes.append( + ( + potential_link, + os.path.realpath(potential_link), + "rw" if write else "ro", + ) + ) return volumes, path + + +def remove(container: Union[None, Container]): + """Remove the container""" + if container: + _logger.debug("Removing container %s", container.name) + container.remove() + + +def stop_remove(container: Union[None, Container]): + """Stop then remove the container.""" + if container: + _logger.debug("Stopping container %s", container.name) + container.stop() + _logger.debug("Waiting for container %s", container.name) + docker.wait(container) + remove(container) + + +class _MutableInt: + """Class with an int. + Used in context manager to provide exit status.""" + + def __init__(self): + self.value = 0 + + def set(self, value: int): + """Update the value""" + self.value = value + + def __int__(self) -> int: + return self.value + + def __bool__(self): + return bool(self.value) + + +class capture_docker_exit_code: + """Context manager to get error code when using docker::run""" + + def __init__(self, print_exc: bool = True): + self.exit_code = _MutableInt() + self.print_exc = print_exc + + def __enter__(self): + return self.exit_code + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type == DockerException: + self.exit_code = exc_val.return_code + if self.print_exc: + if exc_val.stdout: + print(exc_val.stdout) + if exc_val.stderr: + print(exc_val.stderr, file=sys.stderr) + # Need to return a true value to ignore the error + return True + return False diff --git a/odoo_scripts/docker_dev_start.py b/odoo_scripts/docker_dev_start.py --- a/odoo_scripts/docker_dev_start.py +++ b/odoo_scripts/docker_dev_start.py @@ -15,11 +15,9 @@ from subprocess import call, check_output from typing import Any, Dict, List, Optional -import dockerpty # type: ignore[import] -from docker.errors import APIError # type: ignore[import] -from docker.types import Mount # type: ignore[import] from psycopg2 import OperationalError, connect from psycopg2.errors import DuplicateDatabase, DuplicateObject +from python_on_whales import docker from .config import ( ODOO_7, @@ -37,8 +35,11 @@ from .docker_build import add_build_options, build_local_image, get_build_options from .docker_client import ( DockerClient, + Mount, anthem_mounts, + capture_docker_exit_code, modules_mount, + mount, odoo_source_mounts, ) from .docker_flake8 import apply_flake8, parser_add_flake8_group @@ -54,9 +55,9 @@ _logger = logging.getLogger(__name__) -__version__ = "3.14.0" +__version__ = "3.15.0" __date__ = "2017-08-11" -__updated__ = "2023-07-21" +__updated__ = "2023-10-31" def __parser(project_name: str) -> ArgumentParser: @@ -421,7 +422,6 @@ type=int, ) - # workers options parser.add_argument( "--activate-s3-storage", help="Activate S3 storage using the provided bucket as the main one [default: " @@ -536,7 +536,6 @@ _logger.debug("Docker image: %s", image) # detect if docker image already exists - client = DockerClient.client if not nmspc.no_build: failed = build_local_image(get_build_options(nmspc), nmspc.force_build) if failed: @@ -545,13 +544,12 @@ # options is only used with subprocess call options: Dict[str, Any] = { "name": project_name, - "tty": True, - "extra_hosts": {}, - "ports": {}, - "environment": {}, + "add_hosts": {}, + "publish": [], + "envs": {}, } if activate_s3_storage: - options["environment"].update( + options["envs"].update( { "s3AttachmentStorageActivate": "true", "s3AttachmentStorageServerBucket": activate_s3_storage, @@ -563,7 +561,7 @@ # TODO detect that from configuration or force it odoo_run_port = 8069 if not gevent and not use_host_network: - options["ports"][exposed_port] = odoo_run_port + options["publish"].append((exposed_port, odoo_run_port)) mounts: List[Mount] = [] """Mounts for Odoo docker""" tested_modules: str = "" @@ -656,7 +654,7 @@ # auto detect local ip if use_host_network: local_ip = "127.0.0.1" - options["network_mode"] = "host" + options["networks"] = "host" else: local_ip = None try: @@ -696,10 +694,10 @@ else: odoo_conf_file = "/etc/opt/odoo/odoo.conf" # change ODOO_RC for version<ODOO_16 - options["environment"]["ODOO_RC"] = odoo_conf_file + options["envs"]["ODOO_RC"] = odoo_conf_file mounts.append( - Mount( + mount( odoo_conf_file, os.path.join(project_path, local_conf_path), "bind", @@ -719,7 +717,7 @@ and cp_local.get("options", "redis_host") ) or None if redis_host: - options["extra_hosts"][redis_host] = local_ip + options["add_hosts"][redis_host] = local_ip else: _logger.info("No configuration file at: %s", local_conf_path) # default values if nothing else @@ -746,8 +744,8 @@ # this path is in odoo code, the last part is the system user session_volume_name = "{}{}".format(base_data_volume_name, "_sessions") target = "/tmp/oe-sessions-{}".format("odoo") - client.create_volume(session_volume_name, {"mounted in": target}) - mounts.append(Mount(target, session_volume_name)) + docker.volume.create(session_volume_name, labels={"mount-location": target}) + mounts.append(mount(target, session_volume_name)) # by default odoo store attachments in database but it can be set on # disk. mount the volume in all cases. # XXX data is the path set in the database (in the form file:///data) @@ -759,15 +757,15 @@ "openerp-7.0-py2.7.egg/openerp/data" ) filestore_volume_name = "{}_filestore".format(base_data_volume_name) - client.create_volume(filestore_volume_name, {"mounted in": target}) - mounts.append(Mount(target, filestore_volume_name)) + docker.volume.create(filestore_volume_name, labels={"mount-location": target}) + mounts.append(mount(target, filestore_volume_name)) else: data_volume_path = "/var/lib/data" - mounts.append(Mount(data_volume_path, base_data_volume_name)) + mounts.append(mount(data_volume_path, base_data_volume_name)) _, created = DockerClient.create_volume( base_data_volume_name, { - "mounted in": data_volume_path, + "mount-location": data_volume_path, }, ) if created: @@ -777,52 +775,13 @@ if run_chown: chown_directories.add(data_volume_path) - # avoid the duplication of unbind volumes with all addons - if odoo_type == ODOO_7: - extra_volumes = [ - "/opt/odoo/additional_addons", - "/opt/odoo/var", - # not used but declared as a mount point in Dockerfile :-( - "/opt/odoo/data", - ] - elif odoo_type == ODOO_8: - extra_volumes = [ - "/opt/odoo/additional_addons", - "/opt/odoo/var", - "/opt/odoo/data", - ] - elif odoo_type == ODOO_10: - extra_volumes = [ - "/opt/odoo/additional_addons", - "/opt/odoo/var", - "/opt/odoo/data", - ] - elif odoo_type in (ODOO_11, ODOO_12, ODOO_13, ODOO_14): - extra_volumes = ["/var/lib/odoo"] - else: - extra_volumes = ["/var/opt/odoo"] - for extra_volume in extra_volumes: - volume_name = "{}_{}".format(project_name, extra_volume.replace("/", "_")) - _volume, created = DockerClient.create_volume( - volume_name, - { - "mounted in": extra_volume, - "comment": "only used to avoid creating too many volumes", - }, - ) - mounts.append(Mount(extra_volume, volume_name)) - if created: - chown_directories.add(extra_volume) - # make sure the permission in the volumes are correct if run_chown: - for extra_volume in extra_volumes: - chown_directories.add(extra_volume) chown_directories.add("/mnt") if chown_directories: _logger.info("chown container’s %s to Odoo user", ", ".join(chown_directories)) - client.containers.run( + docker.run( image, command=["odoo", "--recursive"] + list(chown_directories), remove=True, @@ -834,8 +793,8 @@ if start_py3o_stack: start_py3o() # provide a couple of handy dns name for py3o - options["extra_hosts"]["py3o"] = local_ip - options["extra_hosts"]["py3o-fusion-server"] = local_ip + options["add_hosts"]["py3o"] = local_ip + options["add_hosts"]["py3o-fusion-server"] = local_ip # Database connection @@ -864,7 +823,8 @@ pg, _stop_postgresql, socket_path = docker_run_postgresql( project_name, postgresql_version, host_pg_port=None ) - mounts.append(Mount(db_host, socket_path, "bind")) + if socket_path is not None: + mounts.append(mount(db_host, socket_path, "bind")) _logger.info("Testing database connection on base postgres") try: if start_postgresql: @@ -875,7 +835,7 @@ host=socket_path, port=dbport, ) - _logger.info("Connection successful") + _logger.info("DB connection successful") else: # psql uses PGHOST if provided and a socket otherwise if db_host: @@ -890,7 +850,7 @@ db_host = local_ip if db_host.startswith("/"): # mount the path inside the docker - mounts.append(Mount(db_host, db_host, "bind")) + mounts.append(mount(db_host, db_host, "bind")) connect( user=user, password=password, @@ -908,7 +868,7 @@ connection = connect( user="postgres", database="postgres", - host=db_host, + host=socket_path, port=5432, password=POSTGRES_PASSWORD, ) @@ -917,6 +877,7 @@ # use socket to create user # TODO test if removing more arguments works connection = connect(user=loginname, database="postgres") + _logger.info("User created") with connection.cursor() as cursor: # not injection safe but you are on your own machine # with already full access to db @@ -958,30 +919,32 @@ # TODO find out why odoo_help would stop us from restoring if start_postgresql and not odoo_help and (restore_filename or create_database): _logger.info("Creating database %s for %s", database, user) - pg.exec_run(["createdb", "-U", user, database]) + pg.execute(["createdb", "-U", user, database]) if start_postgresql and not odoo_help and restore_filename: restore_basename = os.path.basename(restore_filename) _logger.info("Copying dump file in docker") inside_restore_filename = "/tmp/{}".format(restore_basename) - DockerClient.put_file(restore_filename, pg, inside_restore_filename) + docker.copy(restore_filename, (pg, inside_restore_filename)) _logger.info("Restoring database %s", database) - restore, _output = pg.exec_run( - [ - "pg_restore", - "-U", - user, - "--no-owner", - "--no-acl", - "--job=4", - "--dbname", - database, - inside_restore_filename, - ] - ) - if not restore: + with capture_docker_exit_code() as exit_code: + pg.execute( + [ + "pg_restore", + "-U", + user, + "--no-owner", + "--no-acl", + "--job=4", + "--dbname", + database, + inside_restore_filename, + ] + ) + if int(exit_code): + _logger.fatal("Error when restoring database") return 15 _logger.info("Removing dump file in docker") - pg.exec_run(["rm", inside_restore_filename]) + pg.execute(["rm", inside_restore_filename]) if not start_postgresql and not odoo_help and (restore_filename or create_database): connection = connect( @@ -1028,29 +991,30 @@ pre_sql_script_basename = os.path.basename(pre_sql_script) _logger.info("Copying SQL script %s in docker", pre_sql_script_basename) inside_pre_sql_script = "/tmp/{}".format(pre_sql_script_basename) - client.put_file(pre_sql_script, pg, inside_pre_sql_script) + docker.copy(pre_sql_script, (pg, inside_pre_sql_script)) _logger.info( "Running SQL script %s in %s", pre_sql_script_basename, database, ) - exit_code, _output = pg.exec_run( - [ - "psql", - "-U", - user, - "-d", - database, - "-f", - inside_pre_sql_script, - ] - ) - if exit_code > 0: + with capture_docker_exit_code() as exit_code: + pg.execute( + [ + "psql", + "-U", + user, + "-d", + database, + "-f", + inside_pre_sql_script, + ] + ) + if int(exit_code) > 0: _logger.fatal("Error when running SQL script %s", pre_sql_script) return 16 _logger.info("Removing SQL script %s in docker", pre_sql_script) # remove script - pg.exec_run(["rm", inside_pre_sql_script]) + pg.execute(["rm", inside_pre_sql_script]) else: _logger.info("Running SQL script %s on %s", pre_sql_script, database) env = { @@ -1124,7 +1088,6 @@ ) return 81 redner_docker = DockerRednerd( - DockerClient.client, project_name, config.rednerd_repository, db_user=redner_db_user, @@ -1141,11 +1104,11 @@ ) redner_docker.start() # only useful when using port rather than socket. Not used at the moment - options["extra_hosts"]["redner"] = local_ip + options["add_hosts"]["redner"] = local_ip # bind the socket dir in Odoo if hasattr(redner_docker, "socket_dir"): mounts.append( - Mount(redner_docker.socket_dir, redner_docker.socket_dir, type="bind") + mount(redner_docker.socket_dir, redner_docker.socket_dir, "bind") ) # --- Rednerd change config parameters in Odoo database --- @@ -1177,13 +1140,11 @@ redis_docker: Optional[DockerRedis] = None if start_redis: # TODO stop at exit à vrai sinon s’éteint pas? - redis_docker = DockerRedis(DockerClient.client, project_name, attach=False) + redis_docker = DockerRedis(project_name, attach=False) redis_docker.start() # bind the socket dir in Odoo - mounts.append( - Mount(redis_docker.socket_dir, redis_docker.socket_dir, type="bind") - ) - options["environment"].update( + mounts.append(mount(redis_docker.socket_dir, redis_docker.socket_dir, "bind")) + options["envs"].update( # Variables used by flanker. REDIS_PORT="0", REDIS_DB="0", @@ -1231,7 +1192,7 @@ if force_python_dev_mode or ( config.pythondevmode and not force_no_python_dev_mode ): - options["environment"]["PYTHONDEVMODE"] = 1 + options["envs"]["PYTHONDEVMODE"] = 1 if test_tags: arg.extend(("--test-tags", test_tags)) @@ -1257,9 +1218,9 @@ if os.path.exists(".git"): project_version = check_output(["git", "describe"]).decode() - options["environment"]["VERSION_PACKAGE"] = "" - options["environment"]["SENTRY_RELEASE"] = project_version - options["environment"]["VERSION"] = project_version + options["envs"]["VERSION_PACKAGE"] = "" + options["envs"]["SENTRY_RELEASE"] = project_version + options["envs"]["VERSION"] = project_version if odoo_help: arg.append("--help") @@ -1271,13 +1232,13 @@ # avoid chowning at each run, faster but can cause trouble # newer odoo images does not use this environment variable anymore - options["environment"]["ODOO_CHOWN"] = "false" + options["envs"]["ODOO_CHOWN"] = "false" if coverage: source_files = ",".join( [f"odoo.addons.{module}" for module in tested_modules.split(",")] ) - options["environment"]["COVERAGE_RUN_OPTIONS"] = ( + options["envs"]["COVERAGE_RUN_OPTIONS"] = ( "--source=" + source_files + " --omit=*/__manifest__.py --branch" ) @@ -1294,16 +1255,16 @@ marabunta_container_path = "/etc/marabunta.yaml" # add marabunta file to mounts mounts.append( - Mount( + mount( marabunta_container_path, os.path.join(project_path, marabunta_migration_file), "bind", - read_only=True, + True, ) ) mounts.extend(anthem_mounts(config)) - options["environment"].update( + options["envs"].update( { "MARABUNTA_MIGRATION_FILE": marabunta_container_path, "MARABUNTA_MODE": "local", @@ -1311,29 +1272,27 @@ } ) if start_postgresql: - options["environment"]["MARABUNTA_DB_USER"] = ( - marabunta_db_user or "postgres" - ) + # TODO à tester, je pense que postgres ne se connecte pas + options["envs"]["MARABUNTA_DB_USER"] = marabunta_db_user or "postgres" elif marabunta_db_user: # only set if provided a value, otherwise the image will get the value from # command line arguments or the configuration file (using the same user os # the one used in odoo) - options["environment"]["MARABUNTA_DB_USER"] = marabunta_db_user + options["envs"]["MARABUNTA_DB_USER"] = marabunta_db_user # marabunta is run inside the container by the start command else: # Only set to avoid problems with image that inherit another image using # marabunta while not using marabunta themselves - options["environment"]["MARABUNTA_MIGRATION_FILE"] = "" + options["envs"]["MARABUNTA_MIGRATION_FILE"] = "" if marabunta_force_version: - options["environment"]["MARABUNTA_FORCE_VERSION"] = marabunta_force_version + options["envs"]["MARABUNTA_FORCE_VERSION"] = marabunta_force_version if marabunta_db_password: - options["environment"]["MARABUNTA_DB_PASSWORD"] = marabunta_db_password + options["envs"]["MARABUNTA_DB_PASSWORD"] = marabunta_db_password if gevent: gevent_port = find_free_port() run_kwargs: Dict[str, Any] = { "command": ["caddy", "run", "--config", "/etc/caddy/Caddyfile"], - "detach": True, "name": f"{project_name}_caddy", "mounts": [], } @@ -1343,7 +1302,7 @@ # change the port odoo runs on # TODO hopefully does not return 8069/odoo_run_port odoo_run_port = find_free_port() - options["environment"]["MARABUNTA_WEB_PORT"] = str(odoo_run_port) + options["envs"]["MARABUNTA_WEB_PORT"] = str(odoo_run_port) else: caddy_port = "2019" # TODO pre odoo 13: check if http-port should be changed to xmlrpc-port @@ -1394,19 +1353,12 @@ atexit.register(cleanup_caddyfile) run_kwargs["mounts"].append( - Mount("/etc/caddy/Caddyfile", caddyfile_tmp.name, type="bind") + mount("/etc/caddy/Caddyfile", caddyfile_tmp.name, "bind") ) if use_host_network: - run_kwargs["network_mode"] = "host" + run_kwargs["networks"] = "host" else: - try: - client.networks.create( - name=project_name, driver="bridge", check_duplicate=True - ) - except APIError: - # TODO handle that more correctly - # probably already exists - pass + docker.network.create(name=project_name, driver="bridge") run_kwargs.update( { "network": project_name, @@ -1423,34 +1375,24 @@ # so maybe start it after Odoo started? (only happens if odoo windows are # opened) # TODO also error when Odoo is powered off - DockerClient.run("caddy", "latest", run_kwargs) + DockerClient.launch("caddy", "latest", run_kwargs) - interactive = True - if interactive: - options["stdin_open"] = True - - odoo_container = client.containers.create( - image, command=arg, mounts=mounts, **options - ) _logger.info("Starting Odoo") - if interactive: - dockerpty.start(client.api, odoo_container.id) - # already stopped but this is the easiest way to get the return code - result_dict = odoo_container.wait() - result = result_dict["StatusCode"] - else: - # XXX untested - # that might need a way to pass any signal to the container - # probably prevent the caddy start bellow - result = odoo_container.start() - - # only remove after getting the return code - odoo_container.remove() + with capture_docker_exit_code() as exit_code: + docker.container.run( + image, + command=arg, + mounts=mounts, + **options, + tty=True, + interactive=True, + remove=True, + ) # Stop redis (python does not exit as the log process is still running) if redis_docker: _logger.info("Stopping redis") redis_docker.stop() - return result + return int(exit_code) def find_free_port(): diff --git a/odoo_scripts/docker_flake8.py b/odoo_scripts/docker_flake8.py --- a/odoo_scripts/docker_flake8.py +++ b/odoo_scripts/docker_flake8.py @@ -100,7 +100,7 @@ "entrypoint": "flake8", "command": ["/mnt"], "volumes": {pwd: {"bind": "/mnt", "mode": "ro"}}, - "working_dir": "/mnt", + "workdir": "/mnt", }, pull, {"/mnt": pwd}, diff --git a/odoo_scripts/docker_isort.py b/odoo_scripts/docker_isort.py --- a/odoo_scripts/docker_isort.py +++ b/odoo_scripts/docker_isort.py @@ -179,7 +179,7 @@ "user": os.getuid(), "command": command, "volumes": volumes, - "working_dir": path, + "workdir": path, }, pull, ) diff --git a/odoo_scripts/docker_postgresql.py b/odoo_scripts/docker_postgresql.py --- a/odoo_scripts/docker_postgresql.py +++ b/odoo_scripts/docker_postgresql.py @@ -11,17 +11,18 @@ from functools import partial from typing import Callable, List, Optional, Tuple -import docker # type: ignore[import] +from python_on_whales import Container, docker +from python_on_whales.exceptions import NoSuchContainer from .config import Config -from .docker_client import DockerClient +from .docker_client import DockerClient, Mount, mount from .parsing import apply, basic_parser _logger = logging.getLogger(__name__) -__version__ = "1.0.1" +__version__ = "1.1.0" __date__ = "2020-11-09" -__updated__ = "2022-12-16" +__updated__ = "2023-10-31" POSTGRES_PASSWORD = "this-could-be-anything" PSQL = "psql" @@ -33,9 +34,9 @@ def docker_run_postgresql( project_name: str, postgresql_version: str, - host_pg_port=None, + host_pg_port: Optional[int] = None, stop_at_exit: bool = True, -) -> Tuple[docker.models.containers.Container, Callable, Optional[str]]: +) -> Tuple[Container, Callable, Optional[str]]: """ :param project_name: @@ -45,60 +46,63 @@ :param stop_at_exit: True to stop the container at exit :return: container, method to call to stop it, socket directory """ - docker_client = DockerClient.client pg_repository = "postgres" version = "{}-alpine".format(postgresql_version) pg_image = "{}:{}".format(pg_repository, version) name = "postgresql-{}-{}".format(postgresql_version, project_name) # need to pull otherwise the containers.create might fail - docker_client.images.pull(repository=pg_repository, tag=version) + docker.image.pull(f"{pg_repository}:{version}") pg_data_volume_name = "postgresql_{}-{}".format(postgresql_version, project_name) path: Optional[str] = None env = {"POSTGRES_PASSWORD": POSTGRES_PASSWORD} - def stop_postgresql(pg_container: docker.models.containers.Container): + def stop_postgresql(pg_container: Container): # TODO test if still exists # maybe by looking at pg.status # TODO factorized with other methods that start dockers try: _logger.info("Stopping postgresql %s", pg_container.name) pg_container.stop() - pg_container.wait() + docker.wait(pg_container) _logger.info("Removing postgresql container %s", pg_container.name) pg_container.remove() - except docker.errors.NotFound: + except NoSuchContainer: _logger.info("Postgresql container %s already stopped", pg_container.name) # crash because docker or pg changes the owner of the directory # temp_directory.cleanup() - for container in docker_client.containers.list(): + for container in docker.container.list(): if name == container.name: _logger.info("Postgresql Container already running") - for mount_dict in container.attrs["Mounts"]: - if mount_dict["Destination"] == "/var/run/postgresql": - path = str(mount_dict["Source"]) + for mount_ in container.mounts: + if mount_.destination == "/var/run/postgresql": + path = str(mount_.source) return container, partial(stop_postgresql, container), path - volumes = { - pg_data_volume_name: { - "bind": "/var/lib/postgresql/data", - "mode": "rw", - } - } + mounts: List[Mount] = [ + mount( + "/var/lib/postgresql/data", + pg_data_volume_name, + "volume", + ) + ] - port_bindings = {} - if host_pg_port: - port_bindings[5432] = host_pg_port + port_bindings: List[ + Tuple[str | int, str | int] | Tuple[str | int, str | int, str] + ] = [] + if isinstance(host_pg_port, int): + port_bindings.append((5432, host_pg_port)) else: path = tempfile.TemporaryDirectory(prefix="odoo_scripts_postgres_socket-").name - volumes[path] = {"bind": "/var/run/postgresql", "mode": "rw"} + os.makedirs(path) + mounts.append(mount("/var/run/postgresql", path, "bind")) _logger.debug("Creating postgresql container") - pg = docker_client.containers.create( + pg = docker.container.create( pg_image, - volumes=volumes, - ports=port_bindings, + mounts=mounts, + publish=port_bindings, name=name, - environment=env, + envs=env, ) _logger.info("Starting postgresql container %s", pg.name) diff --git a/odoo_scripts/docker_prettier.py b/odoo_scripts/docker_prettier.py --- a/odoo_scripts/docker_prettier.py +++ b/odoo_scripts/docker_prettier.py @@ -97,8 +97,7 @@ "user": os.getuid(), "command": command, "volumes": volumes, - "working_dir": path, - "tty": True, + "workdir": path, }, pull=False, ) diff --git a/odoo_scripts/docker_py3o.py b/odoo_scripts/docker_py3o.py --- a/odoo_scripts/docker_py3o.py +++ b/odoo_scripts/docker_py3o.py @@ -3,9 +3,9 @@ import atexit import logging -from docker.errors import APIError # type: ignore[import] +from python_on_whales import docker -from .docker_client import DockerClient +from .docker_client import capture_docker_exit_code _logger = logging.getLogger(__name__) @@ -13,9 +13,8 @@ # TODO move to docker_client directly def find_container(name): """Return container object from its name""" - for container in DockerClient.client.containers(): - if name == container.names: - return container + for container in docker.container.list(filters={"name": name}): + return container return None @@ -28,25 +27,22 @@ def start_py3o(host_fusion_port=8765, stop_at_exit=True): """start a py3o container""" - docker_client = DockerClient.client fusion_repository = "xcgd/py3o" fusion_version = "1.0.0" fusion_name = "py3o_fusion" fusion_image = "{}:{}".format(fusion_repository, fusion_version) - try: + with capture_docker_exit_code() as exit_code: _logger.debug("Pulling %s", fusion_repository) - docker_client.images.pull(repository=fusion_repository, tag=fusion_version) - except APIError as e: - _logger.warning("Exception when trying to pull: %s", e) - if any( - fusion_name == container.name for container in docker_client.containers.list() - ): + docker.images.pull(f"{fusion_repository}:{fusion_version}") + if exit_code: + _logger.warning("Error when pulling") + if docker.containers.list(filter={"name": fusion_name}): _logger.debug("%s Container already running", fusion_name) - remove_and_stop(docker_client, fusion_name) + remove_and_stop(docker, fusion_name) # TODO handle --host-network option port_bindings = {8765: host_fusion_port} _logger.debug("Starting %s container", fusion_name) - fusion = docker_client.containers.create( + fusion = docker.containers.create( image=fusion_image, ports=port_bindings, name=fusion_name ) _logger.debug("Starting %s container", fusion_name) diff --git a/odoo_scripts/docker_pylint.py b/odoo_scripts/docker_pylint.py --- a/odoo_scripts/docker_pylint.py +++ b/odoo_scripts/docker_pylint.py @@ -6,11 +6,9 @@ import sys from typing import Dict, List, Optional -from docker.types import Mount # type: ignore[import] - from .config import Config from .docker_build import add_build_options, build_local_image, get_build_options -from .docker_client import DockerClient, anthem_mounts, modules_mount +from .docker_client import DockerClient, Mount, anthem_mounts, modules_mount, mount from .parsing import apply, basic_parser __version__ = "1.2.0" @@ -96,7 +94,7 @@ cache_volume_name = "pylint-cache" DockerClient.create_volume(cache_volume_name) cache_target_path = "/var/cache/pylint" - mounts.append(Mount(cache_target_path, cache_volume_name)) + mounts.append(mount(cache_target_path, cache_volume_name)) environment["PYLINTHOME"] = cache_target_path mounts.extend(anthem_mounts(config)) @@ -113,7 +111,7 @@ # needed to write in the cache "user": "root", "mounts": mounts, - "environment": environment, + "envs": environment, }, False, target_source_dict, diff --git a/odoo_scripts/docker_redis.py b/odoo_scripts/docker_redis.py --- a/odoo_scripts/docker_redis.py +++ b/odoo_scripts/docker_redis.py @@ -9,14 +9,12 @@ import time from typing import List, Optional -from docker.types import Mount # type: ignore[import] - -from .docker_client import DockerClient, DockerService +from .docker_client import DockerClient, DockerService, mount from .parsing import apply, basic_parser -__version__ = "1.0.0" +__version__ = "1.1.0" __date__ = "2023-03-20" -__updated__ = "2023-03-22" +__updated__ = "2023-10-31" _logger = logging.getLogger(__name__) @@ -26,13 +24,11 @@ def __init__( self, - docker_client, project_name: str, attach: bool = True, stop_at_exit: bool = True, ): super().__init__( - docker_client, f"redis_{project_name}", "redis", "latest", @@ -49,14 +45,14 @@ result = super()._create_param() target = "/data" result["mounts"] = [ - Mount( + mount( target, DockerClient.create_volume( - f"{self.name}{target.replace('/', '_')}", {"mounted in": target} + f"{self.name}{target.replace('/', '_')}", {"mount-location": target} )[0].name, "volume", ), - Mount("/var/run/redis", self.socket_dir, "bind"), + mount("/var/run/redis", self.socket_dir, "bind"), ] result["command"] = [ "redis-server", @@ -76,9 +72,14 @@ # socket exists, but it needs to be done inside the container due to permission # issue) time.sleep(0.01) - DockerClient.exec( - self._container, ["chmod", "777", "/var/run/redis/redis.sock"], "redis" - ) + if self._container is not None: + DockerClient.exec( + self._container, ["chmod", "777", "/var/run/redis/redis.sock"], "redis" + ) + else: + # Could happen if container did not start properly + _logger.warning("Redis container is not present") + return False return result def _start(self) -> None: @@ -126,7 +127,7 @@ parser = __parser() namespace = parser.parse_args(argv) apply(namespace) - docker_redis = DockerRedis(DockerClient.client, namespace.name) + docker_redis = DockerRedis(namespace.name) docker_redis.start() return 0 diff --git a/odoo_scripts/docker_rednerd.py b/odoo_scripts/docker_rednerd.py --- a/odoo_scripts/docker_rednerd.py +++ b/odoo_scripts/docker_rednerd.py @@ -10,23 +10,22 @@ import tempfile import time from multiprocessing import Process -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from urllib.parse import quote -import docker # type: ignore[import] import requests import requests_unixsocket # type: ignore[import] -from docker.types import Mount # type: ignore[import] +from python_on_whales import docker from .config import Config -from .docker_client import DockerClient, DockerService +from .docker_client import DockerClient, DockerService, Mount, mount from .parsing import apply, basic_parser _logger = logging.getLogger(__name__) -__version__ = "1.1.3" +__version__ = "1.2.0" __date__ = "2021-01-14" -__updated__ = "2023-03-16" +__updated__ = "2023-10-31" MIGRATE = "migrate" ADMIN_PASSWORD = "admin-password" @@ -40,7 +39,6 @@ def __init__( self, - docker_client: docker.client.DockerClient, project_name: str, repository: str, db_user: str, @@ -60,7 +58,6 @@ log_level: str = "info", ): """ - :param docker_client: instance of docker client :param project_name: name of the project, used for some volume and container :param repository: repository to use for the redner image :param db_user: redner database user @@ -75,7 +72,6 @@ """ for i, argument in enumerate( ( - docker_client, project_name, repository, db_user, @@ -86,7 +82,6 @@ assert argument is not None, "Missing required argument %s" % i # db_password can be none, in that case expect that any password would work super().__init__( - docker_client, f"rednerd_{project_name}", repository, version, @@ -129,7 +124,7 @@ ports: Dict[str, str] = {} volumes: Dict[str, Dict[str, str]] = {} mounts: List[Mount] = [] - create_param = dict( + create_param: Dict[str, Any] = dict( name=self.name, ports=ports, environment=self.environment, @@ -138,7 +133,7 @@ network_mode="host", ) if self.db_is_socket_path: - mounts.append(Mount(self.db_host, self.db_host, "bind")) + mounts.append(mount(self.db_host, self.db_host, "bind")) # migrate _logger.info("migrate") DockerClient.run( @@ -175,11 +170,12 @@ ports[f"{self.redner_port}/tcp"] = self.redner_port _logger.info("Using port %s", self.redner_port) if hasattr(self, "socket_dir"): + # TODO change to use mounts volumes[self.socket_dir] = { "bind": self.socket_dir, "mode": "rw", } - self._container = self.docker_cli.containers.create(**create_param) + self._container = docker.container.create(**create_param) _logger.info("Starting container %s", self.name) _logger.debug(create_param) self._container.start() @@ -401,7 +397,6 @@ return 81 # Instantiate docker rednerd docker_rednerd = DockerRednerd( - DockerClient.client, project_name, rednerd_repository, db_user=db_user, diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,7 @@ ] conf2reST = [] docker = [ - "docker >=3.4", - "docker <6.0.0; python_version<'3.7'", - "dockerpty", + "python-on-whales", # used in docker_rednerd "requests_unixsocket", "requests",