fanch 1 anno fa
parent
commit
428f3cf88f
42 ha cambiato i file con 1207 aggiunte e 0 eliminazioni
  1. 1 0
      MANIFEST.in
  2. 0 0
      conftest.py
  3. 24 0
      setup.cfg
  4. 55 0
      setup.py
  5. 8 0
      src/metadocker.egg-info/PKG-INFO
  6. 87 0
      src/metadocker.egg-info/SOURCES.txt
  7. 1 0
      src/metadocker.egg-info/dependency_links.txt
  8. 1 0
      src/metadocker.egg-info/not-zip-safe
  9. 8 0
      src/metadocker.egg-info/requires.txt
  10. 1 0
      src/metadocker.egg-info/top_level.txt
  11. 0 0
      src/metadocker/__init__.py
  12. 6 0
      src/metadocker/__main__.py
  13. 0 0
      src/metadocker/cmdline/__init__.py
  14. 55 0
      src/metadocker/cmdline/base.py
  15. 0 0
      src/metadocker/cmdline/commands/__init__.py
  16. 42 0
      src/metadocker/cmdline/commands/base.py
  17. 0 0
      src/metadocker/cmdline/commands/main/__init__.py
  18. 30 0
      src/metadocker/cmdline/commands/main/init.py
  19. 17 0
      src/metadocker/cmdline/commands/main/list.py
  20. 0 0
      src/metadocker/cmdline/commands/specific/__init__.py
  21. 20 0
      src/metadocker/cmdline/commands/specific/build.py
  22. 34 0
      src/metadocker/cmdline/commands/specific/logs.py
  23. 12 0
      src/metadocker/cmdline/commands/specific/rm.py
  24. 57 0
      src/metadocker/cmdline/commands/specific/run.py
  25. 12 0
      src/metadocker/cmdline/commands/specific/start.py
  26. 22 0
      src/metadocker/cmdline/commands/specific/status.py
  27. 21 0
      src/metadocker/cmdline/commands/specific/stop.py
  28. 51 0
      src/metadocker/cmdline/commands/specific/systemd.py
  29. 0 0
      src/metadocker/common/__init__.py
  30. 17 0
      src/metadocker/common/errors.py
  31. 8 0
      src/metadocker/common/misc.py
  32. 1 0
      src/metadocker/docker/__init__.py
  33. 66 0
      src/metadocker/docker/builder.py
  34. 104 0
      src/metadocker/docker/driver.py
  35. 1 0
      src/metadocker/env/__init__.py
  36. 255 0
      src/metadocker/env/base.py
  37. 163 0
      src/metadocker/env/pythonserver.py
  38. 8 0
      src/metadocker/main.py
  39. 12 0
      src/metadocker/register.py
  40. 0 0
      src/metadocker/scripts/__init__.py
  41. 7 0
      src/metadocker/scripts/metadocker
  42. 0 0
      tests/__init__.py

+ 1 - 0
MANIFEST.in

@@ -0,0 +1 @@
+recursive-include src *.*

+ 0 - 0
conftest.py


+ 24 - 0
setup.cfg

@@ -0,0 +1,24 @@
+[coverage:run]
+source = src
+
+[coverage:report]
+ignore_errors = False
+show_missing = True
+
+[coverage:xml]
+output = coverage.xml
+
+[tool:pytest]
+markers =
+    noci: marks tests so that they are not executed in continuous integration (jenkins)
+testpaths = tests/
+junit_family = xunit2
+console_output_style = progress
+log_level = DEBUG
+
+[build_sphinx]
+; python setup.py build_sphinx  # Generate the HTML documentation in dist/docs/html
+source-dir = docs/source
+build-dir = dist/docs
+all_files = 1
+

+ 55 - 0
setup.py

@@ -0,0 +1,55 @@
+from setuptools import setup, find_packages
+from pathlib import Path
+
+install_requires = [
+    "requests",
+]
+
+tests_require = [
+    "mkfab",
+    "pytest",
+]
+
+def get_files(path):
+    root = Path(path)
+    queue = [root]
+    ret = []
+    while queue:
+        curr = queue.pop()
+        for file in curr.iterdir():
+            if file.is_file():
+                ret.append(str(file))
+            else:
+                queue.append(file)
+    return ret
+
+
+setup(
+    name="metadocker",
+    version="0.1.1",
+    description="Un outil pour gérer des conteneurs",
+    author="François GAUTRAIS",
+    author_email="francois@gautrais.eu",
+    install_requires=install_requires,
+    packages=find_packages("src"),
+    include_package_data=True,
+    zip_safe=False,
+    data_files=[
+    ],
+    test_suite="tests",
+    tests_require=tests_require,
+    extras_require={
+        "test": tests_require,
+        "pylint": ["pylint"],
+    },
+    scripts=[
+      "src/metadocker/scripts/metadocker"
+    ],
+
+    entry_points={
+        "console_scripts": [
+        ]
+    },
+    package_dir={"": "src"},
+)
+

+ 8 - 0
src/metadocker.egg-info/PKG-INFO

@@ -0,0 +1,8 @@
+Metadata-Version: 2.1
+Name: metadocker
+Version: 0.1.1
+Summary: Un outil pour gérer des conteneurs
+Author: François GAUTRAIS
+Author-email: francois@gautrais.eu
+Provides-Extra: test
+Provides-Extra: pylint

+ 87 - 0
src/metadocker.egg-info/SOURCES.txt

