From 3f49d7d6a937597997a69b0ec73a746f9e21a6e4 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 10 Jul 2024 14:52:28 +0300 Subject: [PATCH] Add a conformance test workflow * The conformance test suite is likely to still change quite a bit so the workflow is not enabled on PRs yet * The actual conformance client is copied from the tuf-conformance project * This is mostly a test to see how things should work out, and a demonstration of how the tuf-conformance project should be used Signed-off-by: Jussi Kukkonen --- .github/scripts/conformance-client.py | 124 ++++++++++++++++++++++++++ .github/workflows/conformance.yml | 16 ++++ pyproject.toml | 3 + tox.ini | 2 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/conformance-client.py create mode 100644 .github/workflows/conformance.yml diff --git a/.github/scripts/conformance-client.py b/.github/scripts/conformance-client.py new file mode 100755 index 0000000000..2fab7bf0c5 --- /dev/null +++ b/.github/scripts/conformance-client.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +"""Conformance client for python-tuf, part of tuf-conformance""" + +# Copyright 2024 tuf-conformance contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +import argparse +import os +import shutil +import sys +from datetime import datetime, timedelta, timezone + +from tuf.ngclient import Updater, UpdaterConfig + +def init(metadata_dir: str, trusted_root: str) -> None: + """Initialize local trusted metadata""" + + # No need to actually run python-tuf code at this point + shutil.copyfile(trusted_root, os.path.join(metadata_dir, "root.json")) + print(f"python-tuf test client: Initialized repository in {metadata_dir}") + + +def refresh( + metadata_url: str, + metadata_dir: str, + days_in_future: str, + max_root_rotations: int, +) -> None: + """Refresh local metadata from remote""" + + updater = Updater( + metadata_dir, + metadata_url, + config=UpdaterConfig(max_root_rotations=int(max_root_rotations)), + ) + if days_in_future != "0": + day_int = int(days_in_future) + day_in_future = datetime.now(timezone.utc) + timedelta(days=day_int) + updater._trusted_set.reference_time = day_in_future # noqa: SLF001 + updater.refresh() + print(f"python-tuf test client: Refreshed metadata in {metadata_dir}") + + +def download_target( + metadata_url: str, + metadata_dir: str, + target_name: str, + download_dir: str, + target_base_url: str, +) -> None: + """Download target.""" + + updater = Updater( + metadata_dir, + metadata_url, + download_dir, + target_base_url, + config=UpdaterConfig(prefix_targets_with_hash=False), + ) + target_info = updater.get_targetinfo(target_name) + if not target_info: + raise RuntimeError(f"{target_name} not found in repository") + updater.download_target(target_info) + + +def main() -> int: + """Main TUF Client Example function""" + + parser = argparse.ArgumentParser(description="TUF Client Example") + parser.add_argument("--metadata-url", required=False) + parser.add_argument("--metadata-dir", required=True) + parser.add_argument("--target-name", required=False) + parser.add_argument("--target-dir", required=False) + parser.add_argument("--target-base-url", required=False) + parser.add_argument("--days-in-future", required=False, default="0") + parser.add_argument( + "--max-root-rotations", required=False, default=32, type=int + ) + + sub_command = parser.add_subparsers(dest="sub_command") + init_parser = sub_command.add_parser( + "init", + help="Initialize client with given trusted root", + ) + init_parser.add_argument("trusted_root") + + sub_command.add_parser( + "refresh", + help="Refresh the client metadata", + ) + + sub_command.add_parser( + "download", + help="Downloads a target", + ) + + command_args = parser.parse_args() + + # initialize the TUF Client Example infrastructure + if command_args.sub_command == "init": + init(command_args.metadata_dir, command_args.trusted_root) + elif command_args.sub_command == "refresh": + refresh( + command_args.metadata_url, + command_args.metadata_dir, + command_args.days_in_future, + command_args.max_root_rotations, + ) + elif command_args.sub_command == "download": + download_target( + command_args.metadata_url, + command_args.metadata_dir, + command_args.target_name, + command_args.target_dir, + command_args.target_base_url, + ) + else: + parser.print_help() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000000..0da19c2fde --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,16 @@ +on: + # manual dispatch only while the conformance test suite is under rapid development + workflow_dispatch: + +name: CI +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - name: Checkout the client wrapper + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Run test suite + uses: theupdateframework/tuf-conformance@main + with: + entrypoint: ".github/scripts/conformance-client.py" diff --git a/pyproject.toml b/pyproject.toml index f5e8a8429b..331a641264 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,9 @@ ignore = [ "S603", # bandit: this flags all uses of subprocess.run as vulnerable "T201", # print is ok in verify_release ] +".github/scripts/*" = [ + "T201", # print is ok in conformance client +] [tool.ruff.lint.flake8-annotations] mypy-init-return = true diff --git a/tox.ini b/tox.ini index f767e7af5c..9d4679749f 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,7 @@ changedir = {toxinidir} deps = -r{toxinidir}/requirements/lint.txt --editable {toxinidir} -lint_dirs = tuf examples tests verify_release +lint_dirs = tuf examples tests verify_release .github/scripts passenv = RUFF_OUTPUT_FORMAT commands = ruff check {[testenv:lint]lint_dirs}