From 033a9a677291eb298b44b8828a7d5f8f51856b4f Mon Sep 17 00:00:00 2001 From: Erik Moeller Date: Wed, 17 Feb 2021 18:50:31 -0800 Subject: [PATCH] Add branch information and troubleshooting tips --- admin/securedrop_admin/__init__.py | 30 ++++++++++++++++- admin/tests/test_securedrop-admin.py | 48 ++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py index cc1b50c84ac..daf9960f9d6 100755 --- a/admin/securedrop_admin/__init__.py +++ b/admin/securedrop_admin/__init__.py @@ -720,11 +720,23 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: update_status, latest_tag = check_for_updates(cli_args) if update_status is True: + + # Useful for troubleshooting + branch_status = get_git_branch(cli_args) + sdlog.error("You are not running the most recent signed SecureDrop release " "on this workstation.") sdlog.error("Latest available version: {}".format(latest_tag)) + + if branch_status is not None: + sdlog.error("Current branch status: {}".format(branch_status)) + else: + sdlog.error("Problem determining current branch status.") + sdlog.error("Running outdated or mismatched code can cause significant " "technical issues.") + sdlog.error("To display more information about your repository state, run:\n\n\t" + "git status\n") sdlog.error("If you are certain you want to proceed, run:\n\n\t" "./securedrop-admin --force {}\n".format(cmd_name)) sdlog.error("To apply the latest updates, run:\n\n\t" @@ -905,7 +917,10 @@ def check_for_updates(args: argparse.Namespace) -> Tuple[bool, str]: """Check for SecureDrop updates""" sdlog.info("Checking for SecureDrop updates...") - # Determine what branch we are on + # Determine what tag we are likely to be on. Caveat: git describe + # may produce very surprising results, because it will locate the most recent + # _reachable_ tag. However, in our current branching model, it can be + # relied on to determine if we're on the latest tag or not. current_tag = subprocess.check_output(['git', 'describe'], cwd=args.root).decode('utf-8').rstrip('\n') # noqa: E501 @@ -933,6 +948,19 @@ def check_for_updates(args: argparse.Namespace) -> Tuple[bool, str]: return False, latest_tag +def get_git_branch(args: argparse.Namespace) -> Optional[str]: + """ + Returns the starred line of `git branch` output. + """ + git_branch_raw = subprocess.check_output(['git', 'branch'], + cwd=args.root).decode('utf-8') + match = re.search(r"\* (.*)\n", git_branch_raw) + if match is not None and len(match.groups()) > 0: + return match.group(1) + else: + return None + + def get_release_key_from_keyserver( args: argparse.Namespace, keyserver: Optional[str] = None, timeout: int = 45 ) -> None: diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py index 167e05260fd..2f9fc2a4b2c 100644 --- a/admin/tests/test_securedrop-admin.py +++ b/admin/tests/test_securedrop-admin.py @@ -67,7 +67,9 @@ def test_update_check_decorator_when_no_update_needed(self, caplog): """ with mock.patch( "securedrop_admin.check_for_updates", side_effect=[[False, "1.5.0"]] - ) as mocked_check, mock.patch("sys.exit") as mocked_exit: + ) as mocked_check, mock.patch( + "securedrop_admin.get_git_branch", side_effect=["develop"] + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=False) rv = securedrop_admin.update_check_required("update_check_test")( @@ -85,11 +87,14 @@ def test_update_check_decorator_when_update_needed(self, caplog): And an update is required Then the update check should run to completion And an error referencing the command should be displayed + And the current branch state should be included in the output And the program should exit """ with mock.patch( "securedrop_admin.check_for_updates", side_effect=[[True, "1.5.0"]] - ) as mocked_check, mock.patch("sys.exit") as mocked_exit: + ) as mocked_check, mock.patch( + "securedrop_admin.get_git_branch", side_effect=["bad_branch"] + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=False) securedrop_admin.update_check_required("update_check_test")( @@ -98,6 +103,7 @@ def test_update_check_decorator_when_update_needed(self, caplog): assert mocked_check.called assert mocked_exit.called assert "update_check_test" in caplog.text + assert "bad_branch" in caplog.text def test_update_check_decorator_when_skipped(self, caplog): """ @@ -110,7 +116,9 @@ def test_update_check_decorator_when_skipped(self, caplog): """ with mock.patch( "securedrop_admin.check_for_updates", side_effect=[[True, "1.5.0"]] - ) as mocked_check, mock.patch("sys.exit") as mocked_exit: + ) as mocked_check, mock.patch( + "securedrop_admin.get_git_branch", side_effect=["develop"] + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=True) rv = securedrop_admin.update_check_required("update_check_test")( @@ -194,6 +202,40 @@ def test_check_for_updates_if_most_recent_tag_is_rc(self, tmpdir, caplog): assert update_status is False assert tag == '0.6.1' + @pytest.mark.parametrize( + "git_output, expected_rv", + [ + (b'* develop\n', + 'develop'), + (b' develop\n' + b'* release/1.7.0\n', + 'release/1.7.0'), + (b'* (HEAD detached at 1.7.0)\n' + b' develop\n' + b' release/1.7.0\n', + '(HEAD detached at 1.7.0)'), + (b' main\n' + b'* valid_+!@#$%&_branch_name\n', + 'valid_+!@#$%&_branch_name'), + (b'Unrecognized output.', + None) + ] + ) + def test_get_git_branch(self, git_output, expected_rv): + """ + When `git branch` completes with exit code 0 + And the output conforms to the expected format + Then `get_git_branch` should return a description of the current HEAD + + When `git branch` completes with exit code 0 + And the output does not conform to the expected format + Then `get_git_branch` should return `None` + """ + args = argparse.Namespace(root=None) + with mock.patch('subprocess.check_output', side_effect=[git_output]): + rv = securedrop_admin.get_git_branch(args) + assert rv == expected_rv + def test_update_exits_if_not_needed(self, tmpdir, caplog): git_repo_path = str(tmpdir) args = argparse.Namespace(root=git_repo_path)