Skip to content

Commit

Permalink
Merge pull request #159 from akretion/UPD-use_docker_compose_v2-fixes
Browse files Browse the repository at this point in the history
Upd use docker compose v2 fixes
  • Loading branch information
sebastienbeau authored Oct 21, 2024
2 parents 251612b + 9ced2c2 commit 555bd16
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 147 deletions.
1 change: 0 additions & 1 deletion docky/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
from . import cmd
from . import common
from .main import Docky
from . import dcpatched
1 change: 0 additions & 1 deletion docky/cmd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
from . import base
from . import forward
from . import run_open
from . import kill
8 changes: 4 additions & 4 deletions docky/cmd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions docky/cmd/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
17 changes: 0 additions & 17 deletions docky/cmd/kill.py

This file was deleted.

77 changes: 68 additions & 9 deletions docky/cmd/run_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
# @author Sébastien BEAU <[email protected]>
# 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):
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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)
108 changes: 30 additions & 78 deletions docky/common/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,108 +2,60 @@
# @author Sébastien BEAU <[email protected]>
# 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


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")
31 changes: 0 additions & 31 deletions docky/dcpatched.py

This file was deleted.

10 changes: 4 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 555bd16

Please sign in to comment.