Asyncio rewrite

This commit is contained in:
jpic 2020-02-03 05:25:04 +01:00
parent 7753175d6c
commit dc92b484e8
17 changed files with 188 additions and 137 deletions

View File

@ -1,3 +1,5 @@
from .build import Build # noqa
from .container import Container # noqa from .container import Container # noqa
from .pod import Pod # noqa from .pod import Pod # noqa
from .script import Script # noqa
from .visitors import * # noqa from .visitors import * # noqa

View File

@ -1,36 +1,71 @@
import asyncio import asyncio
import os import os
import asyncio
import signal
import subprocess import subprocess
import textwrap import textwrap
from .script import Script from .script import Script
class WrongResult(Exception):
pass
class Build(Script): class Build(Script):
def __init__(self, container): def __init__(self, container):
super().__init__() super().__init__()
self.container = container self.container = container
self.log = []
self.mounts = dict()
def append(self, value): async def cmd(self, line):
res = [] log = dict(cmd=line)
self.log.append(log)
line = self.unshare(line)
print(self.container.name + ' | ' + line)
def protocol_factory():
from .console_script import BuildStreamProtocol
return BuildStreamProtocol(
self.container,
limit=asyncio.streams._DEFAULT_LIMIT,
loop=self.loop,
)
transport, protocol = await self.loop.subprocess_shell(
protocol_factory,
line,
)
proc = asyncio.subprocess.Process(
transport,
protocol,
self.loop,
)
result = await proc.wait()
if result:
raise WrongResult(proc)
return proc
'''
proc = await asyncio.create_subprocess_shell(
line,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
'''
async def append(self, value):
for line in value.split('\n'): for line in value.split('\n'):
if line.startswith('#') or not line.strip(): if line.startswith('#') or not line.strip():
continue continue
res.append(self.unshare(line)) log = dict(proc=await self.cmd(line))
return '\n'.join(res) log['stdout'], log['stderr'] = await log['proc'].communicate()
async def unshare(self, line): return log['stdout']
print('+ buildah unshare ' + line)
proc = await asyncio.create_subprocess_shell(
'buildah unshare ' + line,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
).decode('utf8')
stdout, stderr = await proc.communicate()
return stdout
def config(self, line): def unshare(self, line):
self.append(f'buildah config {line} {self.ctr}') return 'buildah unshare ' + line
async def config(self, line):
return await self.append(f'buildah config {line} {self.ctr}')
def _run(self, cmd, inject=False): def _run(self, cmd, inject=False):
user = self.container.variable('username') user = self.container.variable('username')
@ -54,20 +89,25 @@ class Build(Script):
else: else:
return f'buildah run {self.ctr} -- {_cmd}' return f'buildah run {self.ctr} -- {_cmd}'
def run(self, cmd): async def run(self, cmd):
self.append(self._run(cmd)) return await self.append(self._run(cmd))
def copy(self, src, dst): async def copy(self, src, dst):
self.append(f'buildah copy {self.ctr} {src} {dst}') return await self.append(f'buildah copy {self.ctr} {src} {dst}')
def mount(self, src, dst): async def mount(self, src, dst):
self.run('sudo mkdir -p ' + dst) await self.run('sudo mkdir -p ' + dst)
self.append('mkdir -p ' + src) await self.append('mkdir -p ' + src)
self.append(f'mount -o bind {src} {self.mnt}{dst}') await self.append(f'mount -o bind {src} {self.mnt}{dst}')
#self.append('mounts=("$mnt' + dst + '" "${mounts[@]}")') #await self.append('mounts=("$mnt' + dst + '" "${mounts[@]}")')
self.mounts.append((src, dst)) self.mounts[dst] = src
def umounts(self): async def umounts(self):
for src, dst in self.mounts: for src, dst in self.mounts:
self.append('buildah unmount ' + dst) await self.append('buildah unmount ' + dst)
self.append('buildah unmount ' + self.ctr) await self.append('buildah unmount ' + self.ctr)
def which(self, cmd):
for path in self.container.paths:
if os.path.exists(os.path.join(path, cmd)):
return True

View File

@ -42,24 +42,14 @@ async def build(*services, **kwargs):
else: else:
services = console_script.pod.services services = console_script.pod.services
a = []
loop = asyncio.events.get_event_loop() loop = asyncio.events.get_event_loop()
debug = console_script.parser.options.get('debug', False)
def protocol_factory():
return BuildStreamProtocol(
service,
limit=asyncio.streams._DEFAULT_LIMIT,
loop=loop,
)
for name, service in services.items(): for name, service in services.items():
container = service.container service.container.name = name
if not container.variable('base'): a.append(service.container.script('build', loop))
continue
await container.build(loop, protocol_factory)
for proc in procs: result = await asyncio.gather(*a, return_exceptions=False)
if proc.returncode != 0: print(result)
sys.exit(proc.returncode)
class ConsoleScript(cli2.ConsoleScript): class ConsoleScript(cli2.ConsoleScript):