@@ -0,0 +1,87 @@
+MANIFEST.in
+setup.cfg
+setup.py
+src/metadocker/__init__.py
+src/metadocker/__main__.py
+src/metadocker/main.py
+src/metadocker/register.py
+src/metadocker.egg-info/PKG-INFO
+src/metadocker.egg-info/SOURCES.txt
+src/metadocker.egg-info/dependency_links.txt
+src/metadocker.egg-info/not-zip-safe
+src/metadocker.egg-info/requires.txt
+src/metadocker.egg-info/top_level.txt
+src/metadocker/__pycache__/__init__.cpython-311.pyc
+src/metadocker/__pycache__/__init__.cpython-38.pyc
+src/metadocker/__pycache__/__main__.cpython-311.pyc
+src/metadocker/__pycache__/register.cpython-311.pyc
+src/metadocker/__pycache__/register.cpython-38.pyc
+src/metadocker/cmdline/__init__.py
+src/metadocker/cmdline/base.py
+src/metadocker/cmdline/__pycache__/__init__.cpython-311.pyc
+src/metadocker/cmdline/__pycache__/__init__.cpython-38.pyc
+src/metadocker/cmdline/__pycache__/base.cpython-311.pyc
+src/metadocker/cmdline/__pycache__/base.cpython-38.pyc
+src/metadocker/cmdline/commands/__init__.py
+src/metadocker/cmdline/commands/base.py
+src/metadocker/cmdline/commands/__pycache__/__init__.cpython-311.pyc
+src/metadocker/cmdline/commands/__pycache__/__init__.cpython-38.pyc
+src/metadocker/cmdline/commands/__pycache__/base.cpython-311.pyc
+src/metadocker/cmdline/commands/__pycache__/base.cpython-38.pyc
+src/metadocker/cmdline/commands/main/__init__.py
+src/metadocker/cmdline/commands/main/init.py
+src/metadocker/cmdline/commands/main/list.py
+src/metadocker/cmdline/commands/main/__pycache__/__init__.cpython-311.pyc
+src/metadocker/cmdline/commands/main/__pycache__/__init__.cpython-38.pyc
+src/metadocker/cmdline/commands/main/__pycache__/init.cpython-311.pyc
+src/metadocker/cmdline/commands/main/__pycache__/init.cpython-38.pyc
+src/metadocker/cmdline/commands/main/__pycache__/list.cpython-311.pyc
+src/metadocker/cmdline/commands/main/__pycache__/list.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__init__.py
+src/metadocker/cmdline/commands/specific/build.py
+src/metadocker/cmdline/commands/specific/logs.py
+src/metadocker/cmdline/commands/specific/rm.py
+src/metadocker/cmdline/commands/specific/run.py
+src/metadocker/cmdline/commands/specific/start.py
+src/metadocker/cmdline/commands/specific/status.py
+src/metadocker/cmdline/commands/specific/stop.py
+src/metadocker/cmdline/commands/specific/__pycache__/__init__.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/__init__.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/build.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/build.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/rm.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/rm.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/run.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/run.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/start.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/start.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/status.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/status.cpython-38.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/stop.cpython-311.pyc
+src/metadocker/cmdline/commands/specific/__pycache__/stop.cpython-38.pyc
+src/metadocker/common/__init__.py
+src/metadocker/common/errors.py
+src/metadocker/common/__pycache__/__init__.cpython-311.pyc
+src/metadocker/common/__pycache__/__init__.cpython-38.pyc
+src/metadocker/common/__pycache__/errors.cpython-311.pyc
+src/metadocker/common/__pycache__/errors.cpython-38.pyc
+src/metadocker/docker/__init__.py
+src/metadocker/docker/builder.py
+src/metadocker/docker/driver.py
+src/metadocker/docker/__pycache__/__init__.cpython-311.pyc
+src/metadocker/docker/__pycache__/__init__.cpython-38.pyc
+src/metadocker/docker/__pycache__/builder.cpython-311.pyc
+src/metadocker/docker/__pycache__/builder.cpython-38.pyc
+src/metadocker/docker/__pycache__/driver.cpython-311.pyc
+src/metadocker/docker/__pycache__/driver.cpython-38.pyc
+src/metadocker/env/__init__.py
+src/metadocker/env/base.py
+src/metadocker/env/pythonserver.py
+src/metadocker/env/__pycache__/__init__.cpython-311.pyc
+src/metadocker/env/__pycache__/__init__.cpython-38.pyc
+src/metadocker/env/__pycache__/base.cpython-311.pyc
+src/metadocker/env/__pycache__/base.cpython-38.pyc
+src/metadocker/env/__pycache__/pythonserver.cpython-311.pyc
+src/metadocker/env/__pycache__/pythonserver.cpython-38.pyc
+src/metadocker/scripts/__init__.py
+src/metadocker/scripts/metadocker

+ 1 - 0
src/metadocker.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 1 - 0
src/metadocker.egg-info/not-zip-safe

@@ -0,0 +1 @@
+

+ 8 - 0
src/metadocker.egg-info/requires.txt

@@ -0,0 +1,8 @@
+requests
+
+[pylint]
+pylint
+
+[test]
+mkfab
+pytest

+ 1 - 0
src/metadocker.egg-info/top_level.txt

@@ -0,0 +1 @@
+metadocker

+ 0 - 0
src/metadocker/__init__.py


+ 6 - 0
src/metadocker/__main__.py

@@ -0,0 +1,6 @@
+from metadocker.cmdline.base import MainArgs
+
+def main():
+    MainArgs.main(None)
+
+main()

+ 0 - 0
src/metadocker/cmdline/__init__.py


+ 55 - 0
src/metadocker/cmdline/base.py

