diff --git a/podctl/build.py b/podctl/build.py index e9c9472..f0b10e3 100644 --- a/podctl/build.py +++ b/podctl/build.py @@ -11,46 +11,67 @@ from .script import Script class Build(Script): - def __init__(self, container): + """ + The build script iterates over visitors and runs the build functions, it + also provides wrappers around the buildah command. + """ + + def __init__(self): super().__init__() - self.container = container - self.log = [] self.mounts = dict() async def config(self, line): + """Run buildah config.""" return await self.append(f'buildah config {line} {self.ctr}') async def copy(self, src, dst): + """Run buildah copy to copy a file from host into container.""" return await self.append(f'buildah copy {self.ctr} {src} {dst}') - async def exec(self, *args, **kwargs): - kwargs.setdefault('prefix', self.container.name) - proc = await Proc(*args, **kwargs)() - if kwargs.get('wait', True): - await proc.wait() - return proc - async def cexec(self, *args, user=None, **kwargs): - _args = ['buildah', 'run', self.ctr] + """Execute a command in the container.""" + _args = ['buildah', 'run'] if user: _args += ['--user', user] - _args += ['--', 'sh', '-euc'] - return await self.exec(*(_args + list(args))) + _args += [self.ctr, '--', 'sh', '-euc'] + return await self.exec(*(_args + [' '.join([str(a) for a in args])])) + + async def crexec(self, *args, **kwargs): + """Execute a command in the container as root.""" + kwargs['user'] = 'root' + return await self.cexec(*args, **kwargs) async def mount(self, src, dst): + """Mount a host directory into the container.""" target = self.mnt / str(dst)[1:] await self.exec(f'mkdir -p {src} {target}') await self.exec(f'mount -o bind {src} {target}') self.mounts[src] = dst async def umounts(self): + """Unmount all mounted directories from the container.""" for src, dst in self.mounts.items(): await self.exec('umount', self.mnt / str(dst)[1:]) async def umount(self): + """Unmount the buildah container with buildah unmount.""" await self.exec(f'buildah unmount {self.ctr}') - def which(self, cmd): - for path in self.container.paths: - if os.path.exists(os.path.join(self.mnt, path[1:], cmd)): - return True + async def paths(self): + """Return the list of $PATH directories""" + return (await self.cexec('echo $PATH')).out.split(':') + + async def which(self, cmd): + """ + Return the first path to the cmd in the container. + + If cmd argument is a list then it will try all commands. + """ + if not isinstance(cmd, (list, tuple)): + cmd = [cmd] + + for path in await self.paths(): + for c in cmd: + p = os.path.join(self.mnt, path[1:], c) + if os.path.exists(p): + return p[len(str(self.mnt)):] diff --git a/podctl/console_script.py b/podctl/console_script.py index 72ebadf..615d021 100644 --- a/podctl/console_script.py +++ b/podctl/console_script.py @@ -4,91 +4,66 @@ docker & docker-compose frustrated me, podctl unfrustrates me. import asyncio import cli2 -import importlib +import inspect import os import sys from .container import Container from .pod import Pod -from .proc import WrongResult +from .exceptions import Mistake, WrongResult from .service import Service -@cli2.option('debug', help='Print debug output', color=cli2.GREEN, alias='d') -async def build(*services_or_flags, **kwargs): - flags = [] - services = [] - for arg in services_or_flags: - if arg.startswith('-') or arg.startswith('+'): - flags.append(arg) - else: - services.append(arg) - - if services: - services = { - k: v - for k, v in console_script.pod.services.items() - if k in services - } - else: - services = console_script.pod.services - - procs = [] - asyncio.events.get_event_loop() - for name, service in services.items(): - service.container.name = name - service.container.flags = flags - procs.append(service.container.script('build', flags)) - - try: - result = await asyncio.gather(*procs) - except WrongResult: - sys.exit(1) - - class ConsoleScript(cli2.ConsoleScript): - def __setitem__(self, name, cb): - if name != 'help': - cli2.option( - 'file', - alias='f', - help='Path to pod definition (default: pod.py)', - color=cli2.YELLOW, - default='pod.py', - )(cb.target) - cli2.option( - 'home', - alias='h', - help=f'Pod home (default is cwd: {os.getcwd()})', - color=cli2.YELLOW, - default=os.getcwd(), - )(cb.target) - super().__setitem__(name, cb) + def __call__(self, *args, **kwargs): + import inspect + from podctl.podfile import Podfile + self.podfile = Podfile.factory(os.getenv('PODFILE', 'pod.py')) + for name in self.podfile.pod.script_names(): + self[name] = self.podfile.pod.script(name) + ''' + ee = self.script(pod, name) + ee.__doc__ = inspect.getdoc(cb) + self[name] = cli2.Callable(name, ee) + ''' - def call(self, command): - if command.name != 'help': - self.path = self.parser.options['file'] - self.home = self.parser.options['home'] - self.containers = dict() - self.pods = dict() - self.pod = None - spec = importlib.util.spec_from_file_location('pod', self.path) - pod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(pod) - for name, value in pod.__dict__.items(): - if isinstance(value, Container): - self.containers[name] = value - elif isinstance(value, Pod): - self.pods[name] = value + super().__call__(*args, **kwargs) - if 'pod' in self.pods: - self.pod = self.pods['pod'] - if not self.pod: - self.pod = Pod(*[ - Service(name, value, restart='no') - for name, value in self.containers.items() - ]) - return super().call(command) + @staticmethod + def script(script_name): + async def script(*services_or_flags, **options): + flags = [] + services = [] + for arg in services_or_flags: + if arg.startswith('-') or arg.startswith('+'): + flags.append(arg) + else: + services.append(arg) + + if services: + services = { + k: v + for k, v in console_script.pod.services.items() + if k in services + } + else: + services = console_script.pod.services + + procs = [] + asyncio.events.get_event_loop() + for name, service in services.items(): + service.container.name = name + service.container.flags = flags + procs.append(service.container.script(script_name, flags)) + + try: + result = await asyncio.gather(*procs) + except Mistake as e: + print(e) + sys.exit(1) + except WrongResult: + sys.exit(1) + return script console_script = ConsoleScript(__doc__).add_module('podctl.console_script') diff --git a/podctl/container.py b/podctl/container.py index f9ba5c0..b609dbd 100644 --- a/podctl/container.py +++ b/podctl/container.py @@ -3,28 +3,17 @@ import os import shlex from .build import Build +from .run import Run from .visitable import Visitable class Container(Visitable): default_scripts = dict( - build=Build, + build=Build(), + run=Run(), ) - paths = [ - '/bin', - '/sbin', - '/usr/bin', - '/usr/sbin', - ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.log = [] - - async def script(self, name, flags, loop=None): - self.loop = loop or asyncio.events.get_event_loop() - self.packages = [] - for visitor in self.visitors: - self.packages += getattr(visitor, 'packages', []) - result = await super().script(name, loop) - return result + def script(self, name): + script = super().script(name) + script.container = self + return script diff --git a/podctl/exceptions.py b/podctl/exceptions.py new file mode 100644 index 0000000..1873e11 --- /dev/null +++ b/podctl/exceptions.py @@ -0,0 +1,10 @@ +class PodctlException(Exception): + pass + + +class Mistake(PodctlException): + pass + + +class WrongResult(PodctlException): + pass diff --git a/podctl/pod.py b/podctl/pod.py index 642775e..89cfa65 100644 --- a/podctl/pod.py +++ b/podctl/pod.py @@ -1,6 +1,21 @@ +import os + +from .script import Script +from .visitable import Visitable -class Pod: - def __init__(self, *services, **scripts): - self.scripts = scripts - self.services = {s.name: s for s in services} +class Pod(Visitable): + def script_names(self): + for name in self.scripts.keys(): + yield name + + for visitor in self.visitors: + for script in visitor.scripts.keys(): + yield script + + def script(self, name): + for script_name in self.scripts.keys(): + if script_name == name: + break + script.pod = self + return script diff --git a/podctl/podfile.py b/podctl/podfile.py new file mode 100644 index 0000000..92bad9f --- /dev/null +++ b/podctl/podfile.py @@ -0,0 +1,33 @@ +import importlib + +from .container import Container +from .pod import Pod + + +class Podfile: + def __init__(self, pods, containers): + self.pods = pods + self.containers = containers + if not self.pods: + self.pods['pod'] = Pod(*containers.values()) + + @property + def pod(self): + return self.pods['pod'] + + @classmethod + def factory(cls, path): + containers = dict() + pods = dict() + spec = importlib.util.spec_from_file_location('pod', path) + pod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(pod) + for name, value in pod.__dict__.items(): + if isinstance(value, Container): + containers[name] = value + value.name = name + elif isinstance(value, Pod): + pods[name] = value + value.name = name + + return cls(pods, containers) diff --git a/podctl/proc.py b/podctl/proc.py new file mode 100644 index 0000000..b8842e0 --- /dev/null +++ b/podctl/proc.py @@ -0,0 +1,123 @@ +""" +Asynchronous process execution wrapper. +""" + +import asyncio +import shlex +import sys + +from .exceptions import WrongResult + + +class PrefixStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol): + """ + Internal subprocess stream protocol to add a prefix in front of output to + make asynchronous output readable. + """ + + def __init__(self, prefix, *args, **kwargs): + self.prefix = prefix + super().__init__(*args, **kwargs) + + def pipe_data_received(self, fd, data): + if fd in (1, 2): + for line in data.split(b'\n'): + if not line: + continue + sys.stdout.buffer.write( + self.prefix.encode('utf8') + b' | ' + line + b'\n' + ) + sys.stdout.flush() + super().pipe_data_received(fd, data) + +def protocol_factory(prefix): + def _p(): + return PrefixStreamProtocol( + prefix, + limit=asyncio.streams._DEFAULT_LIMIT, + loop=asyncio.events.get_event_loop() + ) + return _p +''' + +async def proc(args, prefix=None, wait=True, raises=True): + loop = asyncio.events.get_event_loop() + transport, protocol = await loop.subprocess_exec( + protocol_factory(prefix), *args) + proc = asyncio.subprocess.Process(transport, protocol, loop) + + if wait: + stdout, stderr = await proc.communicate() + log['result'] = await proc.wait() + + if raises and log['result']: + raise WrongResult() + + if wait: + return log + + return proc +''' + + +class Proc: + """ + Subprocess wrapper. + + Example usage:: + + proc = Proc('find', '/', prefix='containername') + + await proc() # execute + + print(proc.out) # stdout + print(proc.err) # stderr + print(proc.rc) # return code + """ + + def __init__(self, *args, prefix=None, raises=True): + if len(args) == 1: + if isinstance(args[0], (list, tuple)): + args = args[0] + else: + args = ['sh', '-euc', ' '.join(args)] + self.args = [str(a) for a in args] + self.cmd = shlex.join(self.args) + self.prefix = prefix + self.raises = raises + self.called = False + self.communicated = False + + async def __call__(self, wait=True): + if self.called: + raise Exception('Already called: ' + self.cmd) + + print(f'{self.prefix} | + {self.cmd}') + + loop = asyncio.events.get_event_loop() + transport, protocol = await loop.subprocess_exec( + protocol_factory(self.prefix), *self.args) + self.proc = asyncio.subprocess.Process(transport, protocol, loop) + self.called = True + + if wait: + await self.wait() + + return self + + async def communicate(self): + self.out_raw, self.err_raw = await self.proc.communicate() + self.out = self.out_raw.decode('utf8').strip() + self.err = self.err_raw.decode('utf8').strip() + self.rc = self.proc.returncode + self.communicated = True + return self + + async def wait(self): + if not self.called: + await self() + if not self.communicated: + await self.communicate() + if self.raises and self.proc.returncode: + raise WrongResult() + return self diff --git a/podctl/run.py b/podctl/run.py new file mode 100644 index 0000000..de826f1 --- /dev/null +++ b/podctl/run.py @@ -0,0 +1,5 @@ +from .script import Script + + +class Run(Script): + """Run a container""" diff --git a/podctl/script.py b/podctl/script.py index 11cd3e1..edbe0c3 100644 --- a/podctl/script.py +++ b/podctl/script.py @@ -1,5 +1,41 @@ import textwrap +from .proc import Proc + class Script: - pass + async def exec(self, *args, **kwargs): + """Execute a command on the host.""" + kwargs.setdefault('prefix', self.container.name) + proc = await Proc(*args, **kwargs)() + if kwargs.get('wait', True): + await proc.wait() + return proc + + async def __call__(self, name, loop=None): + script = copy(self.scripts[name]) + script.loop = loop or asyncio.events.get_event_loop() + results = [] + + async def clean(): + for visitor in self.visitors: + if hasattr(visitor, 'clean_' + name): + result = getattr(visitor, 'clean_' + name)(script) + if result: + await result + + for prefix in ('init_', 'pre_', '', 'post_', 'clean_'): + method = prefix + name + for visitor in self.visitors: + if not hasattr(visitor, method): + continue + + rep = {k: v if not isinstance(v, object) else type(v).__name__ for k, v in visitor.__dict__.items()} + print(self.name + ' | ', type(visitor).__name__, method, rep) + result = getattr(visitor, method)(script) + if result: + try: + await result + except Exception as e: + await clean() + raise diff --git a/podctl/visitable.py b/podctl/visitable.py index 1b0533e..e03bb07 100644 --- a/podctl/visitable.py +++ b/podctl/visitable.py @@ -1,3 +1,4 @@ +import asyncio from copy import copy @@ -7,36 +8,12 @@ class Visitable: def __init__(self, *visitors, **scripts): self.visitors = list(visitors) self.scripts = scripts or { - k: v(self) for k, v in self.default_scripts.items() + k: v for k, v in self.default_scripts.items() } - async def script(self, name, loop): + def script(self, name): script = copy(self.scripts[name]) - script.loop = loop - results = [] - - async def clean(): - for visitor in self.visitors: - if hasattr(visitor, 'clean_' + name): - result = getattr(visitor, 'clean_' + name)(self) - if result: - await result - - for prefix in ('init_', 'pre_', '', 'post_', 'clean_'): - method = prefix + name - for visitor in self.visitors: - if not hasattr(visitor, method): - continue - - rep = {k: v if not isinstance(v, object) else type(v).__name__ for k, v in visitor.__dict__.items()} - print(self.name + ' | ', type(visitor).__name__, method, rep) - result = getattr(visitor, method)(script) - if result: - try: - await result - except Exception as e: - await clean() - raise + return script def visitor(self, name): for visitor in self.visitors: diff --git a/podctl/visitors/__init__.py b/podctl/visitors/__init__.py index c256b53..1c4f181 100644 --- a/podctl/visitors/__init__.py +++ b/podctl/visitors/__init__.py @@ -9,5 +9,6 @@ from .mount import Mount # noqa from .packages import Packages # noqa from .pip import Pip # noqa from .run import Run # noqa -from .template import Template # noqa +from .template import Append, Template # noqa from .user import User # noqa +from .uwsgi import uWSGI # noqa diff --git a/podctl/visitors/base.py b/podctl/visitors/base.py index aa97f20..684eef8 100644 --- a/podctl/visitors/base.py +++ b/podctl/visitors/base.py @@ -9,5 +9,7 @@ class Base: script.ctr = Path((await script.exec('buildah', 'from', self.base)).out) script.mnt = Path((await script.exec('buildah', 'mount', script.ctr)).out) - async def post_build(self, script): + async def clean_build(self, script): await script.umounts() + await script.umount() + proc = await script.exec('buildah', 'rm', script.ctr, raises=False) diff --git a/podctl/visitors/commit.py b/podctl/visitors/commit.py index a35bd45..0c0ed73 100644 --- a/podctl/visitors/commit.py +++ b/podctl/visitors/commit.py @@ -42,20 +42,24 @@ class Commit: # filter out tags which resolved to None self.tags = [t for t in self.tags if t is not None] + # default tag by default ... + if not self.tags: + self.tags = ['latest'] + async def post_build(self, script): - await script.exec( + self.sha = (await script.exec( 'buildah', 'commit', '--format=' + self.format, script.ctr, - ) + )).out if 'master' in self.tags: self.tags.append('latest') if self.tags: tags = ' '.join([f'{self.repo}:{tag}' for tag in self.tags]) - await script.run('buildah', 'tag', self.repo, ' '.join(tags)) + await script.exec('buildah', 'tag', self.sha, self.repo, tags) if self.push: user = os.getenv('DOCKER_USER') @@ -72,5 +76,12 @@ class Commit: ) for tag in self.tags: - await script.run('podman', 'push', f'{self.repo}:{tag}') + await script.exec('podman', 'push', f'{self.repo}:{tag}') await script.umount() + + async def run(self, script): + await script.exec( + 'podman', 'run', '-d', + '--name', script.container.name, + ':'.join((self.repo, self.tags[0])), + ) diff --git a/podctl/visitors/packages.py b/podctl/visitors/packages.py index 242fa92..1a9be46 100644 --- a/podctl/visitors/packages.py +++ b/podctl/visitors/packages.py @@ -1,4 +1,5 @@ import asyncio +import copy from datetime import datetime from glob import glob @@ -8,6 +9,14 @@ from textwrap import dedent class Packages: + """ + The Packages visitor wraps around the container's package manager. + + It's a central piece of the build process, and does iterate over other + container visitors in order to pick up packages. For example, the Pip + visitor will declare ``self.packages = dict(apt=['python3-pip'])``, and the + Packages visitor will pick it up. + """ mgrs = dict( apk=dict( update='apk update', @@ -31,99 +40,113 @@ class Packages: ), ) + installed = [] + def __init__(self, *packages, **kwargs): - self.packages = list([ - dedent(l).strip().replace('\n', ' ') for l in packages - ]) + self.packages = [] + + for package in packages: + line = dedent(package).strip().replace('\n', ' ') + self.packages += line.split(' ') + self.mgr = kwargs.pop('mgr') if 'mgr' in kwargs else None @property - def cache(self): + def cache_root(self): if 'CACHE_DIR' in os.environ: - return os.path.join(os.getenv('CACHE_DIR'), self.mgr) + return os.path.join(os.getenv('CACHE_DIR')) else: - return os.path.join(os.getenv('HOME'), '.cache', self.mgr) + return os.path.join(os.getenv('HOME'), '.cache') async def init_build(self, script): - paths = ('bin', 'sbin', 'usr/bin', 'usr/sbin') - for mgr, cmds in self.mgrs.items(): - for path in paths: - if (script.mnt / path / mgr).exists(): + cached = script.container.variable('mgr') + if cached: + self.mgr = cached + else: + for mgr, cmds in self.mgrs.items(): + if await script.which(mgr): self.mgr = mgr - self.cmds = cmds break + if not self.mgr: raise Exception('Packages does not yet support this distro') + self.cmds = self.mgrs[self.mgr] + + async def update(self, script): + # run pkgmgr_setup functions ie. apk_setup + cachedir = await getattr(self, self.mgr + '_setup')(script) + + lastupdate = None + if os.path.exists(cachedir + '/lastupdate'): + with open(cachedir + '/lastupdate', 'r') as f: + try: + lastupdate = int(f.read().strip()) + except: + pass + + now = int(datetime.now().strftime('%s')) + # cache for a week + if not lastupdate or now - lastupdate > 604800: + # crude lockfile implementation, should work against *most* + # race-conditions ... + lockfile = cachedir + '/update.lock' + if not os.path.exists(lockfile): + with open(lockfile, 'w+') as f: + f.write(str(os.getpid())) + + try: + await script.cexec(self.cmds['update']) + finally: + os.unlink(lockfile) + + with open(cachedir + '/lastupdate', 'w+') as f: + f.write(str(now)) + else: + while os.path.exists(lockfile): + print(f'{script.container.name} | Waiting for update ...') + await asyncio.sleep(1) + async def build(self, script): if not getattr(script.container, '_packages_upgraded', None): - # run pkgmgr_setup functions ie. apk_setup - await getattr(self, self.mgr + '_setup')(script) - # first run on container means inject visitor packages - self.packages += script.container.packages + await self.update(script) await script.cexec(self.cmds['upgrade']) - script.container._packages_upgraded = True - await script.cexec(' '.join([self.cmds['install']] + self.packages)) + # first run on container means inject visitor packages + packages = [] + for visitor in script.container.visitors: + pp = getattr(visitor, 'packages', None) + if pp: + if isinstance(pp, list): + packages += pp + elif self.mgr in pp: + packages += pp[self.mgr] + + script.container._packages_upgraded = True + else: + packages = self.packages + + await script.crexec(*self.cmds['install'].split(' ') + packages) async def apk_setup(self, script): - await script.mount(self.cache, f'/var/cache/{self.mgr}') + cachedir = os.path.join(self.cache_root, self.mgr) + await script.mount(cachedir, '/var/cache/apk') # special step to enable apk cache await script.cexec('ln -s /var/cache/apk /etc/apk/cache') - - # do we have to update ? - update = False - for f in glob(self.cache + '/APKINDEX*'): - mtime = os.stat(f).st_mtime - now = int(datetime.now().strftime('%s')) - # expect hacker to have internet at least once a week - if now - mtime > 604800: - update = True - break - else: - update = True - - if update: - await self.apk_update(script) - - async def apk_update(self, script): - while os.path.exists(self.cache + '/update'): - print(f'{script.container.name} | Waiting for update ...') - await asyncio.sleep(1) - return # update was done by another job - - with open(self.cache + '/update', 'w+') as f: - f.write(str(os.getpid())) - - try: - await script.cexec(self.cmds['update']) - except: - raise - finally: - os.unlink(self.cache + '/update') + return cachedir async def dnf_setup(self, script): await script.mount(self.cache, f'/var/cache/{self.mgr}') await script.run('echo keepcache=True >> /etc/dnf/dnf.conf') async def apt_setup(self, script): - cache = self.cache + '/$(source $mnt/etc/os-release; echo $VERSION_CODENAME)/' # noqa - await script.run('rm /etc/apt/apt.conf.d/docker-clean') - cache_archives = os.path.join(self.cache, 'archives') + codename = (await script.exec( + f'source {script.mnt}/etc/os-release; echo $VERSION_CODENAME' + )).out + cachedir = os.path.join(self.cache_root, self.mgr, codename) + await script.cexec('rm /etc/apt/apt.conf.d/docker-clean') + cache_archives = os.path.join(cachedir, 'archives') await script.mount(cache_archives, f'/var/cache/apt/archives') - cache_lists = os.path.join(self.cache, 'lists') + cache_lists = os.path.join(cachedir, 'lists') await script.mount(cache_lists, f'/var/lib/apt/lists') - - await script.run(self.cmds['update']) - """ - await script.append(f''' - old="$(find {cache_lists} -name lastup -mtime +3)" - if [ -n "$old" ] || ! ls {cache_lists}/lastup; then - until [ -z $(lsof /var/lib/dpkg/lock) ]; do sleep 1; done - {script._run(self.cmds['update'])} - touch {cache_lists}/lastup - else - echo Cache recent enough, skipping index update. - fi - ''') - """ + return cachedir diff --git a/podctl/visitors/pip.py b/podctl/visitors/pip.py index 44eaa81..4f41f75 100644 --- a/podctl/visitors/pip.py +++ b/podctl/visitors/pip.py @@ -3,16 +3,19 @@ import os class Pip: + packages = dict( + apt=['python3-pip'], + ) + def __init__(self, *pip_packages, pip=None, requirements=None): self.pip_packages = pip_packages - self.pip = pip + #self.pip = pip self.requirements = requirements async def build(self, script): - for pip in ('pip3', 'pip', 'pip2'): - if script.which(pip): - self.pip = pip - break + self.pip = await script.which(('pip3', 'pip', 'pip2')) + if not self.pip: + raise Exception('Could not find pip command') if 'CACHE_DIR' in os.environ: cache = os.path.join(os.getenv('CACHE_DIR'), 'pip') @@ -20,16 +23,27 @@ class Pip: cache = os.path.join(os.getenv('HOME'), '.cache', 'pip') await script.mount(cache, '/root/.cache/pip') - await script.run(f'sudo {self.pip} install --upgrade pip') - source = [p for p in self.pip_packages if p.startswith('/')] + await script.crexec(f'{self.pip} install --upgrade pip') + + # https://github.com/pypa/pip/issues/5599 + self.pip = 'python3 -m pip' + + pip_packages = [] + for visitor in script.container.visitors: + pp = getattr(visitor, 'pip_packages', None) + if not pp: + continue + pip_packages += pip_packages + + source = [p for p in pip_packages if p.startswith('/')] if source: - await script.run( - f'sudo {self.pip} install --upgrade --editable {" ".join(source)}' + await script.crexec( + f'{self.pip} install --upgrade --editable {" ".join(source)}' ) - nonsource = [p for p in self.pip_packages if not p.startswith('/')] + nonsource = [p for p in pip_packages if not p.startswith('/')] if nonsource: - await script.run(f'sudo {self.pip} install --upgrade {" ".join(source)}') + await script.crexec(f'{self.pip} install --upgrade {" ".join(nonsource)}') if self.requirements: - await script.run(f'sudo {self.pip} install --upgrade -r {self.requirements}') + await script.crexec(f'{self.pip} install --upgrade -r {self.requirements}') diff --git a/podctl/visitors/template.py b/podctl/visitors/template.py index 971f2f2..cb30582 100644 --- a/podctl/visitors/template.py +++ b/podctl/visitors/template.py @@ -1,11 +1,13 @@ from textwrap import dedent -CMD = '''cat < {target} -{script} -EOF''' - class Template: + CMD = dedent( + '''cat < {target} + {script} + EOF''' + ) + def __init__(self, target, *lines, **variables): self.target = target self.lines = lines @@ -15,6 +17,14 @@ class Template: self.script = '\n'.join([ dedent(l).strip() for l in self.lines ]).format(**self.variables) - await script.run(CMD.strip().format(**self.__dict__)) + await script.cexec(self.CMD.strip().format(**self.__dict__)) if self.script.startswith('#!'): - await script.run('sudo chmod +x ' + self.target) + await script.cexec('chmod +x ' + self.target, user='root') + + +class Append(Template): + CMD = dedent( + '''cat <> {target} + {script} + EOF''' + ) diff --git a/podctl/visitors/user.py b/podctl/visitors/user.py index e0be081..11648fb 100644 --- a/podctl/visitors/user.py +++ b/podctl/visitors/user.py @@ -3,25 +3,25 @@ from .packages import Packages class User: """Secure the image with a user""" - packages = [ - 'shadow', - ] + packages = dict( + apk=['shadow'], + ) - def __init__(self, username, uid, home): + def __init__(self, username, uid, home, directories=None): self.username = username self.uid = uid self.home = home self.user_created = False + self.directories = directories async def build(self, script): - await script.run(f''' - if {script._run('id ' + str(self.uid))}; then - i=$({script._run('id -gn ' + str(self.uid))}) - {script._run('usermod -d ' + self.home + ' -l ' + self.username + ' $i')} - else - {script._run('useradd -d ' + self.home + ' -u ' + str(self.uid) + ' ' + self.username)} - fi - ''') # noqa + try: + await script.cexec('id', self.uid) + except: + await script.cexec('useradd', '-d', self.home, '-u', self.uid, ' ', + self.username) + else: + await script.cexec('id', '-gn', self.uid) self.user_created = True def post_build(self, script):