shlax/podctl/build.py
2020-01-24 19:26:43 +01:00

246 lines
7.1 KiB
Python

from glob import glob
import inspect
import os
import subprocess
import time
from .script import Script
class BuildScript(Script):
export = ('base', 'repo', 'tag', 'image')
def __init__(self, container):
super().__init__()
self.container = container
for var in self.export:
if var in self.container:
self.append(f'{var}="{self.container[var]}"')
self.append('''
mounts=()
umounts() {
for i in "${mounts[@]}"; do
umount $i
done
}
trap umounts 0
ctr=$(buildah from $base)
mnt=$(buildah mount $ctr)
mounts=("$mnt" "${mounts[@]}")
''')
if self.container.get('packages'):
self.container.packages_install(self)
def config(self, line):
self.append(f'buildah config {line} $ctr')
def run(self, cmd):
self.append('buildah run $ctr -- ' + cmd)
def copy(self, src, dst):
self.append(f'buildah copy $ctr {src} {dst}')
def mount(self, src, dst):
self.run('mkdir -p ' + dst)
self.append('mkdir -p ' + src)
self.append(f'mount -o bind {src} $mnt{dst}')
# for unmount trap
self.append('mounts=("$mnt%s" "${mounts[@]}")' % dst)
class Plugins(dict):
def __init__(self, *plugins):
default_plugins = [
PkgPlugin(),
UserPlugin(),
FsPlugin(),
BuildPlugin(),
ConfigPlugin(),
]
super().__init__()
for plugin in plugins or default_plugins:
self.add(plugin)
def add(self, plugin):
plugin.plugins = self
self[plugin.name] = plugin
def __call__(self, method, *args, **kwargs):
hook = f'pre_{method}'
for plugin in self.values():
plugin(hook, *args, **kwargs)
for plugin in self.values():
if hasattr(plugin, method):
plugin(method, *args, **kwargs)
hook = f'post_{method}'
for plugin in self.values():
plugin(hook, *args, **kwargs)
class Plugin:
@property
def name(self):
return type(self).__name__.replace('Plugin', '').lower()
def bubble(self, hook, *args, **kwargs):
for plugin in self.plugins.values():
if plugin is self:
continue
plugin(hook, _bubble=False, *args, **kwargs)
def __call__(self, method, *args, **kwargs):
_bubble = kwargs.pop('_bubble', True)
if _bubble:
self.bubble(f'pre_{self.name}_{method}', *args, **kwargs)
if hasattr(self, method):
meth = getattr(self, method)
argspec = inspect.getargspec(meth)
if argspec.varargs and argspec.keywords:
meth(*args, **kwargs)
else:
numargs = len(argspec.args) - 1 - len(argspec.defaults or [])
args = args[:numargs]
kwargs = {
k: v
for k, v in kwargs.items()
if k in argspec.args
}
meth(*args, **kwargs)
if _bubble:
self.bubble(f'post_{self.name}_{method}', *args, **kwargs)
class BuildPlugin(Plugin):
def build(self, script):
user = script.service.get('user', None)
for cmd in script.service['build']:
if cmd.startswith('sudo'):
if user:
script.config(f'--user root')
script.run(cmd[5:])
else:
script.config(f'--user {script.service["user"]}')
script.run(cmd)
class FsPlugin(Plugin):
def build(self, script):
for key, value in script.service.items():
if not key.startswith('/'):
continue
parts = key.split(':')
dst = parts.pop(0)
mode = parts.pop(0) if parts else '0500'
script.run(f'mkdir -p {dst}')
script.run(f'chmod {mode} $mnt{dst}')
if value and isinstance(value, list):
for item in value:
if isinstance(item, str):
script.run(f'cp -a {item} $mnt{dst}')
if not isinstance(item, dict):
pass
class PkgPlugin(Plugin):
mgrs = dict(
apk=dict(
update='apk update',
upgrade='apk upgrade',
install='apk add',
),
)
def pre_build(self, script):
for mgr, cmds in self.mgrs.items():
cmd = [
'podman',
'run',
script.service['base'],
'which',
mgr
]
print('+ ' + ' '.join(cmd))
try:
subprocess.check_call(cmd)
script.service.mgr = mgr
script.service.cmds = cmds
break
except subprocess.CalledProcessError:
continue
def build(self, script):
cache = f'.cache/{script.service.mgr}'
script.mount(
'$(pwd)/' + cache,
f'/var/cache/{script.service.mgr}'
)
cached = False
if script.service.mgr == 'apk':
# special step to enable apk cache
script.run('ln -s /var/cache/apk /etc/apk/cache')
for index in glob(cache + '/APKINDEX*'):
if time.time() - os.stat(index).st_mtime < 3600:
cached = True
break
if not cached:
script.run(script.service.cmds['update'])
script.run(script.service.cmds['upgrade'])
script.run(' '.join([
script.service.cmds['install'],
' '.join(script.service.get('packages', []))
]))
class ConfigPlugin(Plugin):
def post_build(self, script):
for value in script.service['ports']:
script.config(f'--port {value}')
for key, value in script.service['env'].items():
script.config(f'--env {key}={value}')
for key, value in script.service['labels'].items():
script.config(f'--label {key}={value}')
for key, value in script.service['annotations'].items():
script.config(f'--annotation {key}={value}')
for volume in script.service['volumes']:
if ':' in volume:
continue # it's a runtime volume
script.config(f'--volume {volume}')
if 'workdir' in script.service:
script.config(f'--workingdir {script.service["workdir"]}')
class UserPlugin(Plugin):
def pre_pkg_build(self, script):
if script.service.mgr == 'apk':
script.service['packages'].append('shadow')
def build(self, script):
script.append(f'''
if buildah run $ctr -- id {script.service['user']['id']}; then
i=$(buildah run $ctr -- id -n {script.service['user']['id']})
buildah run $ctr -- usermod -d {script.service['user']['home']} -l {script.service['user']['id']} $i
else
buildah run $ctr -- useradd -d {script.service['user']['home']} {script.service['user']['id']}
fi
''')