@@ -0,0 +1,55 @@
+import argparse
+from metadocker.cmdline.commands.specific import start, stop, build, run, rm, status, logs, systemd
+from metadocker.cmdline.commands.main import init, list
+import sys
+
+
+class Args(argparse.ArgumentParser):
+    _COMMANDS = []
+
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.subparsers = self.add_subparsers(prog="action", parser_class=argparse.ArgumentParser)
+        for k in self._COMMANDS:
+            self._add_command(k)
+
+    def parse(self, env, args=None):
+        args = args if args is not None else (sys.argv[1:])
+
+        ret = self.parse_args(args)
+        if hasattr(ret, "command"):
+            ret.command(ret).start(env)
+        else:
+            self.print_help()
+            exit(-1)
+        return ret
+
+    def _add_command(self, classe):
+        kwargs = {}
+        if classe.HELP:
+            kwargs["help"] = classe.HELP
+        if classe.ALIASES:
+            kwargs["aliases"] = classe.ALIASES
+
+        parser = self.subparsers.add_parser(classe.NAME, **kwargs)
+        for k in classe.ARGUMENTS:
+            k.call(parser)
+        parser.set_defaults(command=classe)
+        return parser
+
+    @classmethod
+    def main(cls, env, args=None):
+        return cls().parse(env, args)
+
+
+class ArgsFromFile(Args):
+    _COMMANDS = [build.BuildCommand, start.StartCommand, stop.StopCommand, run.RunCommand,
+                 rm.RmCommand, status.StatusCommand, logs.LogsCommand, systemd.SystemdCommand]
+
+    def __init__(self):
+        super().__init__()
+        self.add_argument("--debug", "-d", help="Mode débug", action="store_true")
+
+class MainArgs(Args):
+    _COMMANDS = [init.InitCommand, list.ListCommand]

+ 0 - 0
src/metadocker/cmdline/commands/__init__.py


+ 42 - 0
src/metadocker/cmdline/commands/base.py

@@ -0,0 +1,42 @@
+import sys
+
+from metadocker.common.errors import MetadockerError
+
+
+class Argument:
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+
+    def call(self, parser):
+        parser.add_argument(*self.args, **self.kwargs)
+
+class Command:
+    NAME = None
+    HELP = None
+    ALIASES = []
+    ARGUMENTS = []
+    CATCH_EXCEPTION = True
+    def __init__(self, data):
+        self._data = data
+        data = data.__dict__
+
+        for k, v in data.items():
+            setattr(self, k, v)
+
+    def start(self, env):
+        env.debug = self.debug
+        try:
+            self.run(env)
+        except MetadockerError as err:
+            print("Erreur", file=sys.stderr)
+            print(err.get_error(), file=sys.stderr)
+
+    def run(self, env):
+        raise NotImplementedError()
+
+
+
+
+
+

+ 0 - 0
src/metadocker/cmdline/commands/main/__init__.py


+ 30 - 0
src/metadocker/cmdline/commands/main/init.py

@@ -0,0 +1,30 @@
+import sys
+from pathlib import Path
+
+from metadocker.cmdline.commands.base import Argument, Command
+from metadocker.register import load_env
+
+
+class InitCommand(Command):
+    NAME = "init"
+    HELP = "Initialise un script"
+    ARGUMENTS = [
+        Argument("env", help="Le type d'environnement à initialiser"),
+        Argument("--output", "-o", action="store_true", help="Le fichier destination", default="./docker")
+    ]
+
+    def run(self, env):
+        env_name = self.env.lower()
+        envs = load_env()
+        if env_name not in envs:
+            print(f"Impossible de trouver l'environnement '{self.env}'", file=sys.stderr)
+            exit(-1)
+
+        output = Path(self.output)
+        output.parent.mkdir(exist_ok=True, parents=True)
+        env = envs[env_name]()
+
+        content = env.create_empty_config()
+        output.write_text(content)
+        output.chmod(0o750)
+

+ 17 - 0
src/metadocker/cmdline/commands/main/list.py

@@ -0,0 +1,17 @@
+from metadocker.cmdline.commands.base import Argument, Command
+from metadocker.register import load_env
+
+
+class ListCommand(Command):
+    NAME = "liste"
+    HELP = "Liste les scripts"
+    ARGUMENTS = [
+    ]
+
+    def run(self, env):
+        print(f"Environnement disponibles:")
+        for v in load_env().values():
+            print(v._name_)
+
+
+

+ 0 - 0
src/metadocker/cmdline/commands/specific/__init__.py


+ 20 - 0
src/metadocker/cmdline/commands/specific/build.py

@@ -0,0 +1,20 @@
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+import os, sys
+
+class BuildCommand(Command):
+    NAME = "build"
+    HELP = "Crée le conteneur"
+    ARGUMENTS = [
+        Argument("--dockerfile", "-d",  default=None, help="Fichier de dockerfile à utiliser. Si non donné utilise un fichier temporarie"),
+        Argument("--dry-run",  default=None, help="Ne lance pas docker"),
+    ]
+
+    def run(self, env):
+        docker = DockerBuilder()
+        env.build_docker_file(docker, self.dockerfile)
+        if not self.dry_run:
+            driver.build(self.dockerfile, tag=docker.tag)
+
+

+ 34 - 0
src/metadocker/cmdline/commands/specific/logs.py

@@ -0,0 +1,34 @@
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.common.errors import SimpleError
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+class LogsCommand(Command):
+    NAME = "logs"
+    HELP = "Accede au log du container"
+    ARGUMENTS = [
+        Argument("--details", "-d", action="store_true", help="Affiche plus de détails"),
+        Argument("--follow", "-f", action="store_true", help="Follow log output"),
+        Argument("--since", "-s", help='Affiche les logs depuis (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes)'),
+        Argument("--tail", "-T", help="Nombre de ligne à afficher"),
+        Argument("--until", "-u", help="Nombre de ligne à afficher"),
+        Argument("--timestamps", "-t", action="store_true", help='Affiche les logs jusque (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes)'),
+    ]
+    def run(self, env):
+        docker = DockerBuilder(env)
+        env = env.get_env()
+        name = docker.name
+
+        for res in driver.ps(name=docker.name):
+            driver.logs(docker.name,
+                        details=self.details,
+                        follow=self.follow,
+                        since=self.since,
+                        tail=self.tail,
+                        until=self.until,
+                        timestamps=self.timestamps,
+                        catch=False)
+            break
+        else:
+            raise SimpleError(f"Le conteneur '{docker.name}' n'est disponible")

