This commit is contained in:
jpic 2020-02-12 03:19:21 +01:00
parent f52cc8971a
commit 6abb061dc8
17 changed files with 489 additions and 244 deletions

View File

@ -11,46 +11,67 @@ from .script import Script
class Build(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__() super().__init__()
self.container = container
self.log = []
self.mounts = dict() self.mounts = dict()
async def config(self, line): async def config(self, line):
"""Run buildah config."""
return await self.append(f'buildah config {line} {self.ctr}') return await self.append(f'buildah config {line} {self.ctr}')
async def copy(self, src, dst): 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}') 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): async def cexec(self, *args, user=None, **kwargs):
_args = ['buildah', 'run', self.ctr] """Execute a command in the container."""
_args = ['buildah', 'run']
if user: if user:
_args += ['--user', user] _args += ['--user', user]
_args += ['--', 'sh', '-euc'] _args += [self.ctr, '--', 'sh', '-euc']
return await self.exec(*(_args + list(args))) 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): async def mount(self, src, dst):
"""Mount a host directory into the container."""
target = self.mnt / str(dst)[1:] target = self.mnt / str(dst)[1:]
await self.exec(f'mkdir -p {src} {target}') await self.exec(f'mkdir -p {src} {target}')
await self.exec(f'mount -o bind {src} {target}') await self.exec(f'mount -o bind {src} {target}')
self.mounts[src] = dst self.mounts[src] = dst
async def umounts(self): async def umounts(self):
"""Unmount all mounted directories from the container."""
for src, dst in self.mounts.items(): for src, dst in self.mounts.items():
await self.exec('umount', self.mnt / str(dst)[1:]) await self.exec('umount', self.mnt / str(dst)[1:])
async def umount(self): async def umount(self):
"""Unmount the buildah container with buildah unmount."""
await self.exec(f'buildah unmount {self.ctr}') await self.exec(f'buildah unmount {self.ctr}')
def which(self, cmd): async def paths(self):
for path in self.container.paths: """Return the list of $PATH directories"""
if os.path.exists(os.path.join(self.mnt, path[1:], cmd)): return (await self.cexec('echo $PATH')).out.split(':')
return True
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)):]

View File

