From 418a8c16c203cb2e50b3052eaff2c5f7d507d2fd Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Wed, 16 Feb 2022 20:44:42 +0100 Subject: [PATCH] wrapper: refactor docker into submodule --- Dockerfile | 2 +- config.py | 5 ++ constants.py | 5 ++ wrapper/__init__.py | 139 ++++++-------------------------------------- wrapper/docker.py | 101 ++++++++++++++++++++++++++++++++ wrapper/wrapper.py | 88 ++++++++++++++++++++++++++++ 6 files changed, 219 insertions(+), 121 deletions(-) create mode 100644 wrapper/docker.py create mode 100644 wrapper/wrapper.py diff --git a/Dockerfile b/Dockerfile index f20bd99..bee7ad0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/config.py b/config.py index c3a54c6..c3e6bbe 100644 --- a/config.py +++ b/config.py @@ -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, diff --git a/constants.py b/constants.py index a6648e1..5235663 100644 --- a/constants.py +++ b/constants.py @@ -151,3 +151,8 @@ CHROOT_PATHS = { 'pkgbuilds': '/pkgbuilds', 'images': '/images', } + +WRAPPER_TYPES = [ + 'none', + 'docker', +] diff --git a/wrapper/__init__.py b/wrapper/__init__.py index 37b1cb4..48a3f2e 100644 --- a/wrapper/__init__.py +++ b/wrapper/__init__.py @@ -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): diff --git a/wrapper/docker.py b/wrapper/docker.py new file mode 100644 index 0000000..5891f3b --- /dev/null +++ b/wrapper/docker.py @@ -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() diff --git a/wrapper/wrapper.py b/wrapper/wrapper.py new file mode 100644 index 0000000..1abb38d --- /dev/null +++ b/wrapper/wrapper.py @@ -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