From 2db971234a406ac326c9f9b81c516a6b5791e5f4 Mon Sep 17 00:00:00 2001 From: jpic Date: Mon, 17 Feb 2020 01:00:59 +0100 Subject: [PATCH] Trying to get somewhere with traefik module --- shlax/actions/__init__.py | 1 + shlax/actions/base.py | 20 +++++++++++-- shlax/actions/htpasswd.py | 29 +++++++++++++++++++ shlax/cli.py | 31 ++++++++++----------- shlax/repo/traefik.py | 15 ++++++++-- shlax/strategies/pod.py | 24 ++++++++++------ shlax/strategies/script.py | 15 +++++++--- shlax/targets/docker.py | 57 ++++++++++++++++++++++++++------------ shlax/targets/localhost.py | 13 ++++++++- 9 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 shlax/actions/htpasswd.py diff --git a/shlax/actions/__init__.py b/shlax/actions/__init__.py index 44accfa..f3514b0 100644 --- a/shlax/actions/__init__.py +++ b/shlax/actions/__init__.py @@ -1,6 +1,7 @@ from .copy import Copy from .packages import Packages # noqa from .base import Action # noqa +from .htpasswd import Htpasswd from .run import Run # noqa from .pip import Pip from .service import Service diff --git a/shlax/actions/base.py b/shlax/actions/base.py index d2e0e76..b09fec2 100644 --- a/shlax/actions/base.py +++ b/shlax/actions/base.py @@ -1,3 +1,4 @@ +from copy import deepcopy import functools import inspect import importlib @@ -25,9 +26,17 @@ class Action: ), ) - def __init__(self, *args, **kwargs): + def __init__(self, *args, doc=None, **kwargs): self.args = args self.kwargs = kwargs + self.call_args = [] + self.call_kwargs = {} + self._doc = doc + self.menu = { + name: value + for name, value in kwargs.items() + if isinstance(value, Action) + } @property def context(self): @@ -106,8 +115,8 @@ class Action: return Output(**kwargs) async def __call__(self, *args, **kwargs): - self.call_args = args - self.call_kwargs = kwargs + self.call_args = list(self.call_args) + list(args) + self.call_kwargs.update(kwargs) self.output = self.output_factory(*args, **kwargs) self.output_start() self.status = 'running' @@ -209,3 +218,8 @@ class Action: from ..strategies.script import Actions self.actions = Actions(self, [p]) return p + + def bind(self, *args): + clone = deepcopy(self) + clone.call_args = args + return clone diff --git a/shlax/actions/htpasswd.py b/shlax/actions/htpasswd.py new file mode 100644 index 0000000..54d6964 --- /dev/null +++ b/shlax/actions/htpasswd.py @@ -0,0 +1,29 @@ +import hashlib +import secrets +import string + +from .base import Action + + +class Htpasswd(Action): + def __init__(self, path, user, *args, **kwargs): + self.path = path + self.user = user + super().__init__(*args, **kwargs) + + async def call(self, *args, **kwargs): + found = False + htpasswd = await self.exec('cat', self.path, raises=False) + if htpasswd.rc == 0: + for line in htpasswd.out.split('\n'): + if line.startswith(self.user + ':'): + found = True + break + + if not found: + self.password = ''.join(secrets.choice( + string.ascii_letters + string.digits + ) for i in range(20)) + hashed = hashlib.sha1(self.password.encode('utf8')) + line = f'{self.user}:\\$sha1\\${hashed.hexdigest()}' + await self.exec(f'echo {line} >> {self.path}') diff --git a/shlax/cli.py b/shlax/cli.py index 8a1eb26..283c2f8 100644 --- a/shlax/cli.py +++ b/shlax/cli.py @@ -36,31 +36,30 @@ class ConsoleScript(cli2.ConsoleScript): self.shlaxfile = Shlaxfile() self.shlaxfile.parse(shlaxfile) - if len(self.shlaxfile.actions) == 1 and 'main' in self.shlaxfile.actions: + self._doc = inspect.getdoc(mod) + if 'main' in self.shlaxfile.actions: action = self.shlaxfile.actions['main'] - self._doc = inspect.getdoc(action) - for name, doc in self.shlaxfile.actions['main'].doc.items(): + for name, child in self.shlaxfile.actions['main'].menu.items(): self[name] = cli2.Callable( name, - action.callable(), - options={ - k: cli2.Option(name=k, **v) - for k, v in action.options.items() - }, - color=getattr(action, 'color', cli2.YELLOW), - doc=doc, - ) - else: - for name, action in self.shlaxfile.actions.items(): - self[name] = cli2.Callable( - name, - action.callable(), + child.callable(), options={ k: cli2.Option(name=k, **v) for k, v in action.options.items() }, color=getattr(action, 'color', cli2.YELLOW), ) + for name, action in self.shlaxfile.actions.items(): + self[name] = cli2.Callable( + name, + action.callable(), + options={ + k: cli2.Option(name=k, **v) + for k, v in action.options.items() + }, + color=getattr(action, 'color', cli2.YELLOW), + doc=inspect.getdoc(getattr(action, name, None)) or action._doc, + ) else: from shlax import repo path = repo.__path__._path[0] diff --git a/shlax/repo/traefik.py b/shlax/repo/traefik.py index ee6b280..eccab07 100755 --- a/shlax/repo/traefik.py +++ b/shlax/repo/traefik.py @@ -5,7 +5,8 @@ Manage a traefik container maintained by Shlax community. from shlax import * -main = Container( +main = Docker( + name='traefik', image='traefik:v2.0.0', networks=['web'], command=[ @@ -26,5 +27,15 @@ main = Container( 'traefik.http.routers.traefik.rule=Host(`{{ url.split("/")[2] }}`)', 'traefik.http.routers.traefik.service=api@internal', 'traefik.http.routers.traefik.entrypoints=web', - ] + ], + doc='Current traefik instance', ) + +install = Script( + Htpasswd('./htpasswd', 'root'), + main.bind('up'), + doc='Deploy a Traefik instance', +) + +up = main.bind('up') +rm = main.bind('rm') diff --git a/shlax/strategies/pod.py b/shlax/strategies/pod.py index fc22ca7..e6ce14a 100644 --- a/shlax/strategies/pod.py +++ b/shlax/strategies/pod.py @@ -1,5 +1,6 @@ import os from .script import Script +from ..image import Image class Container(Script): @@ -8,18 +9,23 @@ class Container(Script): Such wow """ - doc = dict( - build='Execute the Build script', - test='Execute the Test script (if any)', - push='Push the container (if Build script defines commit kwarg)', - ) + def __init__(self, *args, **kwargs): + kwargs.setdefault('start', dict()) + super().__init__(*args, **kwargs) async def call(self, *args, **kwargs): - if not args or 'build' in args: + if step('build'): await self.kwargs['build'](**kwargs) - self.image = self.kwargs['build'].image + self.image = self.kwargs['build'].image + else: + self.image = kwargs.get('image', 'alpine') + if isinstance(self.image, str): + self.image = Image(self.image) - if not args or 'test' in args: + if step('install'): + await self.install(*args, **kwargs) + + if step('test'): self.output.test(self) await self.action('Docker', *self.kwargs['test'].actions, @@ -28,7 +34,7 @@ class Container(Script): workdir='/app', )(**kwargs) - if not args or 'push' in args: + if step('push'): await self.image.push(action=self) #name = kwargs.get('name', os.getcwd()).split('/')[-1] diff --git a/shlax/strategies/script.py b/shlax/strategies/script.py index bd8b0d7..e4b69b6 100644 --- a/shlax/strategies/script.py +++ b/shlax/strategies/script.py @@ -14,16 +14,17 @@ class Actions(list): self.append(action) def append(self, value): - value = copy.deepcopy(value) - value.parent = self.owner - value.status = 'pending' - super().append(value) + action = copy.deepcopy(value) + action.parent = self.owner + action.status = 'pending' + super().append(action) class Script(Action): contextualize = ['shargs', 'exec', 'rexec', 'env', 'which', 'copy'] def __init__(self, *actions, **kwargs): + self.home = kwargs.pop('home', os.getcwd()) super().__init__(**kwargs) self.actions = Actions(self, actions) @@ -32,3 +33,9 @@ class Script(Action): result = await action(*args, **kwargs) if action.status != 'success': break + + def pollute(self, gbls): + for name, script in self.kwargs.items(): + if not isinstance(script, Script): + continue + gbls[name] = script diff --git a/shlax/targets/docker.py b/shlax/targets/docker.py index 331c8b9..f97d24d 100644 --- a/shlax/targets/docker.py +++ b/shlax/targets/docker.py @@ -11,10 +11,12 @@ class Docker(Localhost): def __init__(self, *args, **kwargs): self.image = kwargs.get('image', 'alpine') + self.name = kwargs.get('name', os.getcwd().split('/')[-1]) + if not isinstance(self.image, Image): self.image = Image(self.image) + super().__init__(*args, **kwargs) - self.context['ctr'] = None def shargs(self, *args, daemon=False, **kwargs): if args[0] == 'docker': @@ -26,9 +28,9 @@ class Docker(Localhost): args, kwargs = super().shargs(*args, **kwargs) - if self.context['ctr']: + if self.name: executor = 'exec' - extra = [self.context['ctr']] + extra = [self.name] return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + list(args), kwargs executor = 'run' @@ -39,23 +41,44 @@ class Docker(Localhost): return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + [str(self.image)] + list(args), kwargs async def call(self, *args, **kwargs): - name = kwargs.get('name', os.getcwd()).split('/')[-1] - self.context['ctr'] = ( - await self.exec( - 'docker', 'ps', '-aq', '--filter', - 'name=' + name, - raises=False - ) - ).out.split('\n')[0] + def step(step): + return not args or step in args - if 'recreate' in args and self.context['ctr']: - await self.exec('docker', 'rm', '-f', self.context['ctr']) - self.context['ctr'] = None + # self.name = ( + # await self.exec( + # 'docker', 'ps', '-aq', '--filter', + # 'name=' + self.name, + # raises=False + # ) + # ).out.split('\n')[0] - if self.context['ctr']: - self.context['ctr'] = (await self.exec('docker', 'start', name)).out + if step('rm'): + await self.rm(*args, **kwargs) + + if step('down') and self.name: + await self.exec('docker', 'down', '-f', self.name) + + if step('up'): + await self.up(*args, **kwargs) return await super().call(*args, **kwargs) + async def rm(self, *args, **kwargs): + return await self.exec('docker', 'rm', '-f', self.name) + + async def down(self, *args, **kwargs): + """Remove instance, except persistent data if any""" + if self.name: + self.name = (await self.exec('docker', 'start', self.name)).out + else: + self.name = (await self.exec('docker', 'run', '-d', '--name', self.name)).out + + async def up(self, *args, **kwargs): + """Perform start or run""" + if self.name: + self.name = (await self.exec('docker', 'start', self.name)).out + else: + self.id = (await self.exec('docker', 'run', '-d', '--name', self.name)).out + async def copy(self, *args): src = args[:-1] dst = args[-1] @@ -70,7 +93,7 @@ class Docker(Localhost): else: args = ['docker', 'copy', self.ctr, s, dst] ''' - args = ['docker', 'cp', s, self.context['ctr'] + ':' + dst] + args = ['docker', 'cp', s, self.name + ':' + dst] procs.append(self.exec(*args)) return await asyncio.gather(*procs) diff --git a/shlax/targets/localhost.py b/shlax/targets/localhost.py index d424179..829e78f 100644 --- a/shlax/targets/localhost.py +++ b/shlax/targets/localhost.py @@ -8,6 +8,11 @@ from ..strategies.script import Script class Localhost(Script): root = '/' + contextualize = Script.contextualize + ['home'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.home = kwargs.pop('home', os.getcwd()) def shargs(self, *args, **kwargs): user = kwargs.pop('user', None) @@ -46,6 +51,9 @@ class Localhost(Script): async def env(self, name): return (await self.exec('echo $' + name)).out + async def exists(self, *paths): + proc = await self.exec('type ' + ' '.join(cmd), raises=False) + async def which(self, *cmd): """ Return the first path to the cmd in the container. @@ -61,7 +69,10 @@ class Localhost(Script): return result async def copy(self, *args): - args = ['cp', '-ra'] + list(args) + if args[-1].startswith('./'): + args = list(args) + args[-1] = self.home + '/' + args[-1][2:] + args = ['cp', '-rua'] + list(args) return await self.exec(*args) async def mount(self, *dirs):