wrapper: refactor docker into submodule

This commit is contained in:
InsanePrawn 2022-02-16 20:44:42 +01:00
parent b6dfc9e065
commit 418a8c16c2
6 changed files with 219 additions and 121 deletions

View file

@ -22,7 +22,7 @@ RUN yes | pacman -Scc
RUN sed -i "s/SigLevel.*/SigLevel = Never/g" /etc/pacman.conf
ENV KUPFERBOOTSTRAP_DOCKER=1
ENV KUPFERBOOTSTRAP_WRAPPED=DOCKER
ENV PATH=/app/bin:/app/local/bin:$PATH
WORKDIR /app

View file

@ -5,6 +5,8 @@ import logging
from copy import deepcopy
import click
from constants import WRAPPER_TYPES
CONFIG_DIR = appdirs.user_config_dir('kupfer')
CACHE_DIR = appdirs.user_cache_dir('kupfer')
@ -27,6 +29,9 @@ PROFILE_DEFAULTS: Profile = {
PROFILE_EMPTY: Profile = {key: None for key in PROFILE_DEFAULTS.keys()}
CONFIG_DEFAULTS = {
'wrapper': {
'type': 'docker',
},
'build': {
'ccache': True,
'clean_mode': True,

View file

@ -151,3 +151,8 @@ CHROOT_PATHS = {
'pkgbuilds': '/pkgbuilds',
'images': '/images',
}
WRAPPER_TYPES = [
'none',
'docker',
]

View file

@ -1,137 +1,36 @@
import atexit
import os
import pathlib
import subprocess
import sys
import uuid
import click
import logging
from config import config, dump_file as dump_config_file
from constants import CHROOT_PATHS
from config import config
from utils import programs_available
from .docker import DockerWrapper
DOCKER_PATHS = CHROOT_PATHS.copy()
wrapper_impls = {
'docker': DockerWrapper,
}
def wrap_docker():
def get_wrapper_type(wrapper_type: str = None):
return wrapper_type or config.file['wrapper']['type']
def _docker_volumes(volume_mappings: dict[str, str]) -> list[str]:
result = []
for source, destination in volume_mappings.items():
result += ['-v', f'{source}:{destination}:z']
return result
def _filter_args(args):
"""hack. filter out --config since it doesn't apply in docker"""
results = []
done = False
for i, arg in enumerate(args):
if done:
break
if arg[0] != '-':
results += args[i:]
done = True
break
for argname in ['--config', '-C']:
if arg.startswith(argname):
done = True
if arg.strip() != argname: # arg is longer, assume --arg=value
offset = 1
else:
offset = 2
results += args[i + offset:]
break
if not done:
results.append(arg)
return results
def wrap(wrapper_type: str = None):
wrapper_type = get_wrapper_type(wrapper_type)
if wrapper_type != 'none':
wrapper_impls[wrapper_type]().wrap()
script_path = config.runtime['script_source_dir']
with open(os.path.join(script_path, 'version.txt')) as version_file:
version = version_file.read().replace('\n', '')
tag = f'registry.gitlab.com/kupfer/kupferbootstrap:{version}'
if version == 'dev':
logging.info(f'Building docker image "{tag}"')
cmd = [
'docker',
'build',
'.',
'-t',
tag,
] + (['-q'] if not config.runtime['verbose'] else [])
logging.debug('Running docker cmd: ' + ' '.join(cmd))
result = subprocess.run(cmd, cwd=script_path, capture_output=True)
if result.returncode != 0:
logging.fatal('Failed to build docker image:\n' + result.stderr.decode())
exit(1)
else:
# Check if the image for the version already exists
result = subprocess.run(
[
'docker',
'images',
'-q',
tag,
],
capture_output=True,
)
if result.stdout == b'':
logging.info(f'Pulling kupferbootstrap docker image version \'{version}\'')
subprocess.run([
'docker',
'pull',
tag,
])
container_name = f'kupferbootstrap-{str(uuid.uuid4())}'
wrapped_config = f'/tmp/kupfer/{container_name}_wrapped.toml'
def at_exit():
subprocess.run(
[
'docker',
'kill',
container_name,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
os.remove(wrapped_config)
atexit.register(at_exit)
dump_config_file(file_path=wrapped_config, config=(config.file | {'paths': DOCKER_PATHS}))
ssh_dir = os.path.join(pathlib.Path.home(), '.ssh')
if not os.path.exists(ssh_dir):
os.makedirs(ssh_dir)
volumes = {
'/dev': '/dev',
os.getcwd(): '/src',
wrapped_config: '/root/.config/kupfer/kupferbootstrap.toml',
ssh_dir: '/root/.ssh',
}
volumes |= dict({config.get_path(vol_name): vol_dest for vol_name, vol_dest in DOCKER_PATHS.items()})
docker_cmd = [
'docker',
'run',
'--name',
container_name,
'--rm',
'--interactive',
'--tty',
'--privileged',
] + _docker_volumes(volumes) + [tag]
kupfer_cmd = ['kupferbootstrap', '--config', '/root/.config/kupfer/kupferbootstrap.toml'] + _filter_args(sys.argv[1:])
cmd = docker_cmd + kupfer_cmd
logging.debug('Wrapping in docker:' + repr(cmd))
result = subprocess.run(cmd)
exit(result.returncode)
def is_wrapped(wrapper_type: str = None):
wrapper_type = get_wrapper_type(wrapper_type)
return os.getenv('KUPFERBOOTSTRAP_WRAPPED') == wrapper_type.capitalize()
def enforce_wrap(no_wrapper=False):
if os.getenv('KUPFERBOOTSTRAP_DOCKER') != '1' and not config.runtime['no_wrap'] and not no_wrapper:
wrap_docker()
wrapper_type = get_wrapper_type()
if wrapper_type != 'none' and not is_wrapped(wrapper_type) and not config.runtime['no_wrap'] and not no_wrapper:
logging.info(f'Wrapping in {wrapper_type}')
wrap()
def check_programs_wrap(programs):

101
wrapper/docker.py Normal file
View file

@ -0,0 +1,101 @@
import logging
import os
import pathlib
import subprocess
import sys
from config import config
from constants import CHROOT_PATHS
from .wrapper import BaseWrapper
DOCKER_PATHS = CHROOT_PATHS.copy()
def docker_volumes_args(volume_mappings: dict[str, str]) -> list[str]:
result = []
for source, destination in volume_mappings.items():
result += ['-v', f'{source}:{destination}:z']
return result
class DockerWrapper(BaseWrapper):
type = 'docker'
def wrap(self):
script_path = config.runtime['script_source_dir']
with open(os.path.join(script_path, 'version.txt')) as version_file:
version = version_file.read().replace('\n', '')
tag = f'registry.gitlab.com/kupfer/kupferbootstrap:{version}'
if version == 'dev':
logging.info(f'Building docker image "{tag}"')
cmd = [
'docker',
'build',
'.',
'-t',
tag,
] + (['-q'] if not config.runtime['verbose'] else [])
logging.debug('Running docker cmd: ' + ' '.join(cmd))
result = subprocess.run(cmd, cwd=script_path, capture_output=True)
if result.returncode != 0:
logging.fatal('Failed to build docker image:\n' + result.stderr.decode())
exit(1)
else:
# Check if the image for the version already exists
result = subprocess.run(
[
'docker',
'images',
'-q',
tag,
],
capture_output=True,
)
if result.stdout == b'':
logging.info(f'Pulling kupferbootstrap docker image version \'{version}\'')
subprocess.run([
'docker',
'pull',
tag,
])
container_name = f'kupferbootstrap-{self.uuid}'
wrapped_config = self.generate_wrapper_config()
ssh_dir = os.path.join(pathlib.Path.home(), '.ssh')
if not os.path.exists(ssh_dir):
os.makedirs(ssh_dir)
volumes = self.get_bind_mounts_default()
volumes |= dict({config.get_path(vol_name): vol_dest for vol_name, vol_dest in DOCKER_PATHS.items()})
docker_cmd = [
'docker',
'run',
'--name',
container_name,
'--rm',
'--interactive',
'--tty',
'--privileged',
] + docker_volumes_args(volumes) + [tag]
kupfer_cmd = ['kupferbootstrap', '--config', '/root/.config/kupfer/kupferbootstrap.toml'] + self.filter_args_wrapper(sys.argv[1:])
cmd = docker_cmd + kupfer_cmd
logging.debug('Wrapping in docker:' + repr(cmd))
result = subprocess.run(cmd)
exit(result.returncode)
def stop(self):
subprocess.run(
[
'docker',
'kill',
self.identifier,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
wrapper = DockerWrapper()

88
wrapper/wrapper.py Normal file
View file

@ -0,0 +1,88 @@
import atexit
import os
import uuid
import pathlib
from config import config, dump_file as dump_config_file
from constants import CHROOT_PATHS
class BaseWrapper:
id = None
identifier = None
type = None
wrapped_config_path = None
def __init__(self, random_id: str = None, name: str = None):
self.uuid = str(random_id or uuid.uuid4())
self.identifier = name or f'kupferbootstrap-{self.uuid}'
def filter_args_wrapper(self, args):
"""filter out -c/--config since it doesn't apply in wrapper"""
results = []
done = False
for i, arg in enumerate(args):
if done:
break
if arg[0] != '-':
results += args[i:]
done = True
break
for argname in ['--config', '-C']:
if arg.startswith(argname):
done = True
if arg.strip() != argname: # arg is longer, assume --arg=value
offset = 1
else:
offset = 2
results += args[i + offset:]
break
if not done:
results.append(arg)
return results
def generate_wrapper_config(
self,
target_path: str = '/tmp/kupferbootstrap',
paths: dict[str, str] = CHROOT_PATHS,
config_overrides: dict[str, dict] = {},
) -> str:
wrapped_config = f'{target_path.rstrip("/")}/{self.identifier}_wrapped.toml'
def at_exit():
self.stop()
os.remove(wrapped_config)
atexit.register(at_exit)
dump_config_file(
file_path=wrapped_config,
config=(config.file | {
'paths': paths,
'wrapper': {
'type': 'none'
}
} | config_overrides),
)
self.wrapped_config_path = wrapped_config
return wrapped_config
def wrap(self):
raise NotImplementedError()
def stop(self):
raise NotImplementedError()
def get_bind_mounts_default(self, wrapped_config_path: str = None, ssh_dir: str = None, target_home: str = '/root'):
wrapped_config_path = wrapped_config_path or self.wrapped_config_path
ssh_dir = ssh_dir or os.path.join(pathlib.Path.home(), '.ssh')
assert (wrapped_config_path)
mounts = {
'/dev': '/dev',
wrapped_config_path: f'{target_home}/.config/kupfer/kupferbootstrap.toml',
}
if ssh_dir:
mounts |= {
ssh_dir: f'{target_home}/.ssh',
}
return mounts