+ 12 - 0
src/metadocker/cmdline/commands/specific/rm.py

@@ -0,0 +1,12 @@
+from metadocker.cmdline.commands.base import Argument, Command
+
+
+class RmCommand(Command):
+    NAME = "rm"
+    HELP = "Supprime le conteneur"
+    ARGUMENTS = [
+    ]
+
+    def run(self, env):
+        pass
+

+ 57 - 0
src/metadocker/cmdline/commands/specific/run.py

@@ -0,0 +1,57 @@
+import sys
+from pathlib import Path
+
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.common.errors import SimpleError
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+class RunCommand(Command):
+    NAME = "run"
+    HELP = "Initialise le conteneur"
+    ARGUMENTS = [
+        Argument("--root", "-r", action="store_true", help="Lance avec utilisateur root"),
+        Argument("--interactive", "-i", action="store_true", help="Lance en mode interactif"),
+        Argument("commande", nargs="*", help="Command à lancer")
+    ]
+
+    def _rm_if_needed(self, env, docker):
+        for data in driver.ps(name=docker.name):
+            if data.running:
+                raise SimpleError(f"Erreur le conteneur '{docker.name}' est déja lancé")
+            driver.rm(docker.name)
+
+    def run(self, env):
+        docker = DockerBuilder(env)
+        env = env.get_env()
+        self._rm_if_needed(env, docker)
+
+        cmdline = ["-i" if self.interactive else "-d",
+                   #"--rm"
+                   ]
+        if self.root:
+            cmdline.extend(("-u", "0"))
+
+
+        for host, container in env.PORTS.items():
+            cmdline.extend(("-p", f"{host}:{container}"))
+
+        for host, container in env.VOLUMES.items():
+            host = Path(host)
+
+            host.mkdir(exist_ok=True, parents=True)
+            if not host.is_absolute():
+                host = f"./{host}"
+            cmdline.extend(("-v", f"{host}:{container}"))
+
+        cmdline.extend(("--name", docker.name, "-t", docker.tag))
+        cmdline.extend(self.commande)
+
+        kwargs = {"catch": False}
+        if self.interactive:
+            kwargs["stdin"]=sys.stdin
+        data = driver.ps(name=docker.name)
+
+
+        driver.run(*cmdline, **kwargs)

+ 12 - 0
src/metadocker/cmdline/commands/specific/start.py

@@ -0,0 +1,12 @@
+from metadocker.cmdline.commands.base import Command
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+class StartCommand(Command):
+    NAME = "start"
+    HELP = "Lance le conteneur"
+
+    def run(self, env):
+        docker = DockerBuilder(env)
+        driver.start("-a")

+ 22 - 0
src/metadocker/cmdline/commands/specific/status.py

@@ -0,0 +1,22 @@
+import json
+
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+class StatusCommand(Command):
+    NAME = "status"
+    HELP = "Stop le conteneur"
+    ARGUMENTS = [
+    ]
+    def run(self, env):
+        docker = DockerBuilder(env)
+        env = env.get_env()
+        name = docker.name
+        data = driver.ps(name=name, classe=dict)
+        if data:
+            print(json.dumps(data[0], indent = 2))
+            return 0
+        return -1
+

+ 21 - 0
src/metadocker/cmdline/commands/specific/stop.py

@@ -0,0 +1,21 @@
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+class StopCommand(Command):
+    NAME = "stop"
+    HELP = "Stop le conteneur"
+    ARGUMENTS = [
+        Argument("--kill", "-k", action="store_true", help="Tue le container"),
+        Argument("--time", "-t", help="Temps avent de forcer le container", type=int),
+        Argument("--signal", "-s", help="Signal à envoyer au container"),
+    ]
+    def run(self, env):
+        docker = DockerBuilder(env)
+        env = env.get_env()
+        name = docker.name
+        if self.kill:
+            driver.kill(name, signal=self.signal)
+        else:
+            driver.stop(name, time=self.time, signal=self.signal)

+ 51 - 0
src/metadocker/cmdline/commands/specific/systemd.py

@@ -0,0 +1,51 @@
+import subprocess
+from pathlib import Path
+
+from metadocker.cmdline.commands.base import Command, Argument
+from metadocker.docker import DockerBuilder
+from metadocker.docker.driver import driver
+
+
+def get_file_content(name, time=10):
+    return f"""[Unit]
+Description=Le docker {name}
+Requires=docker.service
+After=docker.service
+
+[Service]
+Restart=always
+ExecStart=/usr/bin/docker start -a {name}
+ExecStop=/usr/bin/docker stop -t {time} {name}
+
+[Install]
+WantedBy=multi-user.target
+
+"""
+
+class SystemdCommand(Command):
+    NAME = "systemd"
+    HELP = "Permet de gérer le démarrage automatique"
+    ARGUMENTS = [
+        Argument("--output", "-o", help="Fichier de sortie de la génération"),
+        Argument("--install", "-i", action="store_true", help="Install le docker dans systemd"),
+        Argument("--startup", "-s", action="store_true", help="Temps avent de forcer le container"),
+
+    ]
+    def run(self, env):
+        docker = DockerBuilder(env)
+        name = docker.name
+        level = 0
+        if self.startup:
+            level = 2
+        elif self.install:
+            level = 1
+
+        output = (Path(self.output  or env.root) / f"{name}.service").resolve()
+        output.parent.mkdir(exist_ok=True, parents=True)
+        output.write_text(get_file_content(name))
+
+        if level >= 1:
+            subprocess.run(["sudo", "cp", output, "/etc/systemd/system/"])
+
+        if level >= 2:
+            subprocess.run(["sudo", "systemctl", "enable", f"{name}.service"])

