wip
This commit is contained in:
parent
f52cc8971a
commit
6abb061dc8
@ -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)):]
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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
10
podctl/exceptions.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class PodctlException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Mistake(PodctlException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WrongResult(PodctlException):
|
||||||
|
pass
|
||||||
@ -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
33
podctl/podfile.py
Normal 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
123
podctl/proc.py
Normal 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
5
podctl/run.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .script import Script
|
||||||
|
|
||||||
|
|
||||||
|
class Run(Script):
|
||||||
|
"""Run a container"""
|
||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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])),
|
||||||
|
)
|
||||||
|
|||||||
@ -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
|
|
||||||
''')
|
|
||||||
"""
|
|
||||||
|
|||||||
@ -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}')
|
||||||
|
|||||||
@ -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'''
|
||||||
|
)
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user