@ -4,18 +4,34 @@ docker & docker-compose frustrated me, podctl unfrustrates me.
import asyncio import asyncio
import cli2 import cli2
import importlib import inspect
import os import os
import sys import sys
from .container import Container from .container import Container
from .pod import Pod from .pod import Pod
from .proc import WrongResult from .exceptions import Mistake, WrongResult
from .service import Service from .service import Service
@cli2.option('debug', help='Print debug output', color=cli2.GREEN, alias='d') class ConsoleScript(cli2.ConsoleScript):
async def build(*services_or_flags, **kwargs): 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)
'''
super().__call__(*args, **kwargs)
@staticmethod
def script(script_name):
async def script(*services_or_flags, **options):
flags = [] flags = []
services = [] services = []
for arg in services_or_flags: for arg in services_or_flags:
@ -38,57 +54,16 @@ async def build(*services_or_flags, **kwargs):
for name, service in services.items(): for name, service in services.items():
service.container.name = name service.container.name = name
service.container.flags = flags service.container.flags = flags
procs.append(service.container.script('build', flags)) procs.append(service.container.script(script_name, flags))
try: try:
result = await asyncio.gather(*procs) result = await asyncio.gather(*procs)
except Mistake as e:
print(e)
sys.exit(1)
except WrongResult: except WrongResult:
sys.exit(1) sys.exit(1)
return script
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, 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
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)
console_script = ConsoleScript(__doc__).add_module('podctl.console_script') console_script = ConsoleScript(__doc__).add_module('podctl.console_script')

View File

@ -3,28 +3,17 @@ import os
import shlex import shlex
from .build import Build from .build import Build
from .run import Run
from .visitable import Visitable from .visitable import Visitable
class Container(Visitable): class Container(Visitable):
default_scripts = dict( default_scripts = dict(
build=Build, build=Build(),
run=Run(),
) )
paths = [
'/bin',
'/sbin',
'/usr/bin',
'/usr/sbin',
]
def __init__(self, *args, **kwargs): def script(self, name):
super().__init__(*args, **kwargs) script = super().script(name)
self.log = [] script.container = self
return script
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

10
podctl/exceptions.py Normal file
View File

@ -0,0 +1,10 @@
class PodctlException(Exception):
pass
class Mistake(PodctlException):
pass
class WrongResult(PodctlException):
pass

View File

@ -1,6 +1,21 @@
import os
from .script import Script
from .visitable import Visitable
class Pod: class Pod(Visitable):
def __init__(self, *services, **scripts): def script_names(self):
self.scripts = scripts for name in self.scripts.keys():
self.services = {s.name: s for s in services} 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

33
podctl/podfile.py Normal file
View File

@ -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)

123
podctl/proc.py Normal file
View File

@ -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

5
podctl/run.py Normal file
View File

@ -0,0 +1,5 @@
from .script import Script
class Run(Script):
"""Run a container"""

View File

@ -1,5 +1,41 @@
import textwrap import textwrap
from .proc import Proc
class Script: 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

View File

@ -1,3 +1,4 @@
import asyncio
from copy import copy from copy import copy
@ -7,36 +8,12 @@ class Visitable:
def __init__(self, *visitors, **scripts): def __init__(self, *visitors, **scripts):
self.visitors = list(visitors) self.visitors = list(visitors)
self.scripts = scripts or { 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 = copy(self.scripts[name])
script.loop = loop return script
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
def visitor(self, name): def visitor(self, name):
for visitor in self.visitors: for visitor in self.visitors:

View File

@ -9,5 +9,6 @@ from .mount import Mount # noqa
from .packages import Packages # noqa from .packages import Packages # noqa
from .pip import Pip # noqa from .pip import Pip # noqa
from .run import Run # noqa from .run import Run # noqa
from .template import Template # noqa from .template import Append, Template # noqa
from .user import User # noqa from .user import User # noqa
from .uwsgi import uWSGI # noqa

View File

@ -9,5 +9,7 @@ class Base:
script.ctr = Path((await script.exec('buildah', 'from', self.base)).out) script.ctr = Path((await script.exec('buildah', 'from', self.base)).out)
script.mnt = Path((await script.exec('buildah', 'mount', script.ctr)).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.umounts()
await script.umount()
proc = await script.exec('buildah', 'rm', script.ctr, raises=False)

View File

@ -42,20 +42,24 @@ class Commit:
# filter out tags which resolved to None # filter out tags which resolved to None
self.tags = [t for t in self.tags if t is not 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): async def post_build(self, script):
await script.exec( self.sha = (await script.exec(
'buildah', 'buildah',
'commit', 'commit',
'--format=' + self.format, '--format=' + self.format,
script.ctr, script.ctr,
) )).out
if 'master' in self.tags: if 'master' in self.tags:
self.tags.append('latest') self.tags.append('latest')
if self.tags: if self.tags:
tags = ' '.join([f'{self.repo}:{tag}' for tag in 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: if self.push:
user = os.getenv('DOCKER_USER') user = os.getenv('DOCKER_USER')
@ -72,5 +76,12 @@ class Commit:
) )
for tag in self.tags: 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() await script.umount()
async def run(self, script):
await script.exec(
'podman', 'run', '-d',
'--name', script.container.name,
':'.join((self.repo, self.tags[0])),
)

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import copy
from datetime import datetime from datetime import datetime
from glob import glob from glob import glob
@ -8,6 +9,14 @@ from textwrap import dedent
class Packages: 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( mgrs = dict(
apk=dict( apk=dict(
update='apk update', update='apk update',
@ -31,99 +40,113 @@ class Packages:
), ),
) )
installed = []
def __init__(self, *packages, **kwargs): def __init__(self, *packages, **kwargs):
self.packages = list([ self.packages = []
dedent(l).strip().replace('\n', ' ') for l in 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 self.mgr = kwargs.pop('mgr') if 'mgr' in kwargs else None
@property @property
def cache(self): def cache_root(self):
if 'CACHE_DIR' in os.environ: 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: 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): async def init_build(self, script):
paths = ('bin', 'sbin', 'usr/bin', 'usr/sbin') cached = script.container.variable('mgr')
if cached:
self.mgr = cached
else:
for mgr, cmds in self.mgrs.items(): for mgr, cmds in self.mgrs.items():
for path in paths: if await script.which(mgr):
if (script.mnt / path / mgr).exists():
self.mgr = mgr self.mgr = mgr
self.cmds = cmds
break break
if not self.mgr: if not self.mgr:
raise Exception('Packages does not yet support this distro') raise Exception('Packages does not yet support this distro')
async def build(self, script): self.cmds = self.mgrs[self.mgr]
if not getattr(script.container, '_packages_upgraded', None):
async def update(self, script):
# run pkgmgr_setup functions ie. apk_setup # run pkgmgr_setup functions ie. apk_setup
await getattr(self, self.mgr + '_setup')(script) cachedir = await getattr(self, self.mgr + '_setup')(script)
# first run on container means inject visitor packages
self.packages += script.container.packages
await script.cexec(self.cmds['upgrade'])
script.container._packages_upgraded = True
await script.cexec(' '.join([self.cmds['install']] + self.packages)) lastupdate = None
if os.path.exists(cachedir + '/lastupdate'):
with open(cachedir + '/lastupdate', 'r') as f:
try:
lastupdate = int(f.read().strip())
except:
pass
async def apk_setup(self, script):
await script.mount(self.cache, f'/var/cache/{self.mgr}')
# 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')) now = int(datetime.now().strftime('%s'))
# expect hacker to have internet at least once a week # cache for a week
if now - mtime > 604800: if not lastupdate or now - lastupdate > 604800:
update = True # crude lockfile implementation, should work against *most*
break # race-conditions ...
else: lockfile = cachedir + '/update.lock'
update = True if not os.path.exists(lockfile):
with open(lockfile, 'w+') as f:
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())) f.write(str(os.getpid()))
try: try:
await script.cexec(self.cmds['update']) await script.cexec(self.cmds['update'])
except:
raise
finally: finally:
os.unlink(self.cache + '/update') 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):
await self.update(script)
await script.cexec(self.cmds['upgrade'])
# 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):
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')
return cachedir
async def dnf_setup(self, script): async def dnf_setup(self, script):
await script.mount(self.cache, f'/var/cache/{self.mgr}') await script.mount(self.cache, f'/var/cache/{self.mgr}')
await script.run('echo keepcache=True >> /etc/dnf/dnf.conf') await script.run('echo keepcache=True >> /etc/dnf/dnf.conf')
async def apt_setup(self, script): async def apt_setup(self, script):
cache = self.cache + '/$(source $mnt/etc/os-release; echo $VERSION_CODENAME)/' # noqa codename = (await script.exec(
await script.run('rm /etc/apt/apt.conf.d/docker-clean') f'source {script.mnt}/etc/os-release; echo $VERSION_CODENAME'
cache_archives = os.path.join(self.cache, 'archives') )).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') 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.mount(cache_lists, f'/var/lib/apt/lists')
return cachedir
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
''')
"""

View File

@ -3,16 +3,19 @@ import os
class Pip: class Pip:
packages = dict(
apt=['python3-pip'],
)
def __init__(self, *pip_packages, pip=None, requirements=None): def __init__(self, *pip_packages, pip=None, requirements=None):
self.pip_packages = pip_packages self.pip_packages = pip_packages
self.pip = pip #self.pip = pip
self.requirements = requirements self.requirements = requirements
async def build(self, script): async def build(self, script):
for pip in ('pip3', 'pip', 'pip2'): self.pip = await script.which(('pip3', 'pip', 'pip2'))
if script.which(pip): if not self.pip:
self.pip = pip raise Exception('Could not find pip command')
break
if 'CACHE_DIR' in os.environ: if 'CACHE_DIR' in os.environ:
cache = os.path.join(os.getenv('CACHE_DIR'), 'pip') cache = os.path.join(os.getenv('CACHE_DIR'), 'pip')
@ -20,16 +23,27 @@ class Pip:
cache = os.path.join(os.getenv('HOME'), '.cache', 'pip') cache = os.path.join(os.getenv('HOME'), '.cache', 'pip')
await script.mount(cache, '/root/.cache/pip') await script.mount(cache, '/root/.cache/pip')
await script.run(f'sudo {self.pip} install --upgrade pip') await script.crexec(f'{self.pip} install --upgrade pip')
source = [p for p in self.pip_packages if p.startswith('/')]
# 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: if source:
await script.run( await script.crexec(
f'sudo {self.pip} install --upgrade --editable {" ".join(source)}' 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: 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: 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}')

View File

@ -1,11 +1,13 @@
from textwrap import dedent from textwrap import dedent
CMD = '''cat <<EOF > {target}
{script}
EOF'''
class Template: class Template:
CMD = dedent(
'''cat <<EOF > {target}
{script}
EOF'''
)
def __init__(self, target, *lines, **variables): def __init__(self, target, *lines, **variables):
self.target = target self.target = target
self.lines = lines self.lines = lines
@ -15,6 +17,14 @@ class Template:
self.script = '\n'.join([ self.script = '\n'.join([
dedent(l).strip() for l in self.lines dedent(l).strip() for l in self.lines
]).format(**self.variables) ]).format(**self.variables)
await script.run(CMD.strip().format(**self.__dict__)) await script.cexec(self.CMD.strip().format(**self.__dict__))
if self.script.startswith('#!'): 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 <<EOF >> {target}
{script}
EOF'''
)

View File

@ -3,25 +3,25 @@ from .packages import Packages
class User: class User:
"""Secure the image with a user""" """Secure the image with a user"""
packages = [ packages = dict(
'shadow', apk=['shadow'],
] )
def __init__(self, username, uid, home): def __init__(self, username, uid, home, directories=None):
self.username = username self.username = username
self.uid = uid self.uid = uid
self.home = home self.home = home
self.user_created = False self.user_created = False
self.directories = directories
async def build(self, script): async def build(self, script):
await script.run(f''' try:
if {script._run('id ' + str(self.uid))}; then await script.cexec('id', self.uid)
i=$({script._run('id -gn ' + str(self.uid))}) except:
{script._run('usermod -d ' + self.home + ' -l ' + self.username + ' $i')} await script.cexec('useradd', '-d', self.home, '-u', self.uid, ' ',
else self.username)
{script._run('useradd -d ' + self.home + ' -u ' + str(self.uid) + ' ' + self.username)} else:
fi await script.cexec('id', '-gn', self.uid)
''') # noqa
self.user_created = True self.user_created = True
def post_build(self, script): def post_build(self, script):