+ 0 - 0
src/metadocker/common/__init__.py


+ 17 - 0
src/metadocker/common/errors.py

@@ -0,0 +1,17 @@
+
+
+class MetadockerError(Exception):
+
+    def get_error(self):
+        raise NotImplementedError()
+
+    def __repr__(self):
+        return self.get_error()
+
+class SimpleError(MetadockerError):
+    def __init__(self, err):
+        super().__init__(err)
+        self._err = err
+
+    def get_error(self):
+        return repr(self._err)

+ 8 - 0
src/metadocker/common/misc.py

@@ -0,0 +1,8 @@
+import inspect
+import os
+import sys
+from pathlib import Path
+
+
+def get_root_dir():
+    return Path(sys.argv[0]).parent.resolve()

+ 1 - 0
src/metadocker/docker/__init__.py

@@ -0,0 +1 @@
+from metadocker.docker.builder import DockerBuilder

+ 66 - 0
src/metadocker/docker/builder.py

@@ -0,0 +1,66 @@
+import json
+
+
+class DockerBuilder:
+
+    def __init__(self, env=None):
+        self.docker_file = []
+        self.systemd_file = []
+        self.project = None
+        self.version = None
+        self.tag = None
+        self.name = None
+        if env:
+            env.init(self)
+
+    def get_file_content(self):
+        return "\n".join(self.docker_file)
+
+    def init(self, project, version, tag=None, name=None):
+        self.project = project
+        self.version = version
+        self.tag = tag
+        if tag is None:
+            if self.version:
+                self.tag = f"{self.project.lower()}-{self.version.lower()}"
+            else:
+                self.tag = f"{self.project.lower()}-latest"
+        self.name = name if tag else f"{self.project.lower()}"
+
+    def add(self, *args):
+        self.docker_file.append(" ".join([str(x) for x in args if x is not None]))
+
+    def run(self, *args):
+        self.add("RUN", *args)
+
+    def from_(self, image, name = None):
+        self.add("FROM", image, f"as {name}" if name else None)
+
+    def copy(self,src, dst):
+        self.add("COPY", src, dst)
+
+    def cmd(self, *args):
+        self.add("CMD", *args)
+
+    def workdir(self, path):
+        self.add("WORKDIR", path)
+
+    def user(self, user):
+        self.add("USER", user)
+
+
+    def env(self, *args):
+        self.add("ENV", *args)
+
+    def arg(self, k, v):
+        self.add("ARG", k, v)
+
+    def expose(self, port):
+        self.add("EXPOSE", port)
+
+    def volume(self, *args):
+        self.add("VOLUME", *args)
+
+    def meta(self, data):
+        data = json.dumps(dict(data.items()))
+        self.add(f"# meta={data}")

+ 104 - 0
src/metadocker/docker/driver.py

@@ -0,0 +1,104 @@
+import json
+import subprocess
+from pathlib import Path
+
+from metadocker.common.errors import MetadockerError
+
+
+class ExecutionError(MetadockerError):
+    def __init__(self, cmdline, stdout, stderr):
+        super().__init__(cmdline, stdout, stderr)
+        self.cmdline = " ".join([str(x) for x in cmdline]) if not isinstance(cmdline, str) else cmdline
+        self.stdout=stdout
+        self.stderr = stderr
+
+    def get_error(self):
+        return f"ExecutionError: \nCommand Line: {self.cmdline}\nstdout: {self.stdout}\nstderr: {self.stderr}"
+
+class DockerContainer:
+    def __init__(self, data):
+        self.name = data["Names"]
+        self.image = data["Image"]
+        self.id = data["ID"]
+        self.status = data["State"]
+        self.running = self.status == "running"
+
+class DockerDriver:
+
+    def __init__(self):
+        pass
+
+    def _run(self, *args, raise_on_fail=True, catch=True, **kwargs):
+
+        if catch:
+            kwargs.setdefault("stdout", subprocess.PIPE)
+            kwargs.setdefault("stderr", subprocess.PIPE)
+
+        print(f"Running: {' '.join(str(x) for x in args)}")
+        ret = subprocess.run([str(x) for x in args], **kwargs)
+        if ret.returncode != 0 and raise_on_fail:
+            raise ExecutionError(args, ret.stdout, ret.stderr)
+        return ret
+
+
+    def _docker(self, *args, raise_on_fail=True, catch=True, **kwargs):
+        return self._run("docker", *args, raise_on_fail=raise_on_fail, catch=catch, **kwargs)
+
+    def build(self, file, tag=None, **kwarsg):
+        tag = [] if tag is None else ["-t", tag]
+        file = Path(file)
+        name = file.name
+        direct = file.parent
+        self._docker("build", "-f", file, *tag, direct, catch=False, **kwarsg)
+
+    def run(self, *args, **kwarsg):
+        self._docker("run", *args, **kwarsg)
+
+    def start(self, *args, **kwarsg):
+        self._docker("start", *args, **kwarsg)
+
+
+    def ps(self, classe=DockerContainer, **filters):
+        cmdline = ["ps", "-a",  "--format", "json", "--no-trunc"]
+        for k, v in filters.items():
+            cmdline.extend(("--filter", f"{k}={v}"))
+
+        ret = self._docker(*cmdline)
+        content = [classe(json.loads(x)) for x in ret.stdout.split(b"\n") if x]
+        return content
+
+
+    def stop(self, name, time=None, signal=None, **kwargs):
+        cmdlint = ["stop", name]
+        if time is not None:
+            cmdlint.extend(["-t", time])
+        if signal is not None:
+            cmdlint.extend(["-s", signal])
+        return self._docker(*cmdlint, **kwargs)
+
+    def kill(self, name, signal=None, **kwargs):
+        cmdlint = ["kill", name]
+        if signal is not None:
+            cmdlint.extend(["-s", signal])
+        return self._docker(*cmdlint, **kwargs)
+
+    def rm(self, name, force=False, **kwargs):
+        cmdlint = ["rm"]
+        if force: cmdlint.append("--force")
+        cmdlint.append(name)
+        return self._docker(*cmdlint, **kwargs)
+
+
+    def logs(self, name, details=False, follow=False, since=None, tail=None, timestamps=False, until=None, **kwargs):
+        cmdlint = ["logs"]
+        if details: cmdlint.append("--details")
+        if follow: cmdlint.append("--follow")
+        if since: cmdlint.extend(("--since", since))
+        if tail: cmdlint.extend(("--tail", tail))
+        if timestamps: cmdlint.append("--timestamps")
+        if tail: until.extend(("--until", tail))
+        cmdlint.append(name)
+        return self._docker(*cmdlint, **kwargs)
+
+
+driver = DockerDriver()