View File

@ -1,19 +1,53 @@
import asyncio
import os import os
from .build import Build from .build import Build
from .visitable import Visitable from .visitable import Visitable
class BuildStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
def __init__(self, service, *args, **kwargs):
self.service = service
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.service.name.encode('utf8') + b' | ' + line + b'\n'
)
sys.stdout.flush()
super().pipe_data_received(fd, data)
def protocol_factory():
loop = asyncio.events.get_event_loop()
return BuildStreamProtocol(
service,
limit=asyncio.streams._DEFAULT_LIMIT,
loop=loop,
)
class Container(Visitable): class Container(Visitable):
default_scripts = dict( default_scripts = dict(
build=Build, build=Build,
) )
paths = [
'/bin',
'/sbin',
'/usr/bin',
'/usr/sbin',
]
def script(self, name): async def script(self, name, loop):
self.packages = [] self.packages = []
for visitor in self.visitors: for visitor in self.visitors:
self.packages += getattr(visitor, 'packages', []) self.packages += getattr(visitor, 'packages', [])
return super().script(name) result = await super().script(name, loop)
return result
def script_run(self, name, debug): def script_run(self, name, debug):
script = f'.podctl_build_{name}.sh' script = f'.podctl_build_{name}.sh'
@ -27,14 +61,3 @@ class Container(Visitable):
x = 'x' if debug else '' x = 'x' if debug else ''
return prefix + f'bash -eu{x} {script}' return prefix + f'bash -eu{x} {script}'
async def build(self, loop, protocol_factory):
transport, protocol = await loop.subprocess_shell(
protocol_factory,
cmd,
)
await asyncio.subprocess.Process(
transport,
protocol,
loop,
).communicate()

View File

@ -2,10 +2,6 @@ import textwrap
class Script(list): class Script(list):
def __init__(self, shebang=None):
super().__init__()
self.append(shebang or '#/usr/bin/env bash')
def __str__(self): def __str__(self):
if not getattr(self, '_postconfig', False): if not getattr(self, '_postconfig', False):
if hasattr(self, 'post_config'): if hasattr(self, 'post_config'):

View File

@ -10,17 +10,23 @@ class Visitable:
k: v(self) for k, v in self.default_scripts.items() k: v(self) for k, v in self.default_scripts.items()
} }
def script(self, name): async def script(self, name, loop):
script = copy(self.scripts[name]) script = copy(self.scripts[name])
script.loop = loop
results = []
for prefix in ('init_', 'pre_', '', 'post_'): for prefix in ('init_', 'pre_', '', 'post_'):
method = prefix + name method = prefix + name
for visitor in self.visitors: for visitor in self.visitors:
if hasattr(visitor, method): if not hasattr(visitor, method):
script.append(f'echo "{type(visitor).__name__}.{method}"') continue
getattr(visitor, method)(script)
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:
await result
return script
def visitor(self, name): def visitor(self, name):
for visitor in self.visitors: for visitor in self.visitors:

View File

@ -3,3 +3,11 @@
class Base: class Base:
def __init__(self, base): def __init__(self, base):
self.base = base self.base = base
async def init_build(self, script):
ctr = await script.cmd('buildah from ' + self.base)
stdout, stderr = await ctr.communicate()
script.ctr = stdout.decode('utf8').strip()
mnt = await script.cmd('buildah mount ' + script.ctr)
stdout, stderr = await mnt.communicate()
script.mnt = stdout.decode('utf8').strip()

View File

@ -2,6 +2,6 @@ class Cmd:
def __init__(self, cmd): def __init__(self, cmd):
self.cmd = cmd self.cmd = cmd
def build(self, script): async def build(self, script):
# script._run() does not really support sudo code # script._run() does not really support sudo code
script.run(self.cmd) await script.run(self.cmd)

View File

@ -42,8 +42,8 @@ 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]
def post_build(self, script): async def post_build(self, script):
script.append(f''' await script.append(f'''
umounts umounts
buildah commit --format={self.format} $ctr {self.repo} buildah commit --format={self.format} $ctr {self.repo}
''') ''')
@ -53,7 +53,7 @@ class Commit:
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])
script.append(f'buildah tag {self.repo} {tags}') await script.append(f'buildah tag {self.repo} {tags}')
if self.push: if self.push:
user = os.getenv('DOCKER_USER') user = os.getenv('DOCKER_USER')
@ -66,4 +66,4 @@ class Commit:
]) ])
for tag in self.tags: for tag in self.tags:
script.append(f'podman push {self.repo}:{tag}') await script.append(f'podman push {self.repo}:{tag}')

