feature(keyringctl): adding ci command to verify newly added certs

Currently only newly added certificates will be checked against the
expectations as existing keys are not all fully compatible with those
assumptions.  New certificates are determined by using
$CI_MERGE_REQUEST_DIFF_BASE_SHA as the base,
This commit is contained in:
Levente Polyak 2021-10-24 22:08:50 +02:00
parent 9733fbafd8
commit a9e63edfa8
No known key found for this signature in database
GPG Key ID: FC1B547C8D8172C8
5 changed files with 125 additions and 1 deletions

View File

@ -21,6 +21,7 @@ from the provided data structure and to install it:
Optional:
* hopenpgp-tools (verify)
* sq-keyring-linter (verify)
* git (ci)
## Usage

34
libkeyringctl/ci.py Normal file
View File

@ -0,0 +1,34 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from os import environ
from pathlib import Path
from typing import List
from .git import git_changed_files
from .keyring import get_parent_cert_paths
from .keyring import verify
def ci(working_dir: Path, keyring_root: Path, project_root: Path) -> None:
"""Verify certificates against modern expectations using sq-keyring-linter and hokey
Currently only newly added certificates will be checked against the expectations as existing
keys are not all fully compatible with those assumptions.
New certificates are determined by using $CI_MERGE_REQUEST_DIFF_BASE_SHA as the base,
Parameters
----------
working_dir: A directory to use for temporary files
keyring_root: The keyring root directory to look up username shorthand sources
project_root: Path to the root of the git repository
"""
ci_merge_request_diff_base = environ.get("CI_MERGE_REQUEST_DIFF_BASE_SHA")
created, deleted, changed = git_changed_files(
git_path=project_root, base=f"{ci_merge_request_diff_base}", paths=[Path("keyring")]
)
added_certificates: List[Path] = list(get_parent_cert_paths(paths=created))
if added_certificates:
verify(working_dir=working_dir, keyring_root=keyring_root, sources=added_certificates)

View File

@ -8,6 +8,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from tempfile import mkdtemp
from .ci import ci
from .keyring import Username
from .keyring import build
from .keyring import convert
@ -114,6 +115,11 @@ verify_parser.add_argument(
)
verify_parser.set_defaults(lint_hokey=True, lint_sq_keyring=True)
ci_parser = subcommands.add_parser(
"ci",
help="ci command to verify certain aspects and expectations in pipelines",
)
def main() -> None: # noqa: ignore=C901
args = parser.parse_args()
@ -123,6 +129,7 @@ def main() -> None: # noqa: ignore=C901
# temporary working directory that gets auto cleaned
with TemporaryDirectory(prefix="arch-keyringctl-") as tempdir:
project_root = Path(".").absolute()
keyring_root = Path("keyring").absolute()
working_dir = Path(tempdir)
debug(f"Working directory: {working_dir}")
@ -190,6 +197,8 @@ def main() -> None: # noqa: ignore=C901
lint_hokey=args.lint_hokey,
lint_sq_keyring=args.lint_sq_keyring,
)
elif "ci" == args.subcommand:
ci(working_dir=working_dir, keyring_root=keyring_root, project_root=project_root)
else:
parser.print_help()

55
libkeyringctl/git.py Normal file
View File

@ -0,0 +1,55 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from typing import List
from typing import Optional
from typing import Tuple
from .util import system
def git_changed_files(
git_path: Optional[Path], base: Optional[str], paths: Optional[List[Path]] = None
) -> Tuple[List[Path], List[Path], List[Path]]:
"""Returns lists of created, deleted and changed files based on diff stats related to a base commit
and optional paths.
Parameters
----------
git_path: Path to the git repository, current directory by default
base: Optional base rev or current index by default
paths: Optional list of paths to take into account, unfiltered by default
Returns
-------
Lists of created, deleted and changed paths
"""
cmd = ["git"]
if git_path:
cmd += ["-C", str(git_path)]
cmd += ["--no-pager", "diff", "--color=never", "--summary", "--numstat"]
if base:
cmd += [base]
if paths:
cmd += ["--"]
cmd += [str(path) for path in paths]
result: str = system(cmd)
created: List[Path] = []
deleted: List[Path] = []
changed: List[Path] = []
for line in result.splitlines():
line = line.strip()
if line.startswith("create"):
created.append(Path(line.split(maxsplit=3)[3]))
continue
if line.startswith("delete"):
deleted.append(Path(line.split(maxsplit=3)[3]))
continue
changed.append(Path(line.split(maxsplit=2)[2]))
changed = [path for path in changed if path not in created and path not in deleted]
return created, deleted, changed

View File

@ -57,7 +57,7 @@ def get_cert_paths(paths: Iterable[Path]) -> Set[Path]:
Returns
-------
The list of paths to certificates
A set of paths to certificates
"""
# depth first search certificate paths
@ -73,6 +73,31 @@ def get_cert_paths(paths: Iterable[Path]) -> Set[Path]:
return cert_paths
def get_parent_cert_paths(paths: Iterable[Path]) -> Set[Path]:
"""Walks a list of paths upwards and resolves all discovered parent certificate paths
Parameters
----------
paths: A list of paths to walk and resolve to certificate paths.
Returns
-------
A set of paths to certificates
"""
# depth first search certificate paths
cert_paths: Set[Path] = set()
visit: List[Path] = list(paths)
while visit:
node = visit.pop().parent
# this level contains a certificate, abort depth search
if "keyring" == node.parent.parent.parent.name:
cert_paths.add(node)
continue
visit.append(node)
return cert_paths
def transform_username_to_keyring_path(keyring_dir: Path, paths: List[Path]) -> None:
"""Mutates the input sources by transforming passed usernames to keyring paths