+ 1 - 0
src/metadocker/env/__init__.py

@@ -0,0 +1 @@
+from metadocker.env.pythonserver import PythonServerEnv

+ 255 - 0
src/metadocker/env/base.py

@@ -0,0 +1,255 @@
+import json
+import sys
+import tempfile
+from pathlib import Path
+
+from metadocker.cmdline.base import ArgsFromFile
+from metadocker.common.errors import MetadockerError
+from metadocker.common.misc import get_root_dir
+from metadocker.docker import DockerBuilder
+
+NoneType=None.__class__
+
+def get_types(x):
+    if x is None:
+        return tuple()
+    if not isinstance(x, (list, tuple, set)):
+        return (x,)
+    return x
+
+class Var:
+
+    class VarException(MetadockerError):
+        def get_error(self):
+            raise NotImplementedError()
+
+    class RequiredException(VarException):
+        def __init__(self, name):
+            super().__init__(name)
+            self.name = name
+
+        def get_error(self):
+            return f"La variable {self.name} est requise"
+
+    class ValidationException(VarException):
+        def __init__(self, name, err):
+            super().__init__(name, err)
+            self.name = name
+            self.err = err
+
+        def get_error(self):
+            return f"La variable {self.name} est invalide : {self.err}"
+
+    class TypeException(VarException):
+        def __init__(self, name, types, found):
+            super().__init__(name, types)
+            self.name = name
+
+            if isinstance(types, type):
+                types = [types]
+            types = [x.__name__ for x in types]
+            self.types = tuple(types)
+            self.found = found
+
+        def get_error(self):
+            return f"La variable {self.name} doit être de type {'('+', '.join(self.types)+')'}. Type trouvé {self.found.__class__.__name__}"
+
+    def __init__(self, name, default_value=None, required=False, validate=None,
+                 expected_types=None, help=None, is_meta=False, null=False):
+        self.name = name
+        self.default_value = default_value
+        self.required = required
+        self._validate = validate if validate else lambda x: x
+        self.expected_types = expected_types
+        self.help = help
+        self.null = null
+        self.is_meta = is_meta
+
+    def validate(self, obj):
+        if not hasattr(obj, self.name):
+            if self.required:
+                raise self.RequiredException(self.name)
+            return self.default_value
+        data = getattr(obj, self.name)
+        if self.expected_types:
+            if data is None and self.null:
+                pass
+            elif not isinstance(data, self.expected_types):
+                raise self.TypeException(self.name, self.expected_types, data)
+
+            data = self._validate(data)
+
+        return data
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__} {self.name}>"
+
+class VarStr(Var):
+    def __init__(self, name, default_value=None, required=False, validate=None, help=None,
+                 is_meta=False, expected_types=None, null=False):
+        super().__init__(name, default_value, required, validate, help=help,
+                         expected_types=(str, *get_types(expected_types)), is_meta=is_meta, null=null)
+
+class VarPath(Var):
+    def __init__(self, name, default_value=None, required=False, validate=None, help=None,
+                 is_meta=False, expected_types=None, null=False):
+        super().__init__(name, default_value, required, help=help, is_meta=is_meta,
+                         validate=validate or (lambda x: Path(x)),
+                         expected_types=(str, Path, *get_types(expected_types)), null=null)
+
+
+class VarInt(Var):
+    def __init__(self, name, default_value=None, required=False, validate=None, help=None,
+                 is_meta=False, expected_types=None, null=False):
+        super().__init__(name, default_value, required, validate, help=help, is_meta=is_meta,
+                         expected_types=(int, *get_types(expected_types)), null=null)
+
+class VarNumber(Var):
+    def __init__(self, name, default_value=None, required=False, validate=None, help=None,
+                 is_meta=False, expected_types=None, null=False):
+        super().__init__(name, default_value, required, validate, help=help, is_meta=is_meta,
+                         expected_types=(float, int, *get_types(expected_types)), null=null)
+
+class VarDict(Var):
+    def __init__(self, name, default_value=None, required=False, validate=None, help=None,
+                 is_meta=False, expected_types=None, null=False):
+        super().__init__(name, default_value, required, validate, help=help, is_meta=is_meta,
+                         expected_types=(dict, *get_types(expected_types)), null=null)
+
+
+class EnvHolder:
+
+    def __init__(self):
+        self._items = []
+
+    def __getitem__(self, item):
+        return getattr(self, item)
+
+    def __setitem__(self, key, value):
+        if key not in self._items:
+            self._items.append(key)
+
+        return setattr(self, key, value)
+
+    def items(self):
+        return [
+            (k, getattr(self, k)) for k in self._items
+        ]
+
+
+class Env:
+    _current_env = None
+    _vars_ = []
+    _name_ = None
+
+
+    class EnvException(MetadockerError):
+        def __init__(self, errors):
+            super().__init__(errors)
+            self.errors = errors
+
+
+        def get_error(self):
+            return "\n".join(repr(x) for x in self.errors)
+
+    def __init__(self, debug=False, **kwargs):
+        Env._current_env = self
+        self.root = get_root_dir()
+        self.debug = debug
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+    def check(self):
+        self.get_env()
+
+    def get_meta(self):
+        data= EnvHolder()
+        errors = []
+        for var in self._vars_:
+            if not var.is_meta: continue
+            try:
+                value = var.validate(self)
+            except Var.VarException as err:
+                errors.append(err)
+                continue
+            data[var.name] = value
+
+        if errors:
+            raise self.EnvException(errors)
+
+        for k,v in data.items():
+            setattr(data, k, v)
+        return data
+
+
+    def get_env(self):
+        data= EnvHolder()
+        errors = []
+        for var in self._vars_:
+            try:
+                value = var.validate(self)
+            except Var.VarException as err:
+                errors.append(err)
+                continue
+            data[var.name] = value
+
+        if errors:
+            raise self.EnvException(errors)
+
+        for k,v in data.items():
+            setattr(data, k, v)
+        return data
+
+    def generate_docker_file(self, docker : DockerBuilder, ):
+        raise NotImplementedError()
+
+
+    def init(self, docker : DockerBuilder):
+        raise NotImplementedError()
+
+
+    def build_docker_file(self, docker : DockerBuilder, output=None):
+        self.init(docker)
+        if not output:
+            temp = tempfile.TemporaryDirectory()
+            output = Path(temp.name) / "Dockerfile"
+        output = Path(output)
+        output.parent.mkdir(exist_ok=True, parents=True)
+
+        self.generate_docker_file(docker)
+        output.write_text(docker.get_file_content())
+
+    def create_empty_config(self):
+        def _value(v):
+            if v is None:
+                return "None"
+            if isinstance(v, (str, list, int, float, dict)):
+                if v == {}: return "{\n}"
+                if v == []: return "[\n]"
+                return json.dumps(v, indent=2)
+            return v
+
+        def _help(v):
+            return v.replace("\n", "\n#")
+
+        data = [
+                "#!/bin/env python3",
+                f"from metadocker.env import {self.__class__.__name__}",
+                f"env = {self.__class__.__name__}()",
+                f""
+        ]
+        for var in self._vars_:
+            if var.help:
+                data.append(f"# {var.help}")
+            if var.required:
+                data.append(f"env.{var.name}={_value(var.default_value)}")
+            else:
+                data.append(f"# env.{var.name}={_value(var.default_value)}")
+            data.append("")
+        data.append("# lancement ")
+        data.append("env.main()")
+        return "\n".join(data)
+
+    def main(self, args=None):
+        ArgsFromFile.main(self, args)
+

