diff options
Diffstat (limited to 'tests/packaging')
-rw-r--r-- | tests/packaging/README.rst | 87 | ||||
-rw-r--r-- | tests/packaging/conftest.py | 10 | ||||
-rw-r--r-- | tests/packaging/test_packaging.py | 494 |
3 files changed, 591 insertions, 0 deletions
diff --git a/tests/packaging/README.rst b/tests/packaging/README.rst new file mode 100644 index 0000000..997f666 --- /dev/null +++ b/tests/packaging/README.rst @@ -0,0 +1,87 @@ +.. SPDX-License-Identifier: GPL-3.0-or-later + +Packaging tests +=============== + +Packaging tests used pytest, docker and each directory with subdirectory *.packaging* +is called as *component*. + +Run tests for all components: + +.. code-block:: + + pytest -r fEsxX tests/packaging + +List all components: + +.. code-block:: + + pytest tests/packaging --collect-only + +Run test for specific component (*doc/.packaging*): + +.. code-block:: + + pytest -r fEsxX tests/packaging -k test_collect[debian_10-doc/.packaging] + +.. note:: + + For debug add argument :code:`-s`. + +daemon/.packaging component +--------------------------- + +This is special component that is used by all others components. +For each distribution and version are created two docker images with this component. +One with building dependencies and one for running dependencies. +*Build docker image* is tagged as :code:`kr-packaging-tests-<distro><version>-build` +and *Run docker image* is tagged as :code:`kr-packaging-tests-<distro><version>-run`. + +Others components +----------------- + +All others components are based on *daemon/.packaging* component (docker image). +When component needs new building dependencies, new running dependencies +or some scripts that change build or run phase (see `File structure of each component`_), +new docker image is created. +*Build docker image* is tagged as :code:`kr-packaging-tests-<distro><version>-<component>-build` +and *Run docker image* is tagged as :code:`kr-packaging-tests-<distro><version>-<component>-run`. + +File structure of each component +------------------------------------ + +* <distro> + * <version> + * builddeps - list of build dependencies + * rundeps - list of runtime dependencies + * pre-build.sh - script called before build phase + * post-build.sh - script called after build phase + * pre-run.sh - script called before run phase + * post-run.sh - script called after run phase + * install.sh and build.sh - scripts to rewrite standard commands for building and installing knot-resolvers + * pre-test.sh - script called immediately before testing +* test.config or test.sh - kresd config test or shell script (one of them must exists) + +Commands order to create docker image +------------------------------------- + +For *build docker image*: + +#. run pre-build.sh +#. install packages specified in the file *builddeps* +#. run build.sh +#. run install.sh +#. run post-build.sh + +For *run docker image*: + +#. run pre-run.sh +#. install packages specified in the file *rundeps* +#. run pre-test.sh +#. run test (:code:`kresd -c test.config` or :code:`test.sh`) +#. run post-build.sh + + +.. note:: + + knot-resolver builded in *build docker image* is automatically moved to *run docker image*. diff --git a/tests/packaging/conftest.py b/tests/packaging/conftest.py new file mode 100644 index 0000000..7279c15 --- /dev/null +++ b/tests/packaging/conftest.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +import pytest +import os + + +def pytest_configure(): + pytest.KR_PYTESTS_DIR = os.path.dirname(os.path.realpath(__file__)) + pytest.KR_ROOT_DIR = os.path.join(pytest.KR_PYTESTS_DIR, "..", "..") + pytest.KR_PREFIX = "kr-packaging-tests-" diff --git a/tests/packaging/test_packaging.py b/tests/packaging/test_packaging.py new file mode 100644 index 0000000..1a9bc41 --- /dev/null +++ b/tests/packaging/test_packaging.py @@ -0,0 +1,494 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +import pytest +import docker +import logging +from pathlib import Path +from abc import ABC, abstractmethod + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +client = docker.from_env() + + +class DockerCmdError(Exception): + """ Raised when shell command in Docker container failed """ + pass + + +class ContainerHandler(): + def __init__(self, image): + self.img_id = image + self.container = None + + def run(self): + self.container = client.containers.run(self.img_id, network_mode='host', + tty=True, detach=True) + logger.info('Run container ID={}'.format(self.container)) + + def stop(self): + self.container.kill() + + def exec_cmd(self, cmd, workdir): + # workaround: When exec_run is called in GitLab CI/CD workdir argument doesn't work. + inter_cmd = '' + if workdir is not None: + inter_cmd = 'cd {}; '.format(workdir) + + rcode, out = self.container.exec_run('/bin/sh -c \'' + inter_cmd + cmd + '\'') + if rcode != 0: + raise DockerCmdError(rcode, out) + + def getFiles(self, output, path): + strm, stat = self.container.get_archive(path) + with open(output, 'wb') as ofile: + for data in strm: + ofile.write(data) + + +class DockerImages(ABC): + def __init__(self, version): + self.version = version + self.module = None + self.distro = None + self.build_id = None + self.run_id = None + + @abstractmethod + def cmd_pkgs_install(self): + raise NotImplementedError + + @abstractmethod + def cmd_kresd_install(self): + raise NotImplementedError + + @abstractmethod + def cmd_kresd_build(self): + raise NotImplementedError + + def readDependencies(self, deps_file): + """Read dependencies from file""" + listf = None + try: + with open(deps_file, 'r') as f: + listf = f.read().splitlines() + except FileNotFoundError: + pass + + return listf + + def __genDockerFile(self, path, from_image=None): + """Generate Dockerfile for build image""" + if self.module is None: + raise AttributeError + + if from_image is None: + if os.path.isfile(os.path.join(self.module, self.distro, 'docker-image-name')): + with open(os.path.join(self.module, self.distro, 'docker-image-name')) as f: + from_image = f.read() + else: + from_image = '{0}:{1}'.format(self.distro, self.version) + + distro_dir = os.path.join(self.module, self.distro, self.version) + + dockerf = open(os.path.join(path, 'Dockerfile-build'), 'w') + + dockerf.write('FROM {}\n'.format(from_image)) + dockerf.write('WORKDIR /root/kresd\n') + if self.module == 'daemon/.packaging': + dockerf.write('COPY . /root/kresd\n') + # when this file doesn't exists, tzdata needs user interaction + dockerf.write('RUN if [ ! -f /etc/localtime ];' + + 'then ln -fs /usr/share/zoneinfo/Europe/Prague /etc/localtime; fi\n') + if os.path.isfile(os.path.join(distro_dir, 'pre-build.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'pre-build.sh'))) + if os.path.isfile(os.path.join(distro_dir, 'builddeps')): + dockerf.write('RUN {0} {1}\n'.format(self.cmd_pkgs_install(), + ' '.join(self.readDependencies(os.path.join(distro_dir, 'builddeps'))))) + if os.path.isfile(os.path.join(distro_dir, 'build.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'build.sh'))) + else: + dockerf.write('RUN {}\n'.format(self.cmd_kresd_build())) + if os.path.isfile(os.path.join(distro_dir, 'install.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'install.sh'))) + else: + dockerf.write('RUN {}\n'.format(self.cmd_kresd_install())) + if os.path.isfile(os.path.join(distro_dir, 'post-build.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'post-build.sh'))) + + dockerf.close() + + def __genDockerFile_run(self, path, build_id, from_image=None): + """Generate Dockerfile for run image""" + if self.module is None: + raise AttributeError + + if from_image is None: + if os.path.isfile(os.path.join(self.module, self.distro, 'docker-image-name')): + with open(os.path.join(self.module, self.distro, 'docker-image-name')) as f: + from_image = f.read() + else: + from_image = '{0}:{1}'.format(self.distro, self.version) + + distro_dir = os.path.join(self.module, self.distro, self.version) + + dockerf = open(os.path.join(path, 'Dockerfile-run'), 'w') + + dockerf.write('FROM {}\n'.format(from_image)) + dockerf.write('COPY --from={} /root/kresd /root/kresd\n'.format(build_id)) + dockerf.write('WORKDIR /root/kresd\n') + if os.path.isfile(os.path.join(distro_dir, 'pre-run.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'pre-run.sh'))) + if os.path.isfile(os.path.join(distro_dir, 'rundeps')): + dockerf.write('RUN {0} {1}\n'.format(self.cmd_pkgs_install(), + ' '.join(self.readDependencies(os.path.join(distro_dir, 'rundeps'))))) + if os.path.isfile(os.path.join(distro_dir, 'pre-test.sh')): + dockerf.write('RUN {}\n'.format(os.path.join(distro_dir, 'pre-test.sh'))) + + dockerf.close() + + def build_printing_errors(self, path, dockerfile, network_mode, tag, rm): + try: + return client.images.build(path=path, dockerfile=dockerfile, + network_mode=network_mode, tag=tag, rm=rm) + except docker.errors.BuildError as e: + iterable = iter(e.build_log) + while True: + try: + item = next(iterable) + if item['stream']: + for l in item['stream'].splitlines(): + stripped = l.strip() + if stripped: + logging.error(stripped) + except StopIteration: + break + raise e + + def build(self, tmpdir, tag="", from_image=None): + self.__genDockerFile(tmpdir, from_image=from_image) + + logger.debug('tmpdir={}'.format(tmpdir)) + logger.debug('datadir={}'.format(pytest.KR_ROOT_DIR)) + logger.debug('tag={}'.format(tag)) + image = self.build_printing_errors(path=str(pytest.KR_ROOT_DIR), + dockerfile=os.path.join(tmpdir, 'Dockerfile-build'), + network_mode='host', tag=tag, rm=True) + logger.info('"Build image" ID={} created'.format(image[0].short_id)) + self.build_id = image[0].short_id + return self.build_id + + def build_run(self, tmpdir, build_id, from_image=None, tag=""): + self.__genDockerFile_run(tmpdir, build_id, from_image=from_image) + + logger.debug('tmpdir={}'.format(tmpdir)) + logger.debug('datadir={}'.format(tmpdir)) + logger.debug('tag={}'.format(tag)) + image = self.build_printing_errors(path=str(tmpdir), + dockerfile=os.path.join(tmpdir, 'Dockerfile-run'), + network_mode='host', tag=tag, rm=True) + logger.info('"Run image" ID={} created'.format(image[0].short_id)) + self.run_id = image[0].short_id + return self.run_id + + +class DebianImage(DockerImages): + def __init__(self, version): + super().__init__(version) + self.distro = 'debian' + + def cmd_pkgs_install(self): + return 'apt-get install -y ' + + def cmd_kresd_install(self): + return 'ninja -C build_packaging install >/dev/null' + + def cmd_kresd_build(self): + return """\\ + [ -d /root/kresd/build_packaging ] && rm -rf /root/kresd/build_packaging/; \\ + CFLAGS=\"$CFLAGS -Wall -pedantic -fno-omit-frame-pointer\"; \\ + LDFLAGS=\"$LDFLAGS -Wl,--as-needed\"; \\ + meson build_packaging \\ + --buildtype=plain \\ + --prefix=/root/kresd/install_packaging \\ + --libdir=lib \\ + --default-library=static \\ + -Dsystemd_files=enabled \\ + -Dclient=enabled \\ + -Dkeyfile_default=/usr/share/dns/root.key \\ + -Droot_hints=/usr/share/dns/root.hints \\ + -Dinstall_kresd_conf=enabled \\ + -Dunit_tests=enabled \\ + -Dc_args=\"${CFLAGS}\" \\ + -Dc_link_args=\"${LDFLAGS}\"; \\ + ninja -C build_packaging + """ + + +class UbuntuImage(DebianImage): + def __init__(self, version): + super().__init__(version) + self.distro = 'ubuntu' + + +class CentosImage(DockerImages): + def __init__(self, version): + super().__init__(version) + self.distro = 'centos' + + def cmd_pkgs_install(self): + return "yum install -y " + + def cmd_kresd_install(self): + return 'ninja-build -C build_packaging install' + + def cmd_kresd_build(self): + return """\\ + [ -d /root/kresd/build_packaging ] && rm -rf /root/kresd/build_packaging/; \\ + CFLAGS=\"$CFLAGS -Wall -pedantic -fno-omit-frame-pointer\"; \\ + LDFLAGS=\"$LDFLAGS -Wl,--as-needed\"; \\ + meson build_packaging \\ + --buildtype=plain \\ + --prefix=/root/kresd/install_packaging \\ + --sbindir=sbin \\ + --libdir=lib \\ + --includedir=include \\ + --sysconfdir=etc \\ + --default-library=static \\ + -Dsystemd_files=enabled \\ + -Dclient=enabled \\ + -Dunit_tests=enabled \\ + -Dmanaged_ta=enabled \\ + -Dkeyfile_default=/root/kresd/install_packaging/var/lib/knot-resolver/root.keys \\ + -Dinstall_root_keys=enabled \\ + -Dinstall_kresd_conf=enabled; \\ + ninja-build -C build_packaging + """ + + +class FedoraImage(DockerImages): + def __init__(self, version): + super().__init__(version) + self.distro = 'fedora' + + def cmd_pkgs_install(self): + return "dnf install -y " + + def cmd_kresd_install(self): + return 'ninja -C build_packaging install >/dev/null' + + def cmd_kresd_build(self): + return """\\ + [ -d /root/kresd/build_packaging ] && rm -rf /root/kresd/build_packaging/; \\ + CFLAGS=\"$CFLAGS -Wall -pedantic -fno-omit-frame-pointer\"; \\ + LDFLAGS=\"$LDFLAGS -Wl,--as-needed\"; \\ + meson build_packaging \\ + --buildtype=plain \\ + --prefix=/root/kresd/install_packaging \\ + --sbindir=sbin \\ + --libdir=lib \\ + --includedir=include \\ + --sysconfdir=etc \\ + --default-library=static \\ + -Dsystemd_files=enabled \\ + -Dclient=enabled \\ + -Dunit_tests=enabled \\ + -Dmanaged_ta=enabled \\ + -Dkeyfile_default=/root/kresd/install_packaging/var/lib/knot-resolver/root.keys \\ + -Dinstall_root_keys=enabled \\ + -Dinstall_kresd_conf=enabled; \\ + ninja -C build_packaging + """ + + +class LeapImage(FedoraImage): + def __init__(self, version): + super().__init__(version) + self.distro = 'leap' + + def cmd_pkgs_install(self): + return "zypper install -y " + + +def create_distro_image(name, version): + img = None + + if (name == 'debian'): + img = DebianImage(version) + elif (name == 'ubuntu'): + img = UbuntuImage(version) + elif (name == 'centos'): + img = CentosImage(version) + elif (name == 'fedora'): + img = FedoraImage(version) + elif (name == 'leap'): + img = LeapImage(version) + else: + img = None + + return img + + +def list_dirs(path, exclude=None): + """return all 'packaging' directories with full path""" + filtered_dirs = [] + + for rootpath, dirs, _ in os.walk(path): + + if (os.path.basename(rootpath) == '.packaging'): + fdir = os.path.relpath(rootpath, path) + if exclude is not None: + if fdir not in exclude: + filtered_dirs.append(fdir) + else: + filtered_dirs.append(fdir) + + return filtered_dirs + + +def list_tests_dirs(): + """return all 'packaging' directories""" + return list_dirs(pytest.KR_ROOT_DIR) + + +def list_distro_vers(distro_root): + """ + return list of { 'name': distro_name, 'version': distro_version) + pairs found in distro_root + """ + # transform list of paths like TOP/debian/10 into (debian, 10) + dist_ver = [{'name': p.parts[-2], 'version': p.parts[-1]} for p + in Path(distro_root).glob('*/*') if p.is_dir()] + + return list(dist_ver) + + +MODULES = list_tests_dirs() +DISTROS = list_distro_vers(os.path.join(pytest.KR_ROOT_DIR, 'daemon/.packaging')) +DISTROS_NAMES = ['{0}_{1}'.format(distro['name'], distro['version']) for distro in DISTROS] + + +@pytest.fixture(scope='session', params=DISTROS, ids=DISTROS_NAMES) +def buildenv(request, tmpdir_factory): + distro = request.param + + logger.debug('Creating main images for "{0} {1}"'.format(distro['name'], distro['version'])) + img = create_distro_image(distro['name'], distro['version']) + if img is None: + logger.warning('Unknown distro {}'.format(distro['name'])) + else: + img.module = 'daemon/.packaging' + tmpdir = tmpdir_factory.mktemp(distro['name']+distro['version']) + img.build(tmpdir, tag=pytest.KR_PREFIX+distro['name']+distro['version']+'-build') + img.build_run(tmpdir, img.build_id, + tag=pytest.KR_PREFIX+distro['name']+distro['version']+'-run') + + yield img +# client.images.remove(img.run_id) +# client.images.remove(img.build_id) + + +@pytest.mark.parametrize('module', MODULES) +def test_collect(module, buildenv, tmp_path): + logger.info(' ### Run test {} ###'.format(module)) + + if buildenv is None: + logger.error('Distro "{0} {1}" isn\'t implemented'.format(buildenv.distro, + buildenv.version)) + assert False + + rcode = None + buildmod = None + module_dir = os.path.join(pytest.KR_ROOT_DIR, module) + distro_dir = os.path.join(module_dir, buildenv.distro, buildenv.version) + + if os.path.isfile(os.path.join(distro_dir, 'NOTSUPPORTED')): + pytest.skip('Unsupported linux distribution ({0} {1}:{2})'.format(buildenv.distro, buildenv.version, module)) + + try: + if module == 'daemon/.packaging': + # use main "run image" without changes + logging.info('Use main "run image"') + ch = ContainerHandler(buildenv.run_id) + ch.run() + elif buildenv is not None: + if os.path.isfile(os.path.join(distro_dir, 'pre-build.sh')) \ + or os.path.isfile(os.path.join(distro_dir, 'builddeps')): + # create module specific "build image" + logger.info('Create new "build image"') + buildmod = create_distro_image(buildenv.distro, buildenv.version) + buildmod.module = module + buildmod.build(tmp_path, from_image=buildenv.build_id, + tag=pytest.KR_PREFIX+buildmod.distro+buildmod.version+'-' + + module.replace('/.packaging', '')+'-build') + + if buildmod is not None: + # new build image was made, create new module specific "run image" + logger.info('Create module specific "run image" from Dockerfile') + buildmod.build_run(tmp_path, buildmod.build_id, + tag=pytest.KR_PREFIX+buildmod.distro+buildmod.version+'-' + + module.replace('/.packaging', '')+'-run', from_image=buildenv.run_id) + ch = ContainerHandler(buildmod.run_id) + ch.run() + elif os.path.isfile(os.path.join(distro_dir, 'pre-run.sh')) \ + or os.path.isfile(os.path.join(distro_dir, 'rundeps')): + # use main "run image" and apply module specific changes + logger.info('Apply module specific changes to "run image"') + buildmod = buildenv + ch = ContainerHandler(buildmod.run_id) + ch.run() + + if os.path.isfile(os.path.join(distro_dir, 'pre-run.sh')): + ch.exec_cmd(os.path.join(module, buildenv.distro, buildenv.version, + 'pre-run.sh'), '/root/kresd/') + + if os.path.isfile(os.path.join(distro_dir, 'rundeps')): + logger.debug(buildmod.cmd_pkgs_install() + ' '.join( + buildmod.readDependencies(os.path.join(distro_dir, 'rundeps')))) + ch.exec_cmd(buildmod.cmd_pkgs_install() + ' '.join( + buildmod.readDependencies(os.path.join(distro_dir, 'rundeps'))), + '/root/kresd/') + + if os.path.isfile(os.path.join(distro_dir, 'pre-test.sh')): + ch.exec_cmd(os.path.join(module, buildenv.distro, buildenv.version, + 'pre-test.sh'), '/root/kresd/') + else: + # use main "run image" without changes + logging.info('Use main "run image"') + ch = ContainerHandler(buildenv.run_id) + ch.run() + + # run test + if os.path.isfile(os.path.join(module_dir, 'test.config')): + ch.exec_cmd('/root/kresd/install_packaging/sbin/kresd -n -c ' + os.path.join('..', + module, 'test.config'), '/root/kresd/install_packaging/') + elif os.path.isfile(os.path.join(module_dir, 'test.sh')): + ch.exec_cmd(os.path.join('..', module, 'test.sh'), + '/root/kresd/install_packaging/') + else: + ch.stop() + ch.container.remove() + logger.error('Test file (test.config or test.sh) not found') + assert False + + rcode = 0 + + if os.path.isfile(os.path.join(distro_dir, 'post-run.sh')): + ch.exec_cmd(os.path.join(module, buildenv.distro, buildenv.version, 'post-run.sh'), + '/root/kresd/') + + except DockerCmdError as err: + rcode, out = err.args + logger.debug('rcode: {}'.format(rcode)) + logger.error(out.decode('utf-8')) + finally: + ch.stop() + ch.container.remove() + if buildmod is not None and buildmod is not buildenv: + client.images.remove(buildmod.run_id) + client.images.remove(buildmod.build_id) + + assert(rcode == 0) |