diff --git a/taf/auth_repo.py b/taf/auth_repo.py index 337f0d5b..36d6ba84 100644 --- a/taf/auth_repo.py +++ b/taf/auth_repo.py @@ -16,7 +16,7 @@ get_role_metadata_path, get_target_path, ) -from taf.constants import INFO_JSON_PATH +from taf.constants import INFO_JSON_PATH, LOG_DIR_NAME, TARGETS_DIRECTORY_NAME from taf.yubikey.yubikey_manager import PinManager @@ -140,6 +140,16 @@ def certs_dir(self): certs_dir.mkdir(parents=True, exist_ok=True) return str(certs_dir) + @property + def log_dir(self) -> Path: + """ + Location inside the targets directory where dependencies update data is stored + """ + log_dir_path = self.path / TARGETS_DIRECTORY_NAME / LOG_DIR_NAME + if not log_dir_path.is_dir(): + log_dir_path.mkdir(parents=True) + return log_dir_path + @property def dependencies(self) -> Dict: return self._dependencies diff --git a/taf/constants.py b/taf/constants.py index 1b5dffa3..a29aa5a5 100644 --- a/taf/constants.py +++ b/taf/constants.py @@ -5,6 +5,7 @@ TARGETS_DIRECTORY_NAME = "targets" METADATA_DIRECTORY_NAME = "metadata" +LOG_DIR_NAME = "log" DEFAULT_RSA_SIGNATURE_SCHEME = "rsa-pkcs1v15-sha256" diff --git a/taf/tests/test_updater/update_utils.py b/taf/tests/test_updater/update_utils.py index c5b77711..d2fad7df 100644 --- a/taf/tests/test_updater/update_utils.py +++ b/taf/tests/test_updater/update_utils.py @@ -257,6 +257,7 @@ def update_and_check_commit_shas( no_upstream=False, skip_check_last_validated=False, num_of_commits_to_remove=None, + sign=False, ): client_repos = load_target_repositories(origin_auth_repo, clients_dir) client_repos = { @@ -282,6 +283,7 @@ def update_and_check_commit_shas( bare=bare, force=force, no_upstream=no_upstream, + sign=sign, ) if operation == OperationType.CLONE: @@ -510,6 +512,7 @@ def update_and_validate_repositories( client_dir, invalid_target_names=None, excluded_target_globs=None, + sign=False, ): if invalid_target_names is None: invalid_target_names = [] @@ -532,6 +535,7 @@ def update_and_validate_repositories( origin_root_repo, client_dir, excluded_target_globs=excluded_target_globs, + sign=sign, ) except UpdateFailedError as e: if any( diff --git a/taf/tools/repo/__init__.py b/taf/tools/repo/__init__.py index f60db8c7..1300d027 100644 --- a/taf/tools/repo/__init__.py +++ b/taf/tools/repo/__init__.py @@ -245,15 +245,16 @@ def update_repo_command(): @click.option("--no-deps", is_flag=True, default=False, help="Optionally disables updating of dependencies.") @click.option("--upstream/--no-upstream", default=False, help="Skips comparison with remote repositories upstream") @click.option("-v", "--verbosity", count=True, help="Displays varied levels of logging information based on verbosity level") - def update(path, library_dir, expected_repo_type, scripts_root_dir, profile, format_output, exclude_target, strict, no_deps, force, upstream, verbosity): + @click.option("--sign", is_flag=True, default=False, help="Write update date to target files and sign. Applicable when updating a repository that has dependencies") + def update(path, library_dir, expected_repo_type, scripts_root_dir, profile, format_output, exclude_target, strict, no_deps, force, upstream, verbosity, sign): settings.VERBOSITY = verbosity initialize_logger_handlers() if profile: start_profiling() - config = UpdateConfig( - operation=OperationType.UPDATE, + config = UpdConfig( + operation=OpeaterationType.UPDATE, path=path, library_dir=library_dir, expected_repo_type=UpdateType(expected_repo_type), @@ -263,6 +264,7 @@ def update(path, library_dir, expected_repo_type, scripts_root_dir, profile, for force=force, no_upstream=not upstream, no_deps=no_deps, + sign=sign, ) _call_updater(config, format_output) diff --git a/taf/updater/updater.py b/taf/updater/updater.py index 1bf27861..4fb9405d 100644 --- a/taf/updater/updater.py +++ b/taf/updater/updater.py @@ -26,6 +26,8 @@ loads data from a most recent commit. """ import copy +import datetime +import json from logging import ERROR from typing import Dict, Tuple, Any @@ -245,6 +247,12 @@ class UpdateConfig: "docs": "Flag to skip comparison with remote repositories upstream. Optional." }, ) + sign: bool = field( + default=False, + metadata={ + "docs": "Flag to write update date to target files and sign. Applicable when updating a repository that has dependencies." + } + ) def __attrs_post_init__(self): if self.operation == OperationType.CLONE: @@ -469,6 +477,10 @@ def _process_repo_update( child_auth_repos = repositoriesdb.get_deduplicated_auth_repositories( auth_repo, commits ).values() + + current_time = datetime.datetime.now() + formatted_time = current_time.strftime('%Y-%m-%d %H:%M:%S') + outputs, errors = _update_dependencies(update_config, child_auth_repos) if len(errors): errors = "\n".join(errors) @@ -495,6 +507,31 @@ def _process_repo_update( _process_repo_update( child_config, output, visited, repos_update_data, transient_data ) + if update_config.sign: + dependency_name = output.auth_repo_name + dependency_log_path = auth_repo.log_dir / f"{dependency_name}.json" + dependency_target_dir = dependency_log_path.parent + if not dependency_target_dir.is_dir(): + dependency_target_dir.mkdir() + target_content = { + "branch": output.users_auth_repo.default_branch, + "commit": output.commits_data.after_pull.value, + "update_time": formatted_time, + } + repos_data = [] + for repo_name, repo_data in output.targets_data.items(): + commits_data = {} + repo_content = { + "name": repo_name, + "commits": commits_data, + } + for branch, branch_data in repo_data["commits"].items(): + if branch_data["after_pull"]: + commits_data[branch] = branch_data["after_pull"][0]["commit"] + + repos_data.append(repo_content) + target_content["targets"] = repos_data + dependency_log_path.write_text(json.dumps(target_content, indent=4)) # do not call the handlers if only validating the repositories # if a handler fails and we are in the development mode, revert the update