Refactor into visitor pattern

This commit is contained in:
jpic 2020-01-25 16:44:29 +01:00
parent d5d924dd06
commit a866ba5a0d
29 changed files with 590 additions and 444 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.pyc
__pycache__
.cache/
.coverage
.eggs/
.podctl_build_django.sh
.podctl_build_podctl.sh
.setupmeta.version
.testmondata
*.egg-info

15
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,15 @@
qa:
stage: test
image: yourlabs/python
script: flake8 podctl
test:
stage: test
image: yourlabs/python
script: pip install -e . && py.test -v tests
pypi:
stage: deploy
image: yourlabs/python
script: pypi-release
only: [tags]

65
examples/django.py Normal file
View File

@ -0,0 +1,65 @@
import os
from podctl import *
class Django(Container):
tag = 'yourlabs/crudlfap'
base = 'alpine'
packages = [
'bash',
'python3',
switch(dev='vim'),
]
env = dict(FOO='bar')
annotations = dict(test='foo')
labels = dict(foo='test')
cmd = 'bash'
entrypoint = ['bash', '-v']
ports = [1234]
user = dict(
shell='/bin/bash',
name='app',
home='/app',
id=os.getenv('SUDO_ID', os.getenv('UID')),
)
volumes = [
'/bydir',
'byname:/byname',
switch(dev='.:/app'),
]
build = [
'sudo pip3 install --upgrade pip',
'pip3 install --user -e /app',
]
workdir = '/app'
django = Container(
Base('alpine'),
Packages('bash', switch(dev='vim')),
User(
uid=1000,
home='/app',
directories=('log', 'spooler', 'static')
),
switch(default=Mount('.', '/app'), production=Copy('.', '/app')),
Npm('build'),
Env('PATH', 'PATH=/app/node_modules/.bin:$PATH'),
Pip('requirements.txt'),
Env('PATH', 'PATH=$HOME/.local/bin:$PATH'),
switch(dev=Run('''
manage.py collectstatic --noinput --clear
find frontend/static/dist/css -type f | xargs gzip -f -k -9
find frontend/static/dist/js -type f | xargs gzip -f -k -9
''')),
Expose(8000),
Tag('equisafe'),
)
pod = Pod(
Service('django', django, restart='unless-stopped'),
Service('db',
Container(Tag('postgresql:latest')),
restart='unless-stopped'
),
)

17
pod.py Normal file
View File

@ -0,0 +1,17 @@
"""
Basic pod to contain the podctl command.
For advanced examples, check the examples sub-directory of the git repository.
"""
from podctl import *
podctl = Container(
Base('alpine'),
Packages('bash', 'python3'),
User('app', 1000, '/app'),
Copy(['setup.py', 'podctl'], '/app'),
Pip('/app'),
Config(cmd='podctl'),
Tag('yourlabs/podctl'),
)

View File

@ -1,2 +1,3 @@
from .container import Container, switch from .container import Container # noqa
from .pod import Pod from .pod import Pod # noqa
from .visitors import * # noqa

View File