View File

@ -13,13 +13,13 @@ class Copy:
self.dst, self.mode = self.dst.split(':') self.dst, self.mode = self.dst.split(':')
self.owner = script.variable('user') self.owner = script.variable('user')
def build(self, script): async def build(self, script):
script.run(f'sudo mkdir -p {self.dst}') await script.run(f'sudo mkdir -p {self.dst}')
for item in self.src: for item in self.src:
script.append(f'cp -a {item} $mnt{self.dst}') await script.append(f'cp -a {item} $mnt{self.dst}')
if self.mode: if self.mode:
script.run(f'sudo chmod {self.mode} $mnt{self.dst}') await script.run(f'sudo chmod {self.mode} $mnt{self.dst}')
if self.owner: if self.owner:
script.run(f'sudo chown -R {self.owner} $mnt{self.dst}') await script.run(f'sudo chown -R {self.owner} $mnt{self.dst}')

View File

@ -3,5 +3,5 @@ class Mount:
self.src = src self.src = src
self.dst = dst self.dst = dst
def build(self, script): async def build(self, script):
script.mount(self.src, self.dst) await script.mount(self.src, self.dst)

View File

@ -2,9 +2,9 @@ class Npm:
def __init__(self, install=None): def __init__(self, install=None):
self.npm_install = install self.npm_install = install
def build(self, script): async def build(self, script):
script.run('sudo npm update -g npm') await script.run('sudo npm update -g npm')
script.run(f''' await script.run(f'''
cd {self.npm_install} cd {self.npm_install}
npm install npm install
''') ''')

View File