+ 163 - 0
src/metadocker/env/pythonserver.py

@@ -0,0 +1,163 @@
+import argparse
+import os
+import re
+
+from metadocker.docker import DockerBuilder
+from metadocker.env.base import Env, VarStr, VarInt, VarPath, VarDict
+from metadocker.register import register_env
+
+
+
+@register_env
+class PythonServerEnv(Env):
+    _name_ = "PythonServer"
+    _vars_ = [
+
+        VarStr("PROJECT", required=True, help="Le nom du projet", is_meta=True),
+        VarStr("PROJECT_VERSION", required=True, help="La version du projet", is_meta=True, null=True),
+
+        VarDict("ENV", {}, required=True, help="La liste des variable d'environnemnt à utiliser"),
+        VarDict("PORTS", {8101:800}, required=True, help="Le mapping des ports. En clé les ports de l'hote et en valeur les port du container"),
+        VarDict("VOLUMES", {"./data": "/data"}, required=True, help="Le mapping des ports. En clé les ports de l'hote et en valeur les port du container", is_meta=True, null=True),
+        VarDict("COPY", {}, required=True,  help="Dictionaire des fichier a copier: cle: fs hote valeur fs invite"),
+        VarPath("APP_DIR", "/data", required=True, help="Le dossier ou est lancé le projet"),
+
+        VarStr("PYTHON_VERSION", "3.11", help="La version de python à utiliser"),
+        VarStr("GIT_REPO", help="L'adresse du dépot git"),
+        VarStr("GIT_VERSION", help="La version (branche ou tag) à utiliser. Utiliser None pour rester sur le master", null=True),
+        VarStr("USER", os.environ["USER"], help="Le nom d'utilisateur à utiliser. L'utilisateur sera crée. Utiliser None pour rester en root"),#
+        VarInt("UID", os.getuid(), help="L'UID de l'utilisateur, uniquement si USER est défini"), #
+        VarPath("BUILD_DIR", "/build", help="Le dossier au sein du container ou est buildé le projet"),
+
+        VarStr("APT_PACKAGES", help="La liste des packages apt à installer (séparé par des espaces)"),
+
+        VarStr("RUN_BEFORE_SETUP", help="Une commande a utiliser juste avant de lancer la commande d'installation du module python"),
+        VarStr("RUN_AFTER_SETUP", help="Une commande a utiliser juste après avoir lancé la commande d'installation du module python"),
+        VarStr("INSTALL_CMD", "pip install .", help="La commande à utiliser pour installer le package python"),
+
+        VarStr("RUN_CMD", help="La commande à installer pour lancer l'application. Par défaut: python -m {PROJECT}"),
+   ]
+
+
+    def init(self, docker : DockerBuilder):
+        env = self.get_env()
+        meta = self.get_meta()
+        docker.meta(meta)
+        docker.init(
+            env.PROJECT,
+            env.PROJECT_VERSION
+        )
+        return env, meta
+
+    def _get_packages(self, env):
+        data = env.APT_PACKAGES
+        packages = []
+        if env.GIT_REPO:
+            packages.append("git")
+
+        if isinstance(data, ((tuple, list, set))):
+            packages.extend(data)
+        else:
+            packages.extend(re.sub("\s\+", " ", data).split(" "))
+        return packages
+
+
+    def _get_envs(self, env):
+        envs=[f'APP_DIR="{env.APP_DIR}"']
+        if env.ENV:
+            for k, v in env.ENV.items():
+                envs.append(f'{k}=\"{v}\"')
+        for k, v in env.items():
+            envs.append(f'CFG_{k}=\"{v}\"')
+        return "\\\n    ".join(envs)
+
+    def generate_docker_file(self, docker : DockerBuilder):
+
+        debug = self.debug
+        env, meta = self.init(docker)
+
+
+        docker.from_(f"python:{env.PYTHON_VERSION}-slim")
+
+        docker.env(self._get_envs(env))
+
+        if env.COPY:
+            for k, v in env.COPY.items():
+                docker.copy(k, v)
+
+        if env.PORTS:
+            docker.expose(*env.PORTS.values())
+
+
+        if env.USER:
+            docker.run(f'adduser --disabled-password --gecos "" --home "/home/{env.USER}"  '
+                           f'--shell "/sbin/nologin"  --uid "{env.UID}"  {env.USER}')
+
+        packages = self._get_packages(env)
+        if packages:
+            docker.run(f'apt update -y && apt install -y', *packages)
+
+        docker.run("mkdir -p", env.BUILD_DIR, env.APP_DIR)
+
+        if env.USER:
+            docker.run("mkdir -p", f"/home/{env.USER}", " ".join(env.VOLUMES.values()))
+            docker.run(f"chown -R {env.USER}:{env.USER}", env.BUILD_DIR, f"/home/{env.USER}",
+                       " ".join(env.VOLUMES.values()), env.APP_DIR)
+
+            if debug:
+                docker.run(f"echo 'root:root' | chpasswd")
+                docker.run(f"echo '{env.USER}:root' | chpasswd")
+
+            docker.user(env.USER)
+            docker.env("USER", env.USER)
+            docker.env("PATH", f"/home/{env.USER}/.local/bin:$PATH")
+
+
+        if env.VOLUMES:
+            docker.volume(" ".join(env.VOLUMES.values()))
+
+        docker.workdir(env.BUILD_DIR)
+
+        # docker.run("mkdir -p $HOME/.pip", "&&",
+        #            r"echo '[global]' > $HOME/.pip/pip.conf &&",
+        #            r"echo 'index-url = http://download.zope.org/simple' >> $HOME/.pip/pip.conf &&",
+        #            r"echo 'trusted-host = download.zope.org' >> $HOME/.pip/pip.conf &&",
+        #            r"echo 'extra-index-url= https://data.gautrais.eu/fanch/stable' >> $HOME/.pip/pip.conf")
+
+
+        if env.GIT_REPO:
+            if env.GIT_VERSION:
+                docker.run(f"git clone {env.GIT_REPO} . && git checkout {env.GIT_VERSION}")
+            else:
+                docker.run(f"git clone {env.GIT_REPO} . ")
+
+
+            if env.RUN_BEFORE_SETUP:
+                docker.run(env.RUN_BEFORE_SETUP)
+
+            docker.run("pip install .")
+
+            if env.RUN_AFTER_SETUP:
+                docker.run(env.RUN_AFTER_SETUP)
+        else:
+            if env.RUN_BEFORE_SETUP:
+                docker.run(env.RUN_BEFORE_SETUP)
+
+            if env.PROJECT_VERSION:
+                docker.run(f"pip install {env.PROJECT}=={env.PROJECT_VERSION}")
+            else:
+                docker.run(f"pip install {env.PROJECT}")
+
+            if env.RUN_AFTER_SETUP:
+                docker.run(env.RUN_AFTER_SETUP)
+
+
+        docker.workdir(env.APP_DIR)
+
+        if env.RUN_CMD:
+            docker.cmd(env.RUN_CMD)
+        else:
+            docker.run(f"python -m {env.PROJECT} migrate")
+            docker.cmd(f"python -m {env.PROJECT} run")
+
+

+ 8 - 0
src/metadocker/main.py

@@ -0,0 +1,8 @@
+from pathlib import Path
+
+
+
+class PythonConfig:
+
+    def __init__(self, filename):
+        self.filename = Path(filename)

+ 12 - 0
src/metadocker/register.py

@@ -0,0 +1,12 @@
+
+
+registered_env = {}
+
+def register_env(x):
+    global  registered_env
+    registered_env[x._name_.lower()] = x
+    return x
+
+def load_env():
+    from metadocker import  env
+    return registered_env

+ 0 - 0
src/metadocker/scripts/__init__.py


+ 7 - 0
src/metadocker/scripts/metadocker

@@ -0,0 +1,7 @@
+#!/bin/env python3
+from metadocker.cmdline.base import MainArgs
+
+def main():
+    MainArgs.main(None)
+
+main()

+ 0 - 0
tests/__init__.py