@ -1,9 +1,3 @@
from glob import glob
import inspect
import os
import subprocess
import time
from .script import Script from .script import Script
@ -15,14 +9,16 @@ class BuildScript(Script):
self.container = container self.container = container
for var in self.export: for var in self.export:
if var in self.container: self.append(f'{var}="{container.variable(var)}"')
self.append(f'{var}="{self.container[var]}"')
self.append(''' self.append('''
mounts=() mounts=()
umounts() { umounts() {
for i in "${mounts[@]}"; do for i in "${mounts[@]}"; do
umount $i umount $i
echo $mounts
mounts=("${mounts[@]/$i}")
echo $mounts
done done
} }
trap umounts 0 trap umounts 0
@ -31,215 +27,27 @@ class BuildScript(Script):
mounts=("$mnt" "${mounts[@]}") mounts=("$mnt" "${mounts[@]}")
''') ''')
if self.container.get('packages'):
self.container.packages_install(self)
def config(self, line): def config(self, line):
self.append(f'buildah config {line} $ctr') self.append(f'buildah config {line} $ctr')
def _run(self, cmd):
user = self.container.variable('username')
if cmd.startswith('sudo '):
return f'buildah run --user root $ctr -- {cmd[5:]}'
elif user and self.container.variable('user_created'):
return f'buildah run --user {user} $ctr -- {cmd}'
else:
return f'buildah run $ctr -- {cmd}'
def run(self, cmd): def run(self, cmd):
self.append('buildah run $ctr -- ' + cmd) self.append(self._run(cmd))
def copy(self, src, dst): def copy(self, src, dst):
self.append(f'buildah copy $ctr {src} {dst}') self.append(f'buildah copy $ctr {src} {dst}')
def mount(self, src, dst): def mount(self, src, dst):
self.run('mkdir -p ' + dst) self.run('sudo mkdir -p ' + dst)
self.append('mkdir -p ' + src) self.append('mkdir -p ' + src)
self.append(f'mount -o bind {src} $mnt{dst}') self.append(f'mount -o bind {src} $mnt{dst}')
# for unmount trap # for unmount trap
self.append('mounts=("$mnt%s" "${mounts[@]}")' % dst) 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
''')

View File

@ -4,12 +4,13 @@ docker & docker-compose frustrated me, podctl unfrustrates me.
import asyncio import asyncio
import cli2 import cli2
import importlib
import os import os
import subprocess
import sys import sys
import textwrap
from .container import Container
from .pod import Pod from .pod import Pod
from .service import Service
class BuildStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol): class BuildStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
@ -32,20 +33,23 @@ class BuildStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
@cli2.option('debug', help='Print debug output', color=cli2.GREEN, alias='d') @cli2.option('debug', help='Print debug output', color=cli2.GREEN, alias='d')
async def build(service=None, **kwargs): async def build(service=None, **kwargs):
procs = [] procs = []
for name, container in console_script.pod.items(): for name, service in console_script.pod.services.items():
if not container.base: container = service.container
if not container.variable('base'):
continue continue
script = f'.podctl_build_{name}.sh' script = f'.podctl_build_{name}.sh'
with open(script, 'w+') as f: with open(script, 'w+') as f:
f.write(str(container.script_build())) f.write(str(container.script('build')))
loop = asyncio.events.get_event_loop() loop = asyncio.events.get_event_loop()
protocol_factory = lambda: BuildStreamProtocol(
container=container, def protocol_factory():
limit=asyncio.streams._DEFAULT_LIMIT, return BuildStreamProtocol(
loop=loop, service,
) limit=asyncio.streams._DEFAULT_LIMIT,
loop=loop,
)
transport, protocol = await loop.subprocess_shell( transport, protocol = await loop.subprocess_shell(
protocol_factory, protocol_factory,
f'buildah unshare bash -eux {script}', f'buildah unshare bash -eux {script}',
@ -60,37 +64,6 @@ async def build(service=None, **kwargs):
await proc.communicate() await proc.communicate()
@cli2.option('debug', help='Print debug output', color=cli2.GREEN, alias='d')
async def up(service=None, **kwargs):
procs = []
for name, service in console_script.pod.services.items():
if 'base' not in service:
continue
script = f'.podctl_up_{name}.sh'
with open(script, 'w+') as f:
f.write(str(service.build()))
loop = asyncio.events.get_event_loop()
protocol_factory = lambda: BuildStreamProtocol(
service=service,
limit=asyncio.streams._DEFAULT_LIMIT,
loop=loop,
)
transport, protocol = await loop.subprocess_shell(
protocol_factory,
f'bash -eux {script}',
)
procs.append(asyncio.subprocess.Process(
transport,
protocol,
loop,
))
for proc in procs:
await proc.communicate()
class ConsoleScript(cli2.ConsoleScript): class ConsoleScript(cli2.ConsoleScript):
def __setitem__(self, name, cb): def __setitem__(self, name, cb):
if name != 'help': if name != 'help':
@ -114,8 +87,25 @@ class ConsoleScript(cli2.ConsoleScript):
if command.name != 'help': if command.name != 'help':
self.path = self.parser.options['file'] self.path = self.parser.options['file']
self.home = self.parser.options['home'] self.home = self.parser.options['home']
with open(self.path) as f: self.containers = dict()
self.pod = Pod.factory(self.path) 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) return super().call(command)

View File

@ -1,109 +1,8 @@
import collections
import copy
from glob import glob
import subprocess
from .build import BuildScript from .build import BuildScript
from .visitable import Visitable
PACKAGE_MANAGERS = dict( class Container(Visitable):
apk=dict( default_scripts = dict(
update='apk update', build=BuildScript,
upgrade='apk upgrade', )
install='apk add',
),
)
class Container(collections.UserDict):
cfg = dict()
def __init__(self, profile=None, **cfg):
newcfg = copy.deepcopy(self.cfg)
newcfg.update(cfg)
super().__init__(**newcfg)
self.profile = profile or 'default'
def __getitem__(self, name, type=None):
try:
result = super().__getitem__(name)
except KeyError:
if hasattr(self, name + '_get'):
result = self[name] = getattr(self, name + '_get')()
else:
raise
else:
if isinstance(result, (dict, list, tuple, switch)):
return self.switch_value(result)
return result
def switch_value(self, value):
_switch = lambda v: v.value(self) if isinstance(v, switch) else v
if isinstance(value, dict):
return {
k: self.switch_value(v)
for k, v in value.items()
if self.switch_value(v) is not None
}
elif isinstance(value, (list, tuple)):
return [
self.switch_value(i)
for i in value
if self.switch_value(i) is not None
]
else:
return _switch(value)
def script_build(self):
return BuildScript(self)
def package_manager_get(self):
for mgr in PACKAGE_MANAGERS.keys():
cmd = ['podman', 'run', self['base'], 'which', mgr]
try:
subprocess.check_call(cmd)
return mgr
break
except subprocess.CalledProcessError:
continue
raise Exception('Package manager not supported yet')
def package_manager_cmd(self, cmd):
return PACKAGE_MANAGERS[self['package_manager']][cmd]
def packages_install(self, script):
cache = f'.cache/{self["package_manager"]}'
script.mount(
'$(pwd)/' + cache,
f'/var/cache/{self["package_manager"]}'
)
cached = False
if self['package_manager'] == 'apk':
# special step to enable apk cache
script.run('ln -s /var/cache/apk /etc/apk/cache')
script.append(f'''
if [ -n "$(find .cache/apk/ -name APKINDEX.* -mtime +3)" ]; then
buildah run $ctr -- {self.package_manager_cmd("update")}
fi
''')
script.run(self.package_manager_cmd('upgrade'))
script.run(' '.join([
self.package_manager_cmd('install'),
' '.join(self.get('packages', []))
]))
class switch:
def __init__(self, **values):
"""Instanciate a switch to vary values based on container profile."""
self.values = values
def value(self, container):
"""Return value from container profile or default."""
return self.values.get(
container.profile,
self.values.get('default', None)
)

View File

@ -1,11 +1,6 @@
import collections
import importlib.util
class Pod(collections.UserDict): class Pod:
@classmethod def __init__(self, *services, **scripts):
def factory(cls, path): self.scripts = scripts
spec = importlib.util.spec_from_file_location('pod', path) self.services = {s.name: s for s in services}
pod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pod)
return pod.pod

5
podctl/service.py Normal file
View File

@ -0,0 +1,5 @@
class Service:
def __init__(self, name, container, restart=None):
self.name = name
self.container = container
self.restart = restart

32
podctl/visitable.py Normal file
View File

@ -0,0 +1,32 @@
from copy import copy
class Visitable:
default_scripts = dict()
def __init__(self, *visitors, **scripts):
self.visitors = list(visitors)
self.scripts = scripts or {
k: v(self) for k, v in self.default_scripts.items()
}
def script(self, name):
script = copy(self.scripts[name])
for prefix in ('init_', 'pre_', '', 'post_'):
method = prefix + name
for visitor in self.visitors:
if hasattr(visitor, method):
getattr(visitor, method)(script)
return script
def visitor(self, name):
for visitor in self.visitors:
if name.lower() == type(visitor).__name__.lower():
return visitor
def variable(self, name):
for visitor in self.visitors:
if getattr(visitor, name, None) is not None:
return getattr(visitor, name)

View File

@ -0,0 +1,8 @@
from .base import Base # noqa
from .config import Config # noqa
from .copy import Copy # noqa
from .packages import Packages # noqa
from .pip import Pip # noqa
from .run import Run # noqa
from .tag import Tag # noqa
from .user import User # noqa

5
podctl/visitors/base.py Normal file
View File

@ -0,0 +1,5 @@
class Base:
def __init__(self, base):
self.base = base

View File

@ -0,0 +1,7 @@
class Config:
def __init__(self, **values):
self.values = values
def post_build(self, script):
for key, value in self.values.items():
script.config(f'--{key} {value}')

26
podctl/visitors/copy.py Normal file
View File

@ -0,0 +1,26 @@
class Copy:
def __init__(self, src, dst):
self.src = src
self.dst = dst
def init_build(self, script):
count = self.dst.count(':')
self.mode = None
self.owner = None
if count == 2:
self.dst, self.mode, self.owner = self.dst.split(':')
elif count == 1:
self.dst, self.mode = self.dst.split(':')
self.owner = script.variable('user')
def build(self, script):
if isinstance(self.src, list):
script.run(f'sudo mkdir -p {self.dst}')
for item in self.src:
script.append(f'cp -a {item} $mnt{self.dst}')
if self.mode:
script.run(f'sudo chmod {self.mode} $mnt{self.dst}')
if self.owner:
script.run(f'sudo chown -R {self.owner} $mnt{self.dst}')

View File

@ -0,0 +1,54 @@
import subprocess
class Packages:
mgrs = dict(
apk=dict(
update='sudo apk update',
upgrade='sudo apk upgrade',
install='sudo apk add',
),
)
def __init__(self, *packages):
self.packages = list(packages)
def pre_build(self, script):
for mgr, cmds in self.mgrs.items():
cmd = [
'podman',
'run',
script.container.variable('base'),
'which',
mgr
]
print('+ ' + ' '.join(cmd))
try:
subprocess.check_call(cmd)
self.mgr = mgr
self.cmds = cmds
break
except subprocess.CalledProcessError:
continue
def build(self, script):
cache = f'.cache/{self.mgr}'
script.mount(
'$(pwd)/' + cache,
f'/var/cache/{self.mgr}'
)
if self.mgr == 'apk':
# special step to enable apk cache
script.run('ln -s /var/cache/apk /etc/apk/cache')
script.append(f'''
old="$(find .cache/apk/ -name APKINDEX.* -mtime +3)"
if [ -n "$old" ] || ! ls .cache/apk/APKINDEX.*; then
{script._run(self.cmds['update'])}
else
echo Cache recent enough, skipping index update.
fi
''')
script.run(self.cmds['upgrade'])
script.run(' '.join([self.cmds['install']] + self.packages))

25
podctl/visitors/pip.py Normal file
View File

@ -0,0 +1,25 @@
class Pip:
def __init__(self, *pip_packages):
self.pip_packages = pip_packages
def build(self, script):
script.append(f'''
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
''')
script.mount('.cache/pip', '/root/.cache/pip')
script.run('sudo $_pip install --upgrade pip')
source = [p for p in self.pip_packages if p.startswith('/')]
if source:
script.run(
f'sudo $_pip install --upgrade --editable {" ".join(source)}'
)
nonsource = [p for p in self.pip_packages if not p.startswith('/')]
if nonsource:
script.run(f'sudo $_pip install --upgrade {" ".join(source)}')

7
podctl/visitors/run.py Normal file
View File

@ -0,0 +1,7 @@
class Run:
def __init__(self, *commands):
self.commands = commands
def build(self, script):
for command in self.commands:
script.run(command)

6
podctl/visitors/tag.py Normal file
View File

@ -0,0 +1,6 @@
class Tag:
def __init__(self, tag):
self.tag = tag
def post_build(self, script):
script.append(f'umounts && trap - 0 && buildah commit $ctr {self.tag}')

37
podctl/visitors/user.py Normal file
View File

@ -0,0 +1,37 @@
from .packages import Packages
class User:
"""Secure the image with a user"""
def __init__(self, username, uid, home):
self.username = username
self.uid = uid
self.home = home
self.user_created = False
def init_build(self, script):
"""Inject the Packages visitor if necessary."""
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 buildah run $ctr -- id {self.uid}; then
i=$(buildah run $ctr -- id -n {self.uid})
buildah run $ctr -- usermod --home-dir {self.home} --no-log-init {self.uid} $i
else
buildah run $ctr -- useradd --home-dir {self.home} --uid {self.uid} {self.username}
fi
''') # noqa
self.user_created = True
def post_build(self, script):
script.config(f'--user {self.username}')

