diff --git a/docky/__init__.py b/docky/__init__.py index 99ceb0b..d9cbae7 100644 --- a/docky/__init__.py +++ b/docky/__init__.py @@ -3,4 +3,3 @@ from . import cmd from . import common from .main import Docky -from . import dcpatched diff --git a/docky/cmd/__init__.py b/docky/cmd/__init__.py index 85ab009..7d03cbb 100644 --- a/docky/cmd/__init__.py +++ b/docky/cmd/__init__.py @@ -3,4 +3,3 @@ from . import base from . import forward from . import run_open -from . import kill diff --git a/docky/cmd/base.py b/docky/cmd/base.py index c7279eb..f128228 100644 --- a/docky/cmd/base.py +++ b/docky/cmd/base.py @@ -13,18 +13,18 @@ class Docky(cli.Application): PROGNAME = "docky" - VERSION = "8.0.0" + VERSION = "9.0.0" SUBCOMMAND_HELPMSG = None def _run(self, cmd, retcode=FG): """Run a command in a new process and log it""" - logger.debug(str(cmd).replace("/usr/local/bin/", "")) + logger.debug(str("$ " + str(cmd).rsplit("/")[-1])) return cmd & retcode def _exec(self, cmd, args=[]): """Run a command in the same process and log it this will replace the current process by the cmd""" - logger.debug(cmd + " ".join(args)) + logger.debug(str("$ " + str(cmd).rsplit("/")[-1] + " " + " ".join(args))) os.execvpe(cmd, [cmd] + args, local.env) @cli.switch("--verbose", help="Verbose mode", group="Meta-switches") @@ -44,7 +44,7 @@ def _run(self, *args, **kwargs): def _init_project(self): self.project = Project() - self.compose = local["docker-compose"] + self.compose = local["docker"]["compose"] def main(self, *args, **kwargs): if self._project_specific: diff --git a/docky/cmd/forward.py b/docky/cmd/forward.py index fab7e2c..03e2cf3 100644 --- a/docky/cmd/forward.py +++ b/docky/cmd/forward.py @@ -43,6 +43,10 @@ class DockyPs(DockyForward): """List containers""" _cmd = "ps" +@Docky.subcommand("kill") +class DockyKill(DockyForward): + """List containers""" + _cmd = "kill" @Docky.subcommand("logs") class DockyLogs(DockyForward): diff --git a/docky/cmd/kill.py b/docky/cmd/kill.py deleted file mode 100644 index 4786222..0000000 --- a/docky/cmd/kill.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2018-TODAY Akretion (http://www.akretion.com). -# @author Sébastien BEAU -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from .base import Docky, DockySub -from compose.parallel import parallel_kill - - -@Docky.subcommand("kill") -class DockyKill(DockySub): - """Kill all running container of the project""" - - def _main(self, *args): - # docker compose do not kill the container odoo as is was run - # manually, so we implement our own kill - containers = self.project.get_containers() - parallel_kill(containers, {"signal": "SIGKILL"}) diff --git a/docky/cmd/run_open.py b/docky/cmd/run_open.py index ffcc80f..b4e92b5 100644 --- a/docky/cmd/run_open.py +++ b/docky/cmd/run_open.py @@ -2,9 +2,13 @@ # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import sys +import subprocess from plumbum import cli from .base import Docky, DockySub -from ..common.api import raise_error +from ..common.api import raise_error, logger + +from python_on_whales import docker class DockyExec(DockySub): @@ -16,7 +20,7 @@ class DockyExec(DockySub): service = cli.SwitchAttr(["service"]) def _use_specific_user(self, service): - return not self.root and self.project.get_user(service) + return "root" if self.root else self.project.get_user(service) def _get_cmd_line(self, optionnal_command_line): user = self._use_specific_user(self.service) @@ -46,9 +50,15 @@ class DockyRun(DockyExec): """Start services and enter in your dev container""" def _check_running(self): - if self.project.get_containers(service=self.service): - raise_error("This container is already running, kill it or " - "use open to go inside") + for service in docker.compose.ps(services=[self.service], all=True): + if service.state.status == "exited": + # In case that you have used "docker compose run" without the + # option "--rm" you can have exited container + # we purge them here as they are useless + service.remove() + else: + raise_error("This container is already running, kill it or " + "use open to go inside") def _main(self, *optionnal_command_line): super()._main(*optionnal_command_line) @@ -57,9 +67,17 @@ def _main(self, *optionnal_command_line): self._run(self.compose["rm", "-f"]) self.project.display_service_tooltip() self.project.create_volume() - self._exec("docker-compose", [ - "run", "--rm", "--service-ports", "--use-aliases", "-e", "NOGOSU=True", - self.service] + self.cmd) + # Default command + docky_cmd = ["run", "--rm", "--service-ports", "--use-aliases", "-e", "NOGOSU=True", self.service] + self.cmd + + self._exec("docker", ["compose"] + docky_cmd) + + # TODO: Should we use python-on-whales commands? + # Its possible make + # docker.compose.run(self.project.name, and other parameters) + # But until now was not possible make the same command as above, + # if its possible we should consider the option to use it. + # https://gabrieldemarmiesse.github.io/python-on-whales/sub-commands/compose/ @Docky.subcommand("open") @@ -70,4 +88,45 @@ class DockyOpen(DockyExec): def _main(self, *optionnal_command_line): super()._main(*optionnal_command_line) - self._exec("dcpatched", ["exec", "-e", "NOGOSU=True", self.service] + self.cmd) + # self._exec("dcpatched", ["exec", "-e", "NOGOSU=True", self.service] + self.cmd) + + # Get Project Name + # Example: docky-odoo-brasil-14 odoo + project_name = self.project.name + "-" + self.project.service + + # Get User + user = self._use_specific_user(self.service) + + # Get Container ID + command = "docker ps -aqf name=" + project_name + # Example of return value + # b'b5db9db21381\n' + # Option text=true return as string instead of bytes and strip remove break line + # TODO: Is there a better way to do it, for example with Plumbum? + container_id = subprocess.check_output(command, shell=True,text=True).strip() + + self._exec("docker", ["exec", "-u", user, "-it", container_id, "/bin/bash"]) + +@Docky.subcommand("system") +class DockySystem(DockyExec): + """ + Check your System Infos: + OS Type, Kernel, OS, Docker, Docker Compose, and Docky versions. + """ + def _main(self): + # Info + infos = docker.system.info() + # OS Type + logger.info("OS Type " + infos.os_type) + # Kernel Version + logger.info("Kernel Version " + infos.kernel_version) + # Operation System + logger.info("OS " + infos.operating_system) + # Python Version + logger.info("Python Version " + sys.version) + # Docker Version + logger.info("Docker Version " + infos.server_version) + # Docker Compose Version + logger.info(docker.compose.version()) + # Docky Version + logger.info("Docky Version " + Docky.VERSION) diff --git a/docky/common/project.py b/docky/common/project.py index 7c0c78d..3300adc 100644 --- a/docky/common/project.py +++ b/docky/common/project.py @@ -2,12 +2,9 @@ # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import docker -from compose.project import OneOffFilter -from compose.cli import command -from compose.config.errors import ComposeFileNotFound +from python_on_whales import docker from plumbum import local - +import os from .api import logger @@ -15,95 +12,50 @@ class Project(object): def __init__(self): try: - self.project = command.project_from_options(".", {}) - except ComposeFileNotFound: - print("No docker-compose found, create one with :") - print("$ docky init") - exit(-1) - - self.name = self.project.name - self.loaded_config = None + self.project = docker.compose.config(return_json=True) + except Exception as e: + logger.error("Fail to load the configuration, try to validate it") + # If we fail to read the config file, it's mean that the config + # is not valid. In order to raise the same error as docker compose + # we launch the cmd to validate the config + os.execvpe("docker", [ + "docker", "--log-level", "ERROR", "compose", "config" + ], local.env) + + self.name = self.project.get("name") self.service = self._get_main_service(self.project) def _get_main_service(self, project): """main_service has docky.main.service defined in his label.""" - for service in project.services: - labels = service.options.get("labels", {}) - # service.labels() do not contain docky.main.service - # see also compose.service.merge_labels - if labels.get("docky.main.service", False): - return service.name - - def get_containers(self, service=None): - kwargs = {"one_off": OneOffFilter.include} - if service: - kwargs["service_names"] = [service] - return self.project.containers(**kwargs) + for service in project.get("services"): + labels = project["services"][service].get("labels") + if labels and labels.get("docky.main.service"): + return service def display_service_tooltip(self): - infos = self._get_services_info() - for service in self.project.services: - labels = service.options.get("labels", {}) - if labels.get("docky.access.help"): - # TODO remove after some versions - logger.warning( - "'docky.access.help' is replaced by 'docky.help'. " - "Please update this key in your docker files.") - if infos.get(service.name): - # some applications provide extra parameters to access resource - infos[service.name] += labels.get("docky.url_suffix", "") - logger.info(infos[service.name]) - if labels.get("docky.help"): - logger.info(labels.get("docky.help")) - - def _get_services_info(self): - """ Search IP and Port for each services - """ - client = docker.from_env() - services = (x for x in client.containers.list() - if self.project.name in x.attrs["Name"]) - infos = {} - for serv in services: - proj_key = [ - x for x in serv.attrs["NetworkSettings"]["Networks"].keys() - if self.project.name in x] - proj_key = proj_key and proj_key[0] or False - if not serv.attrs["NetworkSettings"]["Networks"].get(proj_key): - continue - ip = serv.attrs["NetworkSettings"]["Networks"][proj_key].get( - "IPAddress", "") - info = { - "name": serv.attrs["Config"]["Labels"].get( - "com.docker.compose.service", ""), - "ip": ip, - "port": [x for x in serv.attrs["NetworkSettings"].get("Ports", "")] - } - if info["name"] != "db" and info.get("port"): - urls = ["http://%s:%s" % (info["ip"], port.replace("/tcp", "")) - for port in info["port"]] - # There is no web app to access 'db' service: try adminer for that - infos[info["name"]] = "%s %s" % (info["name"], " ".join(urls)) - return infos + for _name, service in self.project.get("services").items(): + docky_help = service.get("labels", {}).get("docky.help") + if docky_help: + logger.info(docky_help) def create_volume(self): """Mkdir volumes if they don't exist yet. Only apply to external volumes. - docker-compose up do not attemps to create it - so we have to do it ourselves""" - for service in self.project.services: - for volume in service.options.get("volumes", []): - if volume.external: - path = local.path(local.env.expand(volume.external)) + docker compose will create it but the owner will be root + so we have to do it ourselves with the right owner""" + for service_name, service in self.project.get("services").items(): + for volume in service.get("volumes", []): + if volume["type"] == "bind": + path = local.path(local.env.expand(volume["source"])) if not path.exists(): logger.info( "Create missing directory %s for service %s", - path, service.name) + path, service_name) path.mkdir() def get_user(self, service_name): - service = self.project.get_service(name=service_name) - labels = service.options.get("labels") + labels = self.project["services"].get(service_name).get("labels") if labels: - return labels.get("docky.user", None) + return labels.get("docky.user") diff --git a/docky/dcpatched.py b/docky/dcpatched.py deleted file mode 100644 index 2e3119e..0000000 --- a/docky/dcpatched.py +++ /dev/null @@ -1,31 +0,0 @@ -# coding: utf-8 -from compose import cli -from compose.service import Service -from compose.cli.main import TopLevelCommand -from compose.const import LABEL_CONTAINER_NUMBER - - -# This is a patched version of docker-compose -# We patch it in order to be able to -# - do a docker-compose exec on a running container (launch with run) - - -get_ori_container = Service.get_container - - -def get_run_container(self, number=1): - # search for container running in background - for container in self.containers( - labels=["{0}={1}".format(LABEL_CONTAINER_NUMBER, number)]): - return container - - # search for container running with "run" cmd - for container in self.containers(one_off=True): - if container.service == self.name: - return container - raise ValueError("No container found for %s_%s" % (self.name, number)) - - -def main(): - Service.get_container = get_run_container - cli.main.main() diff --git a/requirements.txt b/requirements.txt index 6a11ba7..dea9cb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,7 @@ -docker-compose>=1.23.1 +python-on-whales plumbum rainbow_logging_handler python-slugify -# Only for solving installation issue with pip that fail to -# solve the version of request compatible with docker and docker-compose -requests<3,>=2.20.0 -importlib-metadata; python_version >= '3.10' -PyYAML >= 5.1, < 5.4 +requests +importlib-metadata +PyYAML >= 6.0.1