diff --git a/NEWS.rst b/NEWS.rst index bbe2a58b80cf6f95ca8271f773b2de9b580eabd8_TkVXUy5yc3Q=..ca586c1c865ef153e3f5486903f5d91cfa682732_TkVXUy5yc3Q= 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,8 @@ Changes to docker_isort so that it can be used to apply changes. +Compatibility with longpolling. + 16.10.0 ------- diff --git a/odoo_scripts/do_tests.py b/odoo_scripts/do_tests.py index bbe2a58b80cf6f95ca8271f773b2de9b580eabd8_b2Rvb19zY3JpcHRzL2RvX3Rlc3RzLnB5..ca586c1c865ef153e3f5486903f5d91cfa682732_b2Rvb19zY3JpcHRzL2RvX3Rlc3RzLnB5 100755 --- a/odoo_scripts/do_tests.py +++ b/odoo_scripts/do_tests.py @@ -224,6 +224,7 @@ dbname, "--stop-after-init", "--max-cron-threads=0", + "--workers=0", "--no-flake8", "--no-isort", "--no-dev", diff --git a/odoo_scripts/docker_client.py b/odoo_scripts/docker_client.py index bbe2a58b80cf6f95ca8271f773b2de9b580eabd8_b2Rvb19zY3JpcHRzL2RvY2tlcl9jbGllbnQucHk=..ca586c1c865ef153e3f5486903f5d91cfa682732_b2Rvb19zY3JpcHRzL2RvY2tlcl9jbGllbnQucHk= 100644 --- a/odoo_scripts/docker_client.py +++ b/odoo_scripts/docker_client.py @@ -4,6 +4,7 @@ import os import re import tarfile +from multiprocessing import Process from typing import Any, Dict, List, Mapping, Optional, Pattern, Tuple import docker @@ -102,6 +103,7 @@ 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""" replaces: List[Tuple[Pattern[str], str]] = ( @@ -140,11 +142,10 @@ def stop_remove(): """Stop then remove the container.""" if container: - if container.status == "running": - _logger.debug("Stopping container %s", container.name) - container.stop() - _logger.debug("Waiting for container %s", container.name) - container.wait() + _logger.debug("Stopping container %s", container.name) + container.stop() + _logger.debug("Waiting for container %s", container.name) + container.wait() remove() atexit.register(stop_remove) @@ -157,6 +158,22 @@ _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) diff --git a/odoo_scripts/docker_dev_start.py b/odoo_scripts/docker_dev_start.py index bbe2a58b80cf6f95ca8271f773b2de9b580eabd8_b2Rvb19zY3JpcHRzL2RvY2tlcl9kZXZfc3RhcnQucHk=..ca586c1c865ef153e3f5486903f5d91cfa682732_b2Rvb19zY3JpcHRzL2RvY2tlcl9kZXZfc3RhcnQucHk= 100755 --- a/odoo_scripts/docker_dev_start.py +++ b/odoo_scripts/docker_dev_start.py @@ -3,6 +3,7 @@ """Launch a docker image but bind the local directory inside the container and define the module path automatically. """ +import atexit import logging import os import pwd @@ -6,4 +7,5 @@ import logging import os import pwd +import socket import sys @@ -9,3 +11,4 @@ import sys +import tempfile from argparse import ArgumentParser from configparser import ConfigParser @@ -10,6 +13,7 @@ from argparse import ArgumentParser from configparser import ConfigParser +from contextlib import closing from subprocess import call, check_output from typing import List import dockerpty @@ -12,7 +16,8 @@ from subprocess import call, check_output from typing import List import dockerpty +from docker.errors import APIError from docker.types import Mount from psycopg2 import OperationalError, connect from psycopg2.errors import DuplicateDatabase, DuplicateObject @@ -370,6 +375,15 @@ default=True, dest="python_dev_mode", ) + + # workers options + parser.add_argument( + "--workers", + help="Number of workers [default: %(default)s]", + default=0, + type=int, + ) + return parser @@ -434,6 +448,9 @@ marabunta_db_password = nmspc.marabunta_db_password marabunta_force_version = nmspc.marabunta_force_version python_dev_mode = nmspc.python_dev_mode + workers = nmspc.workers + # XXX should that variable be called multi_workers? + longpolling = workers > 0 if restore_filename: if not database: @@ -478,9 +495,8 @@ # options is only used with subprocess call options = { "name": project_name, - "hostname": project_name, "tty": True, "extra_hosts": {}, "ports": {}, "environment": {}, } @@ -482,10 +498,16 @@ "tty": True, "extra_hosts": {}, "ports": {}, "environment": {}, } - if not use_host_network: - options["ports"][8069] = 8069 + # if not longpolling: + options["hostname"] = project_name + # TODO detect that from configuration or force it + exposed_port = 8069 + # TODO detect that from configuration or force it + odoo_run_port = 8069 + if not longpolling and not use_host_network: + options["ports"][exposed_port] = odoo_run_port mounts: List[Mount] = [] """Mounts for Odoo docker""" tested_modules = None @@ -500,7 +522,9 @@ arg = [ "populate" if populate_model or populate_size - else ("coverage-start" if coverage else "start") + else ("coverage-start" if coverage else "start"), + "--workers", + str(workers), ] if db_password: arg.extend(("--db_password", db_password)) @@ -1239,6 +1263,87 @@ if marabunta_db_password: options["environment"]["MARABUNTA_DB_PASSWORD"] = marabunta_db_password + if longpolling: + longpolling_port = find_free_port() + run_kwargs = { + "command": ["caddy", "run", "--config", "/etc/caddy/Caddyfile"], + "detach": True, + "name": f"{project_name}_caddy", + "mounts": [], + } + # force the port, so that the caddyfile is correct whatever the configuration + if use_host_network: + caddy_port = str(odoo_run_port) + # 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) + else: + caddy_port = "2019" + # TODO pre odoo 13: check if http-port should be changed to xmlrpc-port + arg.extend( + ( + "--longpolling-port", + str(longpolling_port), + "--http-port", + str(odoo_run_port), + ) + ) + + odoo_host = "" if use_host_network else project_name + caddyfile = f"""{{ + log {{ + format console + }} +}} + +:{caddy_port} {{ + reverse_proxy /longpolling/* {odoo_host}:{longpolling_port} + reverse_proxy {odoo_host}:{odoo_run_port} +}} +""" + with tempfile.NamedTemporaryFile( + delete=False, prefix="Caddyfile." + ) as caddyfile_tmp: + caddyfile_tmp.write(caddyfile.encode("UTF-8")) + + def cleanup_caddyfile(): + os.unlink(caddyfile_tmp.name) + + atexit.register(cleanup_caddyfile) + + run_kwargs["mounts"].append( + Mount("/etc/caddy/Caddyfile", caddyfile_tmp.name, type="bind") + ) + if use_host_network: + run_kwargs["network_mode"] = "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 + run_kwargs.update( + { + "network": project_name, + "ports": {caddy_port: odoo_run_port}, + "hostname": "caddy", + } + ) + options["network"] = project_name + + _logger.info( + f"Starting caddy, use http://localhost:{caddy_port}/ to access Odoo" + ) + # TODO there is an error as Odoo is not started up yet when this is started + # 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) + interactive = True if interactive: options["stdin_open"] = True @@ -1242,6 +1347,7 @@ interactive = True if interactive: options["stdin_open"] = True + odoo_container = client.containers.create( image, command=arg, mounts=mounts, **options ) @@ -1254,4 +1360,5 @@ 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() @@ -1257,6 +1364,7 @@ result = odoo_container.start() + # only remove after getting the return code odoo_container.remove() return result @@ -1258,7 +1366,14 @@ # only remove after getting the return code odoo_container.remove() return result +def find_free_port(): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/odoo_scripts/docker_postgresql.py b/odoo_scripts/docker_postgresql.py index bbe2a58b80cf6f95ca8271f773b2de9b580eabd8_b2Rvb19zY3JpcHRzL2RvY2tlcl9wb3N0Z3Jlc3FsLnB5..ca586c1c865ef153e3f5486903f5d91cfa682732_b2Rvb19zY3JpcHRzL2RvY2tlcl9wb3N0Z3Jlc3FsLnB5 100644 --- a/odoo_scripts/docker_postgresql.py +++ b/odoo_scripts/docker_postgresql.py @@ -9,6 +9,7 @@ import sys import tempfile import time +from functools import partial from typing import Callable, Tuple import docker @@ -79,9 +80,7 @@ for mount_dict in container.attrs["Mounts"]: if mount_dict["Destination"] == "/var/run/postgresql": source = mount_dict["Source"] - break - if source: - return container, lambda: stop_postgresql(container), source + return container, partial(stop_postgresql, container), source volumes = { pg_data_volume_name: { "bind": "/var/lib/postgresql/data", @@ -107,8 +106,7 @@ _logger.info("Starting postgresql container %s", pg.name) pg.start() - def stop_pg(): - return stop_postgresql(pg) + stop_pg = partial(stop_postgresql, pg) if stop_at_exit: atexit.register(stop_pg)