View File

@ -4,9 +4,16 @@ import sys
from podctl.container import Container from podctl.container import Container
from podctl.build import BuildScript from podctl.build import BuildScript
from podctl.visitors import (
Base,
Copy,
Packages,
User,
)
def script_test(name, result): def script_test(name, *visitors):
result = str(Container(*visitors).script('build'))
path = os.path.join( path = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
f'test_{name}.sh', f'test_{name}.sh',
@ -15,7 +22,7 @@ def script_test(name, result):
if not os.path.exists(path): if not os.path.exists(path):
with open(path, 'w+') as f: with open(path, 'w+') as f:
f.write(result) f.write(result)
raise Exception('Fixture created test_build_packages.sh') raise Exception(f'Fixture created test_{name}.sh')
with open(path, 'r') as f: with open(path, 'r') as f:
expected = f.read() expected = f.read()
result = difflib.unified_diff( result = difflib.unified_diff(
@ -28,13 +35,43 @@ def script_test(name, result):
def test_build_empty(): def test_build_empty():
result = str(BuildScript(Container())) script_test(
script_test('build_empty', result) 'build_empty',
Base('alpine'),
)
def test_build_packages(): def test_build_packages():
script_test(
'build_packages',
Base('alpine'),
Packages('bash'),
)
def test_build_user():
script_test(
'build_user',
Base('alpine'),
User('app', 1000, '/app'),
)
def test_build_copy():
script_test(
'build_copy',
Base('alpine'),
Copy(os.path.dirname(__file__), '/app'),
)
'''
def test_build_files():
result = str(BuildScript(Container( result = str(BuildScript(Container(
base='alpine', base='alpine',
packages=['bash'], files=[
Directory('/app', '0500').add('setup.py', 'podctl'),
]
))) )))
script_test('build_packages', result) '''

18
tests/test_build_copy.sh Normal file
View File

@ -0,0 +1,18 @@
#/usr/bin/env bash
base="alpine"
repo="None"
tag="None"
image="None"
mounts=()
umounts() {
for i in "${mounts[@]}"; do
umount $i
echo $mounts
mounts=("${mounts[@]/$i}")
echo $mounts
done
}
trap umounts 0
ctr=$(buildah from $base)
mnt=$(buildah mount $ctr)
mounts=("$mnt" "${mounts[@]}")

View File

@ -1,8 +1,15 @@
#/usr/bin/env bash #/usr/bin/env bash
base="alpine"
repo="None"
tag="None"
image="None"
mounts=() mounts=()
umounts() { umounts() {
for i in "${mounts[@]}"; do for i in "${mounts[@]}"; do
umount $i umount $i
echo $mounts
mounts=("${mounts[@]/$i}")
echo $mounts
done done
} }
trap umounts 0 trap umounts 0

View File

@ -1,22 +1,31 @@
#/usr/bin/env bash #/usr/bin/env bash
base="alpine" base="alpine"
repo="None"
tag="None"
image="None"
mounts=() mounts=()
umounts() { umounts() {
for i in "${mounts[@]}"; do for i in "${mounts[@]}"; do
umount $i umount $i
echo $mounts
mounts=("${mounts[@]/$i}")
echo $mounts
done done
} }
trap umounts 0 trap umounts 0
ctr=$(buildah from $base) ctr=$(buildah from $base)
mnt=$(buildah mount $ctr) mnt=$(buildah mount $ctr)
mounts=("$mnt" "${mounts[@]}") mounts=("$mnt" "${mounts[@]}")
buildah run $ctr -- mkdir -p /var/cache/apk buildah run --user root $ctr -- mkdir -p /var/cache/apk
mkdir -p $(pwd)/.cache/apk mkdir -p $(pwd)/.cache/apk
mount -o bind $(pwd)/.cache/apk $mnt/var/cache/apk mount -o bind $(pwd)/.cache/apk $mnt/var/cache/apk
mounts=("$mnt/var/cache/apk" "${mounts[@]}") mounts=("$mnt/var/cache/apk" "${mounts[@]}")
buildah run $ctr -- ln -s /var/cache/apk /etc/apk/cache buildah run $ctr -- ln -s /var/cache/apk /etc/apk/cache
if [ -n "$(find .cache/apk/ -name APKINDEX.* -mtime +3)" ]; then old="$(find .cache/apk/ -name APKINDEX.* -mtime +3)"
buildah run $ctr -- apk update if [ -n "$old" ] || ! ls .cache/apk/APKINDEX.*; then
buildah run --user root $ctr -- apk update
else
echo Cache recent enough, skipping index update.
fi fi
buildah run $ctr -- apk upgrade buildah run --user root $ctr -- apk upgrade
buildah run $ctr -- apk add bash buildah run --user root $ctr -- apk add bash

38
tests/test_build_user.sh Normal file
View File

@ -0,0 +1,38 @@
#/usr/bin/env bash
base="alpine"
repo="None"
tag="None"
image="None"
mounts=()
umounts() {
for i in "${mounts[@]}"; do
umount $i
echo $mounts
mounts=("${mounts[@]/$i}")
echo $mounts
done
}
trap umounts 0
ctr=$(buildah from $base)
mnt=$(buildah mount $ctr)
mounts=("$mnt" "${mounts[@]}")
buildah run --user root $ctr -- mkdir -p /var/cache/apk
mkdir -p $(pwd)/.cache/apk
mount -o bind $(pwd)/.cache/apk $mnt/var/cache/apk
mounts=("$mnt/var/cache/apk" "${mounts[@]}")
buildah run $ctr -- ln -s /var/cache/apk /etc/apk/cache
old="$(find .cache/apk/ -name APKINDEX.* -mtime +3)"
if [ -n "$old" ] || ! ls .cache/apk/APKINDEX.*; then
buildah run --user root $ctr -- apk update
else
echo Cache recent enough, skipping index update.
fi
buildah run --user root $ctr -- apk upgrade
buildah run --user root $ctr -- apk add shadow
if buildah run $ctr -- id 1000; then
i=$(buildah run $ctr -- id -n 1000)
buildah run $ctr -- usermod --home-dir /app --no-log-init 1000 $i
else
buildah run $ctr -- useradd --home-dir /app --uid 1000 app
fi
buildah config --user app $ctr

View File

@ -1,56 +0,0 @@
from .container import Container, switch
def test_container_configuration():
'''Attributes should be passable to constructor or as class attributes'''
assert Container(a='b')['a'] == 'b'
class Test(Container):
cfg = dict(a='b')
assert Test()['a'] == 'b'
def test_switch_simple():
assert Container(a=switch(default='expected'))['a'] == 'expected'
assert Container(a=switch(noise='noise'))['a'] == None
fixture = Container(
'test',
a=switch(default='noise', test='expected')
)
assert fixture['a'] == 'expected'
assert [*fixture.values()][0] == 'expected'
assert [*fixture.items()][0][1] == 'expected'
def test_switch_iterable():
class TContainer(Container):
cfg = dict(
a=switch(dev='test')
)
assert TContainer()['a'] is None
assert TContainer('dev')['a'] == 'test'
assert TContainer('dev', a=[switch(dev='y')])['a'] == ['y']
assert TContainer('dev', a=[switch(default='y')])['a'] == ['y']
def test_switch_value_list():
assert Container('test').switch_value(
[switch(default='noise', test=False)]
) == [False]
assert Container('none').switch_value(
[switch(noise='noise')]
) == []
def test_switch_value_dict():
assert Container('foo').switch_value(
dict(i=switch(default='expected', noise='noise'))
) == dict(i='expected')
assert Container('test').switch_value(
dict(i=switch(default='noise', test='expected'))
) == dict(i='expected')
assert Container('none').switch_value(
dict(i=switch(noise='noise'), j=dict(e=switch(none=1)))
) == dict(j=dict(e=1))

View File

@ -1,10 +0,0 @@
import os
from pathlib import Path
from pod import Pod
def test_pod_file():
path = Path(os.path.dirname(__file__)) / '..' / 'pod.py'
pod = Pod.factory(path)
assert pod['podctl']

91
tests/test_visitable.py Normal file
View File

@ -0,0 +1,91 @@
from unittest.mock import MagicMock
from podctl.script import Script
from podctl.visitable import Visitable
class Visitor0:
def __init__(self, name=None):
self.name = name or 'visit0'
class Visitor1:
def pre_build(self, script):
script.append('pre_build')
def build(self, script):
script.append('build')
def post_build(self, script):
script.append('post_build')
def test_visitable_visitor():
visitable = Visitable(Visitor0(), Visitor1(), build=Script())
script = visitable.script('build')
assert 'pre_build' in script
assert 'build' in script
assert 'post_build' in script
def test_visitable_visitor():
x = Visitor0()
assert Visitable(x).visitor('visitor0') is x
def test_visitable_variable():
assert Visitable(Visitor0('foo')).variable('name') == 'foo'
#
#
#def test_visitable_configuration():
# '''Attributes should be passable to constructor or as class attributes'''
# assert Container(a='b')['a'] == 'b'
# class Test(Container):
# cfg = dict(a='b')
# assert Test()['a'] == 'b'
#
#
#def test_switch_simple():
# assert Container(a=switch(default='expected'))['a'] == 'expected'
# assert Container(a=switch(noise='noise'))['a'] == None
# fixture = Container(
# 'test',
# a=switch(default='noise', test='expected')
# )
# assert fixture['a'] == 'expected'
# assert [*fixture.values()][0] == 'expected'
# assert [*fixture.items()][0][1] == 'expected'
#
#
#def test_switch_iterable():
# class TContainer(Container):
# cfg = dict(
# a=switch(dev='test')
# )
# assert TContainer()['a'] is None
# assert TContainer('dev')['a'] == 'test'
# assert TContainer('dev', a=[switch(dev='y')])['a'] == ['y']
# assert TContainer('dev', a=[switch(default='y')])['a'] == ['y']
#
#
#def test_switch_value_list():
# assert Container('test').switch_value(
# [switch(default='noise', test=False)]
# ) == [False]
#
# assert Container('none').switch_value(
# [switch(noise='noise')]
# ) == []
#
#
#def test_switch_value_dict():
# assert Container('foo').switch_value(
# dict(i=switch(default='expected', noise='noise'))
# ) == dict(i='expected')
#
# assert Container('test').switch_value(
# dict(i=switch(default='noise', test='expected'))
# ) == dict(i='expected')
#
# assert Container('none').switch_value(
# dict(i=switch(noise='noise'), j=dict(e=switch(none=1)))
# ) == dict(j=dict(e=1))