From 17be32c80d69de20f0dad7bc23f8a2bff7bc7b49 Mon Sep 17 00:00:00 2001 From: Christian Siegel Date: Thu, 1 Oct 2020 12:41:23 +0000 Subject: [PATCH] feat(delete-preview,delete-pr-preview): add flag --expect-preview-exists If a preview doesn't exist yet, `delete-(pr-)-preview` runs successfully since nothing needs to be done. Only if the new flag `--expect-preview-exists` is set, it fails. --- docs/commands/delete-pr-preview.md | 19 +- docs/commands/delete-preview.md | 10 +- gitopscli/cliparser.py | 13 ++ gitopscli/commands/delete_pr_preview.py | 2 + gitopscli/commands/delete_preview.py | 38 ++-- tests/commands/test_delete_preview.py | 283 ++++++++++++++++++++++++ tests/test_cliparser.py | 26 ++- 7 files changed, 364 insertions(+), 27 deletions(-) create mode 100644 tests/commands/test_delete_preview.py diff --git a/docs/commands/delete-pr-preview.md b/docs/commands/delete-pr-preview.md index ed8304b3..6b105604 100644 --- a/docs/commands/delete-pr-preview.md +++ b/docs/commands/delete-pr-preview.md @@ -18,13 +18,16 @@ gitopscli delete-pr-preview \ ## Usage ``` -usage: gitopscli delete-pr-preview [-h] --username USERNAME --password PASSWORD - [--git-user GIT_USER] [--git-email GIT_EMAIL] - --organisation ORGANISATION --repository-name - REPOSITORY_NAME [--git-provider GIT_PROVIDER] - [--git-provider-url GIT_PROVIDER_URL] --branch - BRANCH - [-v [VERBOSE]] +usage: gitopscli delete-pr-preview [-h] --username USERNAME --password + PASSWORD [--git-user GIT_USER] + [--git-email GIT_EMAIL] --organisation + ORGANISATION --repository-name + REPOSITORY_NAME + [--git-provider GIT_PROVIDER] + [--git-provider-url GIT_PROVIDER_URL] + --branch BRANCH + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] + [-v [VERBOSE]] optional arguments: -h, --help show this help message and exit @@ -43,6 +46,8 @@ optional arguments: Git provider base API URL (e.g. https://bitbucket.example.tld) --branch BRANCH The branch for which the preview was created for + --expect-preview-exists [EXPECT_PREVIEW_EXISTS] + Fail if preview does not exist -v [VERBOSE], --verbose [VERBOSE] Verbose exception logging ``` \ No newline at end of file diff --git a/docs/commands/delete-preview.md b/docs/commands/delete-preview.md index 58b015f2..0c56cd0d 100644 --- a/docs/commands/delete-preview.md +++ b/docs/commands/delete-preview.md @@ -22,8 +22,9 @@ usage: gitopscli delete-preview [-h] --username USERNAME --password PASSWORD [--git-user GIT_USER] [--git-email GIT_EMAIL] --organisation ORGANISATION --repository-name REPOSITORY_NAME [--git-provider GIT_PROVIDER] - [--git-provider-url GIT_PROVIDER_URL] --preview-id - PREVIEW_ID + [--git-provider-url GIT_PROVIDER_URL] + --preview-id PREVIEW_ID + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] [-v [VERBOSE]] optional arguments: @@ -42,7 +43,10 @@ optional arguments: --git-provider-url GIT_PROVIDER_URL Git provider base API URL (e.g. https://bitbucket.example.tld) - --preview-id PREVIEW_ID The preview id for which the preview was created for + --preview-id PREVIEW_ID + The preview-id for which the preview was created for + --expect-preview-exists [EXPECT_PREVIEW_EXISTS] + Fail if preview does not exist -v [VERBOSE], --verbose [VERBOSE] Verbose exception logging ``` \ No newline at end of file diff --git a/gitopscli/cliparser.py b/gitopscli/cliparser.py index 3d81744f..e891ebc2 100644 --- a/gitopscli/cliparser.py +++ b/gitopscli/cliparser.py @@ -92,6 +92,7 @@ def __add_delete_preview_command_parser(subparsers): add_delete_preview_p.add_argument( "--preview-id", help="The preview-id for which the preview was created for", required=True ) + __add_expect_preview_exists_parser(add_delete_preview_p) __add_verbose_parser(add_delete_preview_p) @@ -101,6 +102,7 @@ def __add_delete_pr_preview_command_parser(subparsers): add_delete_preview_p.add_argument( "--branch", help="The branch for which the preview was created for", required=True ) + __add_expect_preview_exists_parser(add_delete_preview_p) __add_verbose_parser(add_delete_preview_p) @@ -146,6 +148,17 @@ def __add_create_prid_parser(subparsers): subparsers.add_argument("--parent-id", help="the id of the parent comment, in case of a reply", type=int) +def __add_expect_preview_exists_parser(subparsers): + subparsers.add_argument( + "--expect-preview-exists", + help="Fail if preview does not exist", + type=__str2bool, + nargs="?", + const=True, + default=False, + ) + + def __add_verbose_parser(subparsers): subparsers.add_argument( "-v", "--verbose", help="Verbose exception logging", type=__str2bool, nargs="?", const=True, default=False, diff --git a/gitopscli/commands/delete_pr_preview.py b/gitopscli/commands/delete_pr_preview.py index 27752b44..2eeddf2b 100644 --- a/gitopscli/commands/delete_pr_preview.py +++ b/gitopscli/commands/delete_pr_preview.py @@ -20,6 +20,7 @@ def delete_pr_preview_command( repository_name, git_provider, git_provider_url, + expect_preview_exists, ): assert command == "delete-pr-preview" @@ -73,6 +74,7 @@ def delete_pr_preview_command( git_provider, git_provider_url, branch, + expect_preview_exists, ) finally: diff --git a/gitopscli/commands/delete_preview.py b/gitopscli/commands/delete_preview.py index 6a784332..4942c04f 100644 --- a/gitopscli/commands/delete_preview.py +++ b/gitopscli/commands/delete_preview.py @@ -20,6 +20,7 @@ def delete_preview_command( git_provider, git_provider_url, preview_id, + expect_preview_exists, ): assert command is not None @@ -41,12 +42,12 @@ def delete_preview_command( ) apps_git.checkout("master") - logging.info("App repo branch master checkout successful") + logging.info("App repo '%s/%s' branch 'master' checkout successful", organisation, repository_name) try: gitops_config = GitOpsConfig(apps_git.get_full_file_path(".gitops.config.yaml")) except FileNotFoundError as ex: raise GitOpsException(f"Couldn't find .gitops.config.yaml") from ex - logging.info("Read GitOpsConfig: %s", gitops_config) + logging.info("Read .gitops.config.yaml") root_git = create_git( username, @@ -60,22 +61,33 @@ def delete_preview_command( root_tmp_dir, ) root_git.checkout("master") - logging.info("Config repo branch master checkout successful") - config_branch = "master" + logging.info( + "Config repo '%s/%s' branch 'master' checkout successful", + gitops_config.team_config_org, + gitops_config.team_config_repo, + ) hashed_preview_id = hashlib.sha256(preview_id.encode("utf-8")).hexdigest()[:8] preview_folder_name = gitops_config.application_name + "-" + hashed_preview_id + "-preview" logging.info("Preview folder name: %s", preview_folder_name) - branch_preview_env_exists = os.path.exists(root_git.get_full_file_path(preview_folder_name)) - logging.info("Is preview env already existing for branch? %s", branch_preview_env_exists) + preview_folder_full_path = root_git.get_full_file_path(preview_folder_name) + branch_preview_env_exists = os.path.exists(preview_folder_full_path) + + if expect_preview_exists and not branch_preview_env_exists: + raise GitOpsException(f"There was no preview with name: {preview_folder_name}") + if branch_preview_env_exists: - shutil.rmtree(root_git.get_full_file_path(preview_folder_name), ignore_errors=True) + shutil.rmtree(preview_folder_full_path, ignore_errors=True) + root_git.commit( + f"Delete preview environment for '{gitops_config.application_name}' and preview id '{preview_id}'." + ) + root_git.push("master") + logging.info("Pushed branch 'master'") else: - raise GitOpsException(f"There was no preview with name: {preview_folder_name}") - root_git.commit( - f"Delete preview environment for '{gitops_config.application_name}' and preview id '{preview_id}'." - ) - root_git.push(config_branch) - logging.info("Pushed branch %s", config_branch) + logging.info( + "No preview environment for '%s' and preview id '%s'. Nothing to do..", + gitops_config.application_name, + preview_id, + ) finally: delete_tmp_dir(apps_tmp_dir) diff --git a/tests/commands/test_delete_preview.py b/tests/commands/test_delete_preview.py new file mode 100644 index 00000000..8668c91d --- /dev/null +++ b/tests/commands/test_delete_preview.py @@ -0,0 +1,283 @@ +import unittest +from uuid import UUID +from types import SimpleNamespace +from unittest.mock import patch, MagicMock, Mock, call +import pytest +from gitopscli.gitops_exception import GitOpsException +from gitopscli.commands.delete_preview import delete_preview_command + + +class DeletePreviewCommandTest(unittest.TestCase): + def setUp(self): + def add_patch(target): + patcher = patch(target) + self.addCleanup(patcher.stop) + return patcher.start() + + # Monkey patch all external functions the command is using: + self.os_path_exists_mock = add_patch("gitopscli.commands.delete_preview.os.path.exists") + self.shutil_rmtree_mock = add_patch("gitopscli.commands.delete_preview.shutil.rmtree") + self.create_tmp_dir_mock = add_patch("gitopscli.commands.delete_preview.create_tmp_dir") + self.delete_tmp_dir_mock = add_patch("gitopscli.commands.delete_preview.delete_tmp_dir") + self.logging_mock = add_patch("gitopscli.commands.delete_preview.logging") + self.create_git_mock = add_patch("gitopscli.commands.delete_preview.create_git") + self.git_util_mock = MagicMock() + self.gitops_config_mock = add_patch("gitopscli.commands.delete_preview.GitOpsConfig") + self.gitops_config_team_config_org_mock = add_patch( + "gitopscli.commands.delete_preview.GitOpsConfig.team_config_org" + ) + + # Attach all mocks to a single mock manager + self.mock_manager = Mock() + self.mock_manager.attach_mock(self.create_tmp_dir_mock, "create_tmp_dir") + self.mock_manager.attach_mock(self.create_git_mock, "create_git") + self.mock_manager.attach_mock(self.git_util_mock, "git_util") + self.mock_manager.attach_mock(self.delete_tmp_dir_mock, "delete_tmp_dir") + self.mock_manager.attach_mock(self.os_path_exists_mock, "os.path.exists") + self.mock_manager.attach_mock(self.shutil_rmtree_mock, "shutil.rmtree") + self.mock_manager.attach_mock(self.logging_mock, "logging") + self.mock_manager.attach_mock(self.gitops_config_mock, "GitOpsConfig") + self.mock_manager.attach_mock(self.gitops_config_team_config_org_mock, "GitOpsConfig.team_config_org") + + # Define some common default return values + self.create_tmp_dir_side_effect_count = 0 + + def create_tmp_dir_side_effect(): + self.create_tmp_dir_side_effect_count += 1 + return f"/tmp/created-tmp-dir-{self.create_tmp_dir_side_effect_count}" + + self.create_tmp_dir_mock.side_effect = create_tmp_dir_side_effect + + self.create_git_mock.return_value = self.git_util_mock + self.git_util_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}" + self.git_util_mock.create_pull_request.return_value = "" + self.git_util_mock.get_pull_request_url.side_effect = lambda x: f"" + self.os_path_exists_mock.return_value = True + self.gitops_config_mock.return_value = SimpleNamespace( + team_config_org="TEAM_CONFIG_ORG", team_config_repo="TEAM_CONFIG_REPO", application_name="APP" + ) + + def test_delete_existing_happy_flow(self): + delete_preview_command( + command="delete-preview", + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + preview_id="PREVIEW_ID", + expect_preview_exists=False, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.create_tmp_dir(), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "ORGA", + "REPO", + "github", + None, + "/tmp/created-tmp-dir-1", + ), + call.git_util.checkout("master"), + call.logging.info("App repo '%s/%s' branch 'master' checkout successful", "ORGA", "REPO"), + call.git_util.get_full_file_path(".gitops.config.yaml"), + call.GitOpsConfig("/tmp/created-tmp-dir/.gitops.config.yaml"), + call.logging.info("Read .gitops.config.yaml"), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "TEAM_CONFIG_ORG", + "TEAM_CONFIG_REPO", + "github", + None, + "/tmp/created-tmp-dir-2", + ), + call.git_util.checkout("master"), + call.logging.info( + "Config repo '%s/%s' branch 'master' checkout successful", "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO" + ), + call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), + call.git_util.get_full_file_path("APP-685912d3-preview"), + call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), + call.shutil.rmtree("/tmp/created-tmp-dir/APP-685912d3-preview", ignore_errors=True), + call.git_util.commit("Delete preview environment for 'APP' and preview id 'PREVIEW_ID'."), + call.git_util.push("master"), + call.logging.info("Pushed branch 'master'"), + call.delete_tmp_dir("/tmp/created-tmp-dir-1"), + call.delete_tmp_dir("/tmp/created-tmp-dir-2"), + ] + + def test_delete_missing_happy_flow(self): + self.os_path_exists_mock.return_value = False + + delete_preview_command( + command="delete-preview", + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + preview_id="PREVIEW_ID", + expect_preview_exists=False, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.create_tmp_dir(), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "ORGA", + "REPO", + "github", + None, + "/tmp/created-tmp-dir-1", + ), + call.git_util.checkout("master"), + call.logging.info("App repo '%s/%s' branch 'master' checkout successful", "ORGA", "REPO"), + call.git_util.get_full_file_path(".gitops.config.yaml"), + call.GitOpsConfig("/tmp/created-tmp-dir/.gitops.config.yaml"), + call.logging.info("Read .gitops.config.yaml"), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "TEAM_CONFIG_ORG", + "TEAM_CONFIG_REPO", + "github", + None, + "/tmp/created-tmp-dir-2", + ), + call.git_util.checkout("master"), + call.logging.info( + "Config repo '%s/%s' branch 'master' checkout successful", "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO" + ), + call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), + call.git_util.get_full_file_path("APP-685912d3-preview"), + call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), + call.logging.info( + "No preview environment for '%s' and preview id '%s'. Nothing to do..", "APP", "PREVIEW_ID" + ), + call.delete_tmp_dir("/tmp/created-tmp-dir-1"), + call.delete_tmp_dir("/tmp/created-tmp-dir-2"), + ] + + def test_delete_missing_but_expected_error(self): + self.os_path_exists_mock.return_value = False + self.git_util_mock.checkout.side_effect + + FileNotFoundError + + with pytest.raises(GitOpsException) as ex: + delete_preview_command( + command="delete-preview", + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + preview_id="PREVIEW_ID", + expect_preview_exists=True, # we expect an existing preview + ) + self.assertEqual(str(ex.value), "There was no preview with name: APP-685912d3-preview") + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.create_tmp_dir(), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "ORGA", + "REPO", + "github", + None, + "/tmp/created-tmp-dir-1", + ), + call.git_util.checkout("master"), + call.logging.info("App repo '%s/%s' branch 'master' checkout successful", "ORGA", "REPO"), + call.git_util.get_full_file_path(".gitops.config.yaml"), + call.GitOpsConfig("/tmp/created-tmp-dir/.gitops.config.yaml"), + call.logging.info("Read .gitops.config.yaml"), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "TEAM_CONFIG_ORG", + "TEAM_CONFIG_REPO", + "github", + None, + "/tmp/created-tmp-dir-2", + ), + call.git_util.checkout("master"), + call.logging.info( + "Config repo '%s/%s' branch 'master' checkout successful", "TEAM_CONFIG_ORG", "TEAM_CONFIG_REPO" + ), + call.logging.info("Preview folder name: %s", "APP-685912d3-preview"), + call.git_util.get_full_file_path("APP-685912d3-preview"), + call.os.path.exists("/tmp/created-tmp-dir/APP-685912d3-preview"), + call.delete_tmp_dir("/tmp/created-tmp-dir-1"), + call.delete_tmp_dir("/tmp/created-tmp-dir-2"), + ] + + def test_missing_gitops_config_yaml_error(self): + self.gitops_config_mock.side_effect = FileNotFoundError() + + with pytest.raises(GitOpsException) as ex: + delete_preview_command( + command="delete-preview", + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + preview_id="PREVIEW_ID", + expect_preview_exists=True, # we expect an existing preview + ) + self.assertEqual(str(ex.value), "Couldn't find .gitops.config.yaml") + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.create_tmp_dir(), + call.create_git( + "USERNAME", + "PASSWORD", + "GIT_USER", + "GIT_EMAIL", + "ORGA", + "REPO", + "github", + None, + "/tmp/created-tmp-dir-1", + ), + call.git_util.checkout("master"), + call.logging.info("App repo '%s/%s' branch 'master' checkout successful", "ORGA", "REPO"), + call.git_util.get_full_file_path(".gitops.config.yaml"), + call.GitOpsConfig("/tmp/created-tmp-dir/.gitops.config.yaml"), + call.delete_tmp_dir("/tmp/created-tmp-dir-1"), + call.delete_tmp_dir("/tmp/created-tmp-dir-2"), + ] diff --git a/tests/test_cliparser.py b/tests/test_cliparser.py index c8208155..41786633 100644 --- a/tests/test_cliparser.py +++ b/tests/test_cliparser.py @@ -165,7 +165,9 @@ --organisation ORGANISATION --repository-name REPOSITORY_NAME [--git-provider GIT_PROVIDER] [--git-provider-url GIT_PROVIDER_URL] - --preview-id PREVIEW_ID [-v [VERBOSE]] + --preview-id PREVIEW_ID + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] + [-v [VERBOSE]] gitopscli delete-preview: error: the following arguments are required: --username, --password, --organisation, --repository-name, --preview-id """ @@ -177,7 +179,9 @@ REPOSITORY_NAME [--git-provider GIT_PROVIDER] [--git-provider-url GIT_PROVIDER_URL] - --branch BRANCH [-v [VERBOSE]] + --branch BRANCH + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] + [-v [VERBOSE]] gitopscli delete-pr-preview: error: the following arguments are required: --username, --password, --organisation, --repository-name, --branch """ @@ -187,7 +191,9 @@ --organisation ORGANISATION --repository-name REPOSITORY_NAME [--git-provider GIT_PROVIDER] [--git-provider-url GIT_PROVIDER_URL] - --preview-id PREVIEW_ID [-v [VERBOSE]] + --preview-id PREVIEW_ID + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] + [-v [VERBOSE]] optional arguments: -h, --help show this help message and exit @@ -207,6 +213,8 @@ https://bitbucket.example.tld) --preview-id PREVIEW_ID The preview-id for which the preview was created for + --expect-preview-exists [EXPECT_PREVIEW_EXISTS] + Fail if preview does not exist -v [VERBOSE], --verbose [VERBOSE] Verbose exception logging """ @@ -219,7 +227,9 @@ REPOSITORY_NAME [--git-provider GIT_PROVIDER] [--git-provider-url GIT_PROVIDER_URL] - --branch BRANCH [-v [VERBOSE]] + --branch BRANCH + [--expect-preview-exists [EXPECT_PREVIEW_EXISTS]] + [-v [VERBOSE]] optional arguments: -h, --help show this help message and exit @@ -238,6 +248,8 @@ Git provider base API URL (e.g. https://bitbucket.example.tld) --branch BRANCH The branch for which the preview was created for + --expect-preview-exists [EXPECT_PREVIEW_EXISTS] + Fail if preview does not exist -v [VERBOSE], --verbose [VERBOSE] Verbose exception logging """ @@ -728,6 +740,7 @@ def test_delete_preview_required_args(self): self.assertIsNone(cli.git_provider) self.assertIsNone(cli.git_provider_url) + self.assertFalse(cli.expect_preview_exists) self.assertFalse(cli.verbose) def test_delete_preview_all_args(self): @@ -752,6 +765,7 @@ def test_delete_preview_all_args(self): "REPO", "--preview-id", "abc123", + "--expect-preview-exists", "-v", "n", ] @@ -768,6 +782,7 @@ def test_delete_preview_all_args(self): self.assertEqual(cli.git_provider, "GIT_PROVIDER") self.assertEqual(cli.git_provider_url, "GIT_PROVIDER_URL") + self.assertTrue(cli.expect_preview_exists) self.assertFalse(cli.verbose) def test_delete_pr_preview_no_args(self): @@ -820,6 +835,7 @@ def test_delete_pr_preview_required_args(self): self.assertIsNone(cli.git_provider) self.assertIsNone(cli.git_provider_url) + self.assertFalse(cli.expect_preview_exists) self.assertFalse(cli.verbose) def test_delete_pr_preview_all_args(self): @@ -844,6 +860,7 @@ def test_delete_pr_preview_all_args(self): "REPO", "--branch", "BRANCH", + "--expect-preview-exists", "-v", "n", ] @@ -860,6 +877,7 @@ def test_delete_pr_preview_all_args(self): self.assertEqual(cli.git_provider, "GIT_PROVIDER") self.assertEqual(cli.git_provider_url, "GIT_PROVIDER_URL") + self.assertTrue(cli.expect_preview_exists) self.assertFalse(cli.verbose) def test_deploy_no_args(self):