@ -37,7 +37,7 @@ class Packages:
else: else:
self.cache = os.path.join(os.getenv('HOME'), '.cache', self.mgr) self.cache = os.path.join(os.getenv('HOME'), '.cache', self.mgr)
def pre_build(self, script): async def pre_build(self, script):
base = script.container.variable('base') base = script.container.variable('base')
if self.mgr: if self.mgr:
self.cmds = self.mgrs[self.mgr] self.cmds = self.mgrs[self.mgr]
@ -54,23 +54,23 @@ class Packages:
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')
def build(self, script): async def build(self, script):
if not getattr(script.container, '_packages_upgraded', None): if not getattr(script.container, '_packages_upgraded', None):
# run pkgmgr_setup functions ie. apk_setup # run pkgmgr_setup functions ie. apk_setup
getattr(self, self.mgr + '_setup')(script) await getattr(self, self.mgr + '_setup')(script)
# first run on container means inject visitor packages # first run on container means inject visitor packages
self.packages += script.container.packages self.packages += script.container.packages
script.run(self.cmds['upgrade']) await script.run(self.cmds['upgrade'])
script.container._packages_upgraded = True script.container._packages_upgraded = True
script.run(' '.join([self.cmds['install']] + self.packages)) await script.run(' '.join([self.cmds['install']] + self.packages))
def apk_setup(self, script): async def apk_setup(self, script):
script.mount(self.cache, f'/var/cache/{self.mgr}') await script.mount(self.cache, f'/var/cache/{self.mgr}')
# special step to enable apk cache # special step to enable apk cache
script.run('ln -s /var/cache/apk /etc/apk/cache') await script.run('ln -s /var/cache/apk /etc/apk/cache')
script.append(f''' await script.append(f'''
old="$(find .cache/apk/ -name APKINDEX.* -mtime +3)" old="$(find {self.cache} -name APKINDEX.* -mtime +3)"
if [ -n "$old" ] || ! ls .cache/apk/APKINDEX.*; then if [ -n "$old" ] || ! ls .cache/apk/APKINDEX.*; then
{script._run(self.cmds['update'])} {script._run(self.cmds['update'])}
else else
@ -78,18 +78,18 @@ class Packages:
fi fi
''') ''')
def dnf_setup(self, script): async def dnf_setup(self, script):
script.mount(self.cache, f'/var/cache/{self.mgr}') await script.mount(self.cache, f'/var/cache/{self.mgr}')
script.run('sh -c "echo keepcache=True >> /etc/dnf/dnf.conf"') await script.run('sh -c "echo keepcache=True >> /etc/dnf/dnf.conf"')
def apt_setup(self, script): async def apt_setup(self, script):
cache = self.cache + '/$(source $mnt/etc/os-release; echo $VERSION_CODENAME)/' # noqa cache = self.cache + '/$(source $mnt/etc/os-release; echo $VERSION_CODENAME)/' # noqa
script.run('sudo rm /etc/apt/apt.conf.d/docker-clean') await script.run('sudo rm /etc/apt/apt.conf.d/docker-clean')
cache_archives = os.path.join(self.cache, 'archives') cache_archives = os.path.join(self.cache, 'archives')
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(self.cache, 'lists')
script.mount(cache_lists, f'/var/lib/apt/lists') await script.mount(cache_lists, f'/var/lib/apt/lists')
script.append(f''' await script.append(f'''
old="$(find {cache_lists} -name lastup -mtime +3)" old="$(find {cache_lists} -name lastup -mtime +3)"
if [ -n "$old" ] || ! ls {cache_lists}/lastup; then if [ -n "$old" ] || ! ls {cache_lists}/lastup; then
until [ -z $(lsof /var/lib/dpkg/lock) ]; do sleep 1; done until [ -z $(lsof /var/lib/dpkg/lock) ]; do sleep 1; done

View File

@ -1,3 +1,4 @@
from glob import glob
import os import os
@ -7,34 +8,28 @@ class Pip:
self.pip = pip self.pip = pip
self.requirements = requirements self.requirements = requirements
def build(self, script): async def build(self, script):
if self.pip: for pip in ('pip3', 'pip', 'pip2'):
script.append('_pip=' + self.pip) if script.which(pip):
else: self.pip = pip
script.append(f''' break
if {script._run("bash -c 'type pip3'")}; then
_pip=pip3
elif {script._run("bash -c 'type pip'")}; then
_pip=pip
elif {script._run("bash -c 'type pip2'")}; then
_pip=pip2
fi
''')
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')
else: else:
cache = os.path.join(os.getenv('HOME'), '.cache', 'pip') cache = os.path.join(os.getenv('HOME'), '.cache', 'pip')
script.mount(cache, '/root/.cache/pip')
script.run('sudo $_pip install --upgrade 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('/')] source = [p for p in self.pip_packages if p.startswith('/')]
if source: if source:
script.run( await script.run(
f'sudo $_pip install --upgrade --editable {" ".join(source)}' f'sudo {self.pip} install --upgrade --editable {" ".join(source)}'
) )
nonsource = [p for p in self.pip_packages if not p.startswith('/')] nonsource = [p for p in self.pip_packages if not p.startswith('/')]
if nonsource: if nonsource:
script.run(f'sudo $_pip install --upgrade {" ".join(source)}') await script.run(f'sudo {self.pip} install --upgrade {" ".join(source)}')
if self.requirements: if self.requirements:
script.run(f'sudo $_pip install --upgrade -r {self.requirements}') await script.run(f'sudo {self.pip} install --upgrade -r {self.requirements}')

View File

@ -2,6 +2,6 @@ class Run:
def __init__(self, *commands): def __init__(self, *commands):
self.commands = commands self.commands = commands
def build(self, script): async def build(self, script):
for command in self.commands: for command in self.commands:
script.run(command) await script.run(command)

View File

@ -11,10 +11,10 @@ class Template:
self.lines = lines self.lines = lines
self.variables = variables self.variables = variables
def build(self, script): async def build(self, script):
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)
script.run(CMD.strip().format(**self.__dict__)) await script.run(CMD.strip().format(**self.__dict__))
if self.script.startswith('#!'): if self.script.startswith('#!'):
script.run('sudo chmod +x ' + self.target) await script.run('sudo chmod +x ' + self.target)

View File

@ -3,6 +3,9 @@ from .packages import Packages
class User: class User:
"""Secure the image with a user""" """Secure the image with a user"""
packages = [
'shadow',
]
def __init__(self, username, uid, home): def __init__(self, username, uid, home):
self.username = username self.username = username
@ -10,20 +13,8 @@ class User:
self.home = home self.home = home
self.user_created = False self.user_created = False
def init_build(self, script): async def build(self, script):
"""Inject the Packages visitor if necessary.""" await script.append(f'''
packages = script.container.visitor('packages')
if not packages:
index = script.container.visitors.index(self)
script.container.visitors.insert(index, Packages())
def pre_build(self, script):
"""Inject the shadow package for the usermod command"""
if script.container.variable('mgr') == 'apk':
script.container.variable('packages').append('shadow')
def build(self, script):
script.append(f'''
if {script._run('id ' + str(self.uid))}; then if {script._run('id ' + str(self.uid))}; then
i=$({script._run('id -gn ' + str(self.uid))}) i=$({script._run('id -gn ' + str(self.uid))})
{script._run('usermod -d ' + self.home + ' -l ' + self.username + ' $i')} {script._run('usermod -d ' + self.home + ' -l ' + self.username + ' $i')}