Skip to content

Commit

Permalink
libs: Add envoy.distribution.verify (#35)
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Northey <ryan@synca.io>
  • Loading branch information
phlax authored Aug 25, 2021
1 parent 8c75c60 commit 83d382e
Show file tree
Hide file tree
Showing 11 changed files with 841 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- envoy.base.checker
- envoy.docker.utils
- envoy.distribution.distrotest
- envoy.distribution.verify
- envoy.github.abstract
- envoy.github.release
- envoy.gpg.identity
Expand Down Expand Up @@ -98,6 +99,7 @@ jobs:
- envoy.base.checker
- envoy.docker.utils
- envoy.distribution.distrotest
- envoy.distribution.verify
- envoy.github.abstract
- envoy.github.release
- envoy.gpg.identity
Expand Down
5 changes: 5 additions & 0 deletions envoy.distribution.verify/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

envoy.distribution.verify
=========================

Package distribution verification tool used in Envoy proxy's CI
1 change: 1 addition & 0 deletions envoy.distribution.verify/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.1
11 changes: 11 additions & 0 deletions envoy.distribution.verify/envoy/distribution/verify/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

from .exceptions import PackagesConfigurationError
from .checker import PackagesDistroChecker
from .cmd import cmd, main


__all__ = (
"cmd",
"main",
"PackagesConfigurationError",
"PackagesDistroChecker")
225 changes: 225 additions & 0 deletions envoy.distribution.verify/envoy/distribution/verify/checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@

import argparse
import pathlib
from functools import cached_property
from typing import Optional, Type

import aiodocker

from envoy.base import checker, utils
from envoy.distribution import distrotest

from .exceptions import PackagesConfigurationError


# TODO(phlax): make this configurable
ENVOY_MAINTAINER = "Envoy maintainers <envoy-maintainers@googlegroups.com>"
ENVOY_VERSION = "1.20.0"


class PackagesDistroChecker(checker.AsyncChecker):
_active_distrotest = None
checks = ("distros",)

@property
def active_distrotest(self) -> Optional[distrotest.DistroTest]:
"""Currently active test"""
return self._active_distrotest

@cached_property
def config(self) -> dict:
"""Config parsed from the provided path
Expects a yaml file with distributions in the following format:
```yaml
debian_buster:
# Docker image tag name
image: debian:buster-slim
# File extension of installable packages, for packages signed for
# particular distributions, this can be the distribution name and
# `.changes` extension.
ext: buster.changes
ubuntu_foo:
image: ubuntu:foo
ext: foo.changes
redhat_8.1:
image: registry.access.redhat.com/ubi8/ubi:8.1
```
"""
config = utils.from_yaml(self.args.config)
if not isinstance(config, dict):
raise PackagesConfigurationError(
f"Unable to parse configuration {self.args.config}")
return config

@cached_property
def docker(self) -> aiodocker.Docker:
"""An instance of `aiodocker.Docker`"""
return aiodocker.Docker()

@property
def filter_distributions(self) -> list:
"""List of distributions to filter the tests to be run with"""
return self.args.distribution

@property
def keyfile(self) -> pathlib.Path:
"""Path to a keyfile to to include in the Docker images for verifying
package signatures
"""
return pathlib.Path(self.args.keyfile)

@property
def packages_tarball(self) -> pathlib.Path:
"""Path to the packages tarball"""
return pathlib.Path(self.args.packages)

@property
def path(self) -> pathlib.Path:
"""Path to a temporary directory to run the tests from"""
return pathlib.Path(self.tempdir.name)

@property
def rebuild(self) -> bool:
"""Flag to rebuild the test images even if they exist"""
return self.args.rebuild

@property
def test_class(self) -> Type[distrotest.DistroTest]:
"""The test class to run the tests with"""
return distrotest.DistroTest

@cached_property
def test_config(self) -> distrotest.DistroTestConfig:
"""The test config
Parses global and provided configs to store and resolve configurations
for the test runner.
Also extracts the packages to the temporary directory and provides info
on available packages to test with.
"""
return self.test_config_class(
docker=self.docker,
path=self.path,
tarball=self.packages_tarball,
keyfile=self.keyfile,
testfile=self.testfile,
maintainer=ENVOY_MAINTAINER,
version=ENVOY_VERSION)

@property
def test_config_class(self) -> Type[distrotest.DistroTestConfig]:
"""The test config class"""
return distrotest.DistroTestConfig

@property
def testfile(self) -> pathlib.Path:
"""Path to a testfile to run inside the test containers"""
return pathlib.Path(self.args.testfile)

@cached_property
def tests(self) -> dict:
"""A dictionary of tests and test configuration, filtered according
to provided args
"""
_ret = {}
for name, config in self.config.items():
should_skip = (
self.filter_distributions
and name not in self.filter_distributions)
if should_skip:
continue
_ret[name] = self.get_test_config(config["image"])
_ret[name].update(config)
_ret[name]["packages"] = self.get_test_packages(
_ret[name]["type"],
_ret[name]["ext"])
return _ret

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
super().add_arguments(parser)
parser.add_argument(
"testfile",
help=(
"Path to the test file that will be run inside the "
"distribution containers"))
parser.add_argument(
"config",
help="Path to a YAML configuration with distributions for testing")
parser.add_argument(
"packages",
help="Path to a tarball containing packages to test")
parser.add_argument(
"--keyfile",
"-k",
help=(
"Specify the path to a file containing a gpg key for "
"verifying packages."))
parser.add_argument(
"--distribution",
"-d",
nargs="?",
help=(
"Specify distribution to test. "
"Can be specified multiple times."))
parser.add_argument(
"--rebuild",
action="store_true",
help="Rebuild test images before running the tests.")

async def check_distros(self) -> None:
"""Check runner"""
for name, config in self.tests.items():
self.log.info(
f"[{name}] Testing with: "
f"{','.join(p.name for p in config['packages'])}")
for i, package in enumerate(config["packages"]):
await self.run_test(
name,
config["image"],
package,
(i == 0 and self.rebuild))

def get_test_config(self, image: str) -> dict:
"""Get the type/ext config for a given image name"""
return self.test_config.get_config(image)

def get_test_packages(self, type: str, ext: str) -> list:
"""Get the packages to test for a given type/ext"""
return self.test_config.get_packages(type, ext)

async def on_checks_complete(self) -> int:
"""Cleanup and return the test result"""
await self._cleanup_test()
await self._cleanup_docker()
return await super().on_checks_complete()

async def run_test(
self,
name: str,
image: str,
package: pathlib.Path,
rebuild: bool) -> None:
"""Runs a test for each of the packages against a particular distro"""
if self.exiting:
return
self.log.info(f"[{name}] Testing package: {package}")
self._active_distrotest = self.test_class(
self, self.test_config, name, image, package, rebuild=rebuild)
await self._active_distrotest.run()

async def _cleanup_docker(self) -> None:
"""Close the docker connection"""
if "docker" in self.__dict__:
await self.docker.close()
del self.__dict__["docker"]

async def _cleanup_test(self) -> None:
"""Cleanup test containers"""
if self.active_distrotest:
await self.active_distrotest.cleanup()
16 changes: 16 additions & 0 deletions envoy.distribution.verify/envoy/distribution/verify/cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import sys

from .checker import PackagesDistroChecker


def main(*args: str) -> int:
return PackagesDistroChecker(*args).run()


def cmd():
sys.exit(main(*sys.argv[1:]))


if __name__ == "__main__":
cmd()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


class PackagesConfigurationError(Exception):
pass
Empty file.
64 changes: 64 additions & 0 deletions envoy.distribution.verify/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python

import os
import codecs
from setuptools import find_namespace_packages, setup # type:ignore


def read(fname):
file_path = os.path.join(os.path.dirname(__file__), fname)
return codecs.open(file_path, encoding='utf-8').read()


setup(
name='envoy.distribution.verify',
version=read("VERSION"),
author='Ryan Northey',
author_email='ryan@synca.io',
maintainer='Ryan Northey',
maintainer_email='ryan@synca.io',
license='Apache Software License 2.0',
url='https://github.com/envoyproxy/pytooling/envoy.distribution.verify',
description=(
"Package distribution verification tool used in Envoy proxy's CI"),
long_description=read('README.rst'),
py_modules=['envoy.distribution.verify'],
packages=find_namespace_packages(),
package_data={
'envoy.distribution.verify': [
'py.typed']},
python_requires='>=3.5',
extras_require={
"test": [
"pytest",
"pytest-asyncio",
"pytest-coverage",
"pytest-patches"],
"lint": ['flake8'],
"types": [
'mypy'],
"publish": ['wheel'],
},
install_requires=[
"envoy.base.checker",
"envoy.distribution.distrotest",
],
classifiers=[
'Development Status :: 4 - Beta',
'Framework :: Pytest',
'Intended Audience :: Developers',
'Topic :: Software Development :: Testing',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: OS Independent',
'License :: OSI Approved :: Apache Software License',
],
entry_points={
'console_scripts': [
'envoy.distro-verify=envoy.distribution.verify.cmd:cmd'],
}
)
Loading

0 comments on commit 83d382e

Please sign in to comment.