From 19727b9c471428b0a7bb122a5b53834359c8ccf4 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:40:59 +0100 Subject: [PATCH 01/20] :recycle: Moved `GANDI_FUND_THRESHOLD` variables to `config.constants` --- bin/alert_on_low_gandi_funds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/alert_on_low_gandi_funds.py b/bin/alert_on_low_gandi_funds.py index 1d9782eed..2700d5bda 100644 --- a/bin/alert_on_low_gandi_funds.py +++ b/bin/alert_on_low_gandi_funds.py @@ -19,6 +19,7 @@ def low_gandi_funds_message(remaining_gandi_funds): ) return msg +from config.constants import GANDI_FUND_THRESHOLD def alert_on_low_gandi_funds(): From bc0c22ea53b750127bb775e5d332f88eb45be442 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:42:10 +0100 Subject: [PATCH 02/20] :fire: Removed message construction from bin scripts where relevant. --- bin/alert_on_low_gandi_funds.py | 20 +---------- bin/alert_on_low_github_actions_quota.py | 8 ++--- bin/alert_on_low_github_licences.py | 42 ++++-------------------- bin/check_for_new_github_owners.py | 26 ++------------- bin/fetch_github_joiner_metrics.py | 23 ++----------- bin/generate_pat_token_report.py | 19 ++--------- bin/identify_dormant_github_users.py | 17 +++------- bin/support_stats_reporting.py | 11 ++----- 8 files changed, 25 insertions(+), 141 deletions(-) diff --git a/bin/alert_on_low_gandi_funds.py b/bin/alert_on_low_gandi_funds.py index 2700d5bda..ecd5165f5 100644 --- a/bin/alert_on_low_gandi_funds.py +++ b/bin/alert_on_low_gandi_funds.py @@ -2,23 +2,6 @@ import sys from services.gandi_service import GandiService from services.slack_service import SlackService -from config.constants import SLACK_CHANNEL - -GANDI_FUND_THRESHOLD = 500 - - -def low_gandi_funds_message(remaining_gandi_funds): - msg = ( - f"Hi all, \n\n" - f"This is an alert do inform you that Gandi funds are low. \n\n" - f"*:warning: We currently have £{remaining_gandi_funds} left out of £{GANDI_FUND_THRESHOLD}*\n\n" - f"Please read the following runbook for next steps:\n" - f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates\n\n" - f"Have a swell day, \n\n" - "The GitHub Organisation Monitoring Bot" - ) - - return msg from config.constants import GANDI_FUND_THRESHOLD @@ -40,8 +23,7 @@ def alert_on_low_gandi_funds(): remaining_gandi_funds = gandi_service.get_current_account_balance_from_org(organisation_id) if remaining_gandi_funds < GANDI_FUND_THRESHOLD: - SlackService(slack_token).send_message_to_plaintext_channel_name( - low_gandi_funds_message(remaining_gandi_funds), SLACK_CHANNEL) + SlackService(slack_token).send_low_gandi_funds_alert(remaining_gandi_funds, GANDI_FUND_THRESHOLD) if __name__ == "__main__": diff --git a/bin/alert_on_low_github_actions_quota.py b/bin/alert_on_low_github_actions_quota.py index 402aab554..b7c77bd1a 100644 --- a/bin/alert_on_low_github_actions_quota.py +++ b/bin/alert_on_low_github_actions_quota.py @@ -2,11 +2,7 @@ import sys from services.github_service import GithubService from services.slack_service import SlackService -from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE, SLACK_CHANNEL - - -def low_threshold_triggered_message(percentage_used): - return f"Warning:\n\n {round(100 - percentage_used, 1)}% of the Github Actions minutes quota remains.\n\n What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" +from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE def alert_on_low_quota(): @@ -25,7 +21,7 @@ def alert_on_low_quota(): check_result = github_service.check_if_gha_minutes_quota_is_low() if check_result is not False: - slack_service.send_message_to_plaintext_channel_name(low_threshold_triggered_message(check_result['percentage_used']), SLACK_CHANNEL) + slack_service.send_low_github_actions_quota_alert(check_result['percentage_used']) github_service.modify_gha_minutes_quota_threshold(check_result['threshold'] + 10) diff --git a/bin/alert_on_low_github_licences.py b/bin/alert_on_low_github_licences.py index 54a98cd75..fa7602c73 100644 --- a/bin/alert_on_low_github_licences.py +++ b/bin/alert_on_low_github_licences.py @@ -1,37 +1,11 @@ -""" -GitHub License Monitoring Script - -Purpose: --------- -This script monitors the number of available licenses in a GitHub Enterprise account. -It is designed to provide alerts when the license count falls below a -specified threshold. -""" - import os import sys -from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE, SLACK_CHANNEL +from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE, GITHUB_LICENSE_THRESHOLD from services.github_service import GithubService from services.slack_service import SlackService -def low_threshold_triggered_message(remaining_licences): - msg = ( - f"Hi team 👋, \n\n" - f"There are only {remaining_licences} \ - GitHub licences remaining in the enterprise account. \n\n" - f"Please add more licences using the instructions outlined here: \n" - f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html \n\n" - - f"Thanks, \n\n" - - "The GitHub Licence Alerting Bot" - ) - - return msg - - -def alert_on_low_github_licences(threshold): +def alert_on_low_github_licenses(): github_token = os.environ.get("ADMIN_GITHUB_TOKEN") if github_token is None: print("No GITHUB_TOKEN environment variable set") @@ -41,18 +15,16 @@ def alert_on_low_github_licences(threshold): print("No SLACK_TOKEN environment variable set") sys.exit(1) - remaining_licences = GithubService( + remaining_licenses = GithubService( github_token, MINISTRY_OF_JUSTICE, ENTERPRISE).get_remaining_licences() - trigger_alert = remaining_licences < threshold + trigger_alert = remaining_licenses < GITHUB_LICENSE_THRESHOLD if trigger_alert: print( - f"Low number of GitHub licences remaining, only {remaining_licences} remaining") + f"Low number of GitHub licenses remaining, only {remaining_licenses} remaining") - SlackService(slack_token). \ - send_message_to_plaintext_channel_name( - low_threshold_triggered_message(remaining_licences), SLACK_CHANNEL) + SlackService(slack_token).send_low_github_licenses_alert(remaining_licenses) if __name__ == "__main__": - alert_on_low_github_licences(20) + alert_on_low_github_licenses() diff --git a/bin/check_for_new_github_owners.py b/bin/check_for_new_github_owners.py index 57984016f..3a7c22dd4 100644 --- a/bin/check_for_new_github_owners.py +++ b/bin/check_for_new_github_owners.py @@ -1,6 +1,6 @@ import os from datetime import datetime, timedelta -from config.constants import MINISTRY_OF_JUSTICE, SLACK_CHANNEL +from config.constants import MINISTRY_OF_JUSTICE from services.github_service import GithubService from services.slack_service import SlackService @@ -13,25 +13,6 @@ def _calculate_date(in_last_days: int) -> str: return date.strftime(timestamp_format) -def new_owner_detected_message(new_owner, date_added, added_by, org, audit_log_url): - msg = ( - f"Hi all, \n\n" - f"A new owner has been detected in the `{org}` GitHub org. \n\n" - f"*New owner:* {new_owner}\n" - f"*Date added:* {date_added}\n" - f"*By who:* {added_by}\n\n" - - f"Please review the audit log for more details: {audit_log_url}\n\n" - - - f"Thanks, \n\n" - - "The GitHub Organisation Monitoring Bot" - ) - - return msg - - def check_for_new_organisation_owners(in_last_days: int): audit_log_url = f"https://github.com/organizations/{MINISTRY_OF_JUSTICE}/settings/audit-log?q=action%3Aorg.add_member++action%3Aorg.update_member" @@ -46,10 +27,7 @@ def check_for_new_organisation_owners(in_last_days: int): if changes: for change in changes: - message = new_owner_detected_message( - change["userLogin"], change["createdAt"], change["actorLogin"], MINISTRY_OF_JUSTICE, audit_log_url) - slack.send_message_to_plaintext_channel_name( - message, SLACK_CHANNEL) + slack.send_new_github_owners_alert(change["userLogin"], change["createdAt"], change["actorLogin"], MINISTRY_OF_JUSTICE, audit_log_url) if __name__ == "__main__": diff --git a/bin/fetch_github_joiner_metrics.py b/bin/fetch_github_joiner_metrics.py index 0a8c3d3fa..a39680c59 100644 --- a/bin/fetch_github_joiner_metrics.py +++ b/bin/fetch_github_joiner_metrics.py @@ -1,6 +1,6 @@ import os from datetime import datetime, timedelta -from config.constants import MINISTRY_OF_JUSTICE, SLACK_CHANNEL, OPERATIONS_ENGINEERING_GITHUB_USERNAMES +from config.constants import MINISTRY_OF_JUSTICE, OPERATIONS_ENGINEERING_GITHUB_USERNAMES from services.github_service import GithubService from services.slack_service import SlackService @@ -26,23 +26,6 @@ def _calculate_date(in_last_days: int) -> str: return date.strftime(timestamp_format) -def new_members_detected_message(new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): - msg = ( - f"Hi all, \n\n" - f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. \n\n" - f"*Added by Operations Engineering:*\n" - f"{new_members_added_by_oe}\n\n" - f"*Added externally:*\n" - f"{new_members_added_externally}\n\n" - f"{percentage}% of the new joiners were added by operations engineering.\n\n" - f"Please review the audit log for more details: {audit_log_url}\n\n" - f"Have a swell day, \n\n" - "The GitHub Organisation Monitoring Bot" - ) - - return msg - - def main(): audit_log_url = f"https://github.com/organizations/{MINISTRY_OF_JUSTICE}/settings/audit-log?q=action%3Aorg.add_member" @@ -67,8 +50,8 @@ def main(): new_members_added_externally += individual_message percentage = round((total_members_added_by_oe / len(new_members)) * 100) - slack_service.send_message_to_plaintext_channel_name( - new_members_detected_message(new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, MINISTRY_OF_JUSTICE, audit_log_url, time_delta_in_days), SLACK_CHANNEL) + slack_service.send_new_github_joiner_metrics_alert( + new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, MINISTRY_OF_JUSTICE, audit_log_url, time_delta_in_days) if __name__ == "__main__": diff --git a/bin/generate_pat_token_report.py b/bin/generate_pat_token_report.py index cb49a0f35..c6eca0e2f 100644 --- a/bin/generate_pat_token_report.py +++ b/bin/generate_pat_token_report.py @@ -1,5 +1,5 @@ import os -from config.constants import MINISTRY_OF_JUSTICE, SLACK_CHANNEL +from config.constants import MINISTRY_OF_JUSTICE from services.github_service import GithubService from services.slack_service import SlackService @@ -18,21 +18,6 @@ def get_environment_variables() -> tuple: return slack_token, github_token -def expired_tokens_message(): - msg = ( - "Hi team 👋, \n\n" - "Some expired PAT(s) have been detected. \n\n" - "Please review the current list here: \n" - "https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active \n\n" - - "Have a swell day, \n\n" - - "The GitHub PAT Bot" - ) - - return msg - - def count_expired_tokens(pat_tokens): expired_count = 0 for token in pat_tokens: @@ -50,7 +35,7 @@ def generate_pat_token_report(): pat_tokens = github_service.get_new_pat_creation_events_for_organization() if count_expired_tokens(pat_tokens) > 0: - slack_service.send_message_to_plaintext_channel_name(expired_tokens_message(), SLACK_CHANNEL) + slack_service.send_pat_report_alert() if __name__ == "__main__": diff --git a/bin/identify_dormant_github_users.py b/bin/identify_dormant_github_users.py index cfa6f01b6..eb5c2de58 100644 --- a/bin/identify_dormant_github_users.py +++ b/bin/identify_dormant_github_users.py @@ -16,7 +16,6 @@ CSV_FILE_NAME = "dormant.csv" MOJ_ORGANISATION = "ministryofjustice" AP_ORGANISATION = "moj-analytical-services" -SLACK_CHANNEL = "operations-engineering-alerts" # These are the users that are deemed acceptable to be dormant. # They are either bots or service accounts and will be revisited regularly. ALLOWED_BOT_USERS = [ @@ -143,12 +142,8 @@ def filter_out_active_auth0_users(dormant_users_according_to_github: list) -> li return dormant_users_not_in_auth0 -def message_to_slack_channel(dormant_users: list) -> str: - msg = ( - "Hello 🤖, \n\n" - "Here is a list of dormant GitHub users that have not been seen in Auth0 logs:\n\n" - ) - +def _create_dormant_user_list(dormant_users: list) -> str: + msg = "" for user in dormant_users: msg += f"{user.name} | {user.email}\n" @@ -187,14 +182,12 @@ def identify_dormant_github_users(): githubs_list_of_dormant_users = get_dormant_users_from_github_csv( moj_github_org, ap_github_org) - dormant_users_accoding_to_github_and_auth0 = filter_out_active_auth0_users( + dormant_users_according_to_github_and_auth0 = filter_out_active_auth0_users( githubs_list_of_dormant_users ) - SlackService(env.get("ADMIN_SLACK_TOKEN")).send_message_to_plaintext_channel_name( - message_to_slack_channel(dormant_users_accoding_to_github_and_auth0), - SLACK_CHANNEL, - ) + SlackService(env.get("ADMIN_SLACK_TOKEN")).send_dormant_user_list( + _create_dormant_user_list(dormant_users_according_to_github_and_auth0)) if __name__ == "__main__": diff --git a/bin/support_stats_reporting.py b/bin/support_stats_reporting.py index 886f4807e..35017f675 100644 --- a/bin/support_stats_reporting.py +++ b/bin/support_stats_reporting.py @@ -6,7 +6,6 @@ import pandas as pd from collections import defaultdict -from config.constants import SR_SLACK_CHANNEL from services.slack_service import SlackService @@ -67,7 +66,7 @@ def get_dict_of_requests_and_volume( return dict_of_requests -def craft_message_to_slack( +def craft_support_statistics( yesterdays_support_requests: list[SupportRequest], date_today=date.today() ): dict_of_requests_and_volume = get_dict_of_requests_and_volume( @@ -120,13 +119,9 @@ def main(todays_date=date.today(), file_path="data/support_stats/support_stats.c yesterdays_requests = get_yesterdays_support_requests( all_support_requests, todays_date ) - slack_message = craft_message_to_slack(yesterdays_requests, todays_date) + support_statistics = craft_support_statistics(yesterdays_requests, todays_date) - print(slack_message) - - slack_service.send_message_to_plaintext_channel_name( - slack_message, SR_SLACK_CHANNEL - ) + slack_service.send_slack_support_stats_report(support_statistics) if __name__ == "__main__": From bccfdac2099f72c955be8c61f78bd70a50744d57 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:42:58 +0100 Subject: [PATCH 03/20] :sparkles: `GANDI_FUND_THRESHOLD` and `GITHUB_LICENSE_THRESHOLD` added to constants --- config/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/constants.py b/config/constants.py index 18da765fd..d99803941 100644 --- a/config/constants.py +++ b/config/constants.py @@ -1,4 +1,6 @@ # class Constants: +GANDI_FUND_THRESHOLD = 500 +GITHUB_LICENSE_THRESHOLD = 20 MINISTRY_OF_JUSTICE = "ministryofjustice" MOJ_ANALYTICAL_SERVICES = "moj-analytical-services" MISSING_EMAIL_ADDRESS = "-" From e06ef3bc35dcd75eb63e6b94d69a8c369c47d32d Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:43:26 +0100 Subject: [PATCH 04/20] :recycle: Refactored bin script tests to work with new Slack message practices --- .../test_bin/test_alert_on_low_gandi_funds.py | 4 +-- .../test_alert_on_low_github_actions_quota.py | 27 +++++++------------ .../test_generate_pat_token_report.py | 8 +++--- .../test_identify_dormant_github_users.py | 19 +------------ test/test_bin/test_low_github_licences.py | 22 +++------------ test/test_bin/test_support_stats_reporting.py | 19 +++++++------ 6 files changed, 28 insertions(+), 71 deletions(-) diff --git a/test/test_bin/test_alert_on_low_gandi_funds.py b/test/test_bin/test_alert_on_low_gandi_funds.py index 9b1a04c09..cb15b8cc1 100644 --- a/test/test_bin/test_alert_on_low_gandi_funds.py +++ b/test/test_bin/test_alert_on_low_gandi_funds.py @@ -19,7 +19,7 @@ def test_alert_triggered_below_threshold(self, mock_slack_service, mock_gandi_se alert_on_low_gandi_funds() - mock_slack_instance.send_message_to_plaintext_channel_name.assert_called_once() + mock_slack_instance.send_low_gandi_funds_alert.assert_called_once() @patch.dict('os.environ', {'GANDI_FUNDS_TOKEN': 'test_gandi_token', 'ADMIN_SLACK_TOKEN': 'test_slack_token', 'GANDI_ORG_ID': 'test_org_id'}) @patch('bin.alert_on_low_gandi_funds.GandiService') @@ -31,7 +31,7 @@ def test_no_alert_triggered_above_threshold(self, mock_slack_service, mock_gandi alert_on_low_gandi_funds() - mock_slack_instance.send_message_to_plaintext_channel_name.assert_not_called() + mock_slack_instance.send_low_gandi_funds_alert.assert_not_called() @patch.dict('os.environ', {}) def test_exit_no_gandi_funds_token(self): diff --git a/test/test_bin/test_alert_on_low_github_actions_quota.py b/test/test_bin/test_alert_on_low_github_actions_quota.py index 0bc701125..0a2aaf24b 100644 --- a/test/test_bin/test_alert_on_low_github_actions_quota.py +++ b/test/test_bin/test_alert_on_low_github_actions_quota.py @@ -2,7 +2,6 @@ from unittest.mock import patch, MagicMock from bin.alert_on_low_github_actions_quota import ( - low_threshold_triggered_message, alert_on_low_quota ) @@ -12,50 +11,42 @@ class TestGithubACtionsQuotaAlerting(unittest.TestCase): - def test_low_threshold_triggered_message(self): - - self.assertEqual(low_threshold_triggered_message(10), "Warning:\n\n 90% of the Github Actions minutes quota remains.\n\n What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure") - @patch("gql.transport.aiohttp.AIOHTTPTransport.__new__", new=MagicMock) @patch("gql.Client.__new__", new=MagicMock) @patch("github.Github.__new__") - @patch("bin.alert_on_low_github_actions_quota.low_threshold_triggered_message") @patch.object(GithubService, "modify_gha_minutes_quota_threshold") - @patch.object(SlackService, "send_message_to_plaintext_channel_name") + @patch.object(SlackService, "send_low_github_actions_quota_alert") @patch.object(GithubService, "check_if_gha_minutes_quota_is_low") - @patch('os.environ') + @patch("os.environ.get") def test_alert_on_low_quota_if_low( self, - mock_env, + mock_get_env, mock_check_if_gha_minutes_quota_is_low, - mock_send_message_to_plaintext_channel_name, + mock_send_low_github_actions_quota_alert, mock_modify_gha_minutes_quota_threshold, - mock_low_threshold_triggered_message, _mock_github_client_core_api ): - - mock_env.get.side_effect = lambda k: 'mock_token' if k in ['GH_TOKEN', 'ADMIN_SLACK_TOKEN'] else None + mock_get_env.side_effect = lambda k: 'mock_token' if k in ['GH_TOKEN', 'ADMIN_SLACK_TOKEN'] else None mock_check_if_gha_minutes_quota_is_low.return_value = {'threshold': 70, 'percentage_used': 75} - mock_low_threshold_triggered_message.return_value = "Warning:\n\n 25% of the Github Actions minutes quota remains.\n\n What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" alert_on_low_quota() mock_check_if_gha_minutes_quota_is_low.assert_called_once() - mock_send_message_to_plaintext_channel_name.assert_called_once_with("Warning:\n\n 25% of the Github Actions minutes quota remains.\n\n What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure", "operations-engineering-alerts") + mock_send_low_github_actions_quota_alert.assert_called_once_with(75) mock_modify_gha_minutes_quota_threshold.assert_called_once_with(80) @patch("gql.transport.aiohttp.AIOHTTPTransport.__new__", new=MagicMock) @patch("gql.Client.__new__", new=MagicMock) @patch("github.Github.__new__") @patch.object(GithubService, "modify_gha_minutes_quota_threshold") - @patch.object(SlackService, "send_message_to_plaintext_channel_name") + @patch.object(SlackService, "send_low_github_actions_quota_alert") @patch.object(GithubService, "check_if_gha_minutes_quota_is_low") @patch('os.environ') def test_alert_on_low_quota_if_not_low( self, mock_env, mock_check_if_gha_minutes_quota_is_low, - mock_send_message_to_plaintext_channel_name, + mock_send_low_github_actions_quota_alert, mock_modify_gha_minutes_quota_threshold, _mock_github_client_core_api ): @@ -66,7 +57,7 @@ def test_alert_on_low_quota_if_not_low( alert_on_low_quota() mock_check_if_gha_minutes_quota_is_low.assert_called_once() - assert not mock_send_message_to_plaintext_channel_name.called + assert not mock_send_low_github_actions_quota_alert.called assert not mock_modify_gha_minutes_quota_threshold.called diff --git a/test/test_bin/test_generate_pat_token_report.py b/test/test_bin/test_generate_pat_token_report.py index dd1ff9920..5ce327b23 100644 --- a/test/test_bin/test_generate_pat_token_report.py +++ b/test/test_bin/test_generate_pat_token_report.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch from config.constants import SLACK_CHANNEL -from bin.generate_pat_token_report import generate_pat_token_report, expired_tokens_message, count_expired_tokens +from bin.generate_pat_token_report import generate_pat_token_report, count_expired_tokens from services.github_service import GithubService from services.slack_service import SlackService @@ -22,20 +22,20 @@ def test_count_expired_tokens(self): @patch('bin.generate_pat_token_report.count_expired_tokens') @patch.object(GithubService, "__new__") @patch.object(GithubService, 'get_new_pat_creation_events_for_organization') - @patch.object(SlackService, 'send_message_to_plaintext_channel_name') + @patch.object(SlackService, 'send_pat_report_alert') def test_main_expired_tokens_found(self, mock_send_message, mock_get_pat_events, _mock_github_service, mock_count_expired_tokens): mock_get_pat_events.return_value = [{'token_expired': True}, {'token_expired': True}] mock_count_expired_tokens.return_value = 2 generate_pat_token_report() - mock_send_message.assert_called_once_with(expired_tokens_message(), SLACK_CHANNEL) + mock_send_message.assert_called_once() @patch.dict('os.environ', {'ADMIN_SLACK_TOKEN': 'mock_slack_token', 'GH_APP_TOKEN': 'mock_gh_token'}) @patch('bin.generate_pat_token_report.count_expired_tokens') @patch.object(GithubService, "__new__") @patch.object(GithubService, 'get_new_pat_creation_events_for_organization') - @patch.object(SlackService, 'send_message_to_plaintext_channel_name') + @patch.object(SlackService, 'send_pat_report_alert') def test_main_no_expired_tokens_found(self, mock_send_message, mock_get_pat_events, _mock_github_service, mock_count_expired_tokens): mock_get_pat_events.return_value = [{'token_expired': False}, {'token_expired': False}] mock_count_expired_tokens.return_value = 0 diff --git a/test/test_bin/test_identify_dormant_github_users.py b/test/test_bin/test_identify_dormant_github_users.py index 4d01ddd1f..ffee7d081 100644 --- a/test/test_bin/test_identify_dormant_github_users.py +++ b/test/test_bin/test_identify_dormant_github_users.py @@ -3,7 +3,7 @@ from bin.identify_dormant_github_users import ( ALLOWED_BOT_USERS, DormantUser, filter_out_active_auth0_users, - get_active_users_from_auth0_log_group, message_to_slack_channel) + get_active_users_from_auth0_log_group) class TestDormantGitHubUsers(unittest.TestCase): @@ -39,23 +39,6 @@ def test_filter_out_active_auth0_users(self, mock_get_active_users): self.assertIn(expected_result[0], result) - def test_message_to_slack_channel(self): - dormant_users = [ - DormantUser(name='user1', email='user1@example.com'), - DormantUser(name='user2', email='user2@example.com') - ] - - result = message_to_slack_channel(dormant_users) - - expected_message = ( - "Hello 🤖, \n\n" - "Here is a list of dormant GitHub users that have not been seen in Auth0 logs:\n\n" - "user1 | user1@example.com\n" - "user2 | user2@example.com\n" - ) - - self.assertEqual(result, expected_message) - if __name__ == "__main__": unittest.main() diff --git a/test/test_bin/test_low_github_licences.py b/test/test_bin/test_low_github_licences.py index f3fd128d1..8ec21fbbd 100644 --- a/test/test_bin/test_low_github_licences.py +++ b/test/test_bin/test_low_github_licences.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch, MagicMock -from bin.alert_on_low_github_licences import low_threshold_triggered_message, alert_on_low_github_licences +from bin.alert_on_low_github_licences import alert_on_low_github_licenses from services.github_service import GithubService from services.slack_service import SlackService @@ -9,22 +9,6 @@ class TestGithubLicenseAlerting(unittest.TestCase): - def test_low_threshold_triggered_message(self): - """ Test the message format for low threshold alert. """ - remaining_licences = 5 - expected_message = ( - "Hi team 👋, \n\n" - "There are only 5 GitHub licences remaining in the enterprise account. \n\n" - "Please add more licences using the instructions outlined here: \n" - "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html \n\n" - - "Thanks, \n\n" - - "The GitHub Licence Alerting Bot" - ) - self.assertEqual(low_threshold_triggered_message( - remaining_licences), expected_message) - @patch.object(GithubService, "__new__") @patch.object(SlackService, "__new__") @patch('os.environ') @@ -40,9 +24,9 @@ def test_alert_on_low_github_licences(self, mock_env, mock_slack_service, mock_g mock_slack_instance = MagicMock() mock_slack_service.return_value = mock_slack_instance - alert_on_low_github_licences(10) + alert_on_low_github_licenses() - mock_slack_instance.send_message_to_plaintext_channel_name.assert_called() + mock_slack_instance.send_low_github_licenses_alert.assert_called() if __name__ == '__main__': diff --git a/test/test_bin/test_support_stats_reporting.py b/test/test_bin/test_support_stats_reporting.py index 644a382d5..5e8939b45 100644 --- a/test/test_bin/test_support_stats_reporting.py +++ b/test/test_bin/test_support_stats_reporting.py @@ -8,7 +8,7 @@ get_yesterdays_support_requests, get_previous_working_day, get_environment_variables, - craft_message_to_slack, + craft_support_statistics, main, ) @@ -74,7 +74,6 @@ def test_raises_error_when_no_slack_token(self): class TestCraftMessageToSlack(unittest.TestCase): def test_slack_message(self): - date_today = date(2024, 7, 16) yesterdays_support_requests = [ SupportRequest( @@ -83,12 +82,14 @@ def test_slack_message(self): request_date="2024-07-15", ) ] - expected_message = "On 2024-07-15 we received 1 Support Requests: \n\n--\n*Type:* GitHub\n*Action:* Add user to Org\n*Number of requests:* 1\n" - self.assertEqual( - craft_message_to_slack(yesterdays_support_requests, date_today), - expected_message, + expected_message = ( + "On 2024-07-15 we received 1 Support Requests: \n\n" + "--\n*Type:* GitHub\n*Action:* Add user to Org\n*Number of requests:* 1\n" ) + result_message = craft_support_statistics(yesterdays_support_requests, date_today) + self.assertEqual(result_message, expected_message) + class TestMain(unittest.TestCase): @@ -101,7 +102,5 @@ def test_slack_message_sent_to_slack(self, mock_slack_service: MagicMock): main(todays_date, file_path) - mock_slack_service.return_value.send_message_to_plaintext_channel_name.assert_called_with( - "On 2024-07-22 we received 8 Support Requests: \n\n--\n*Type:* GitHub\n*Action:* GitHub – add user to org\n*Number of requests:* 2\n--\n*Type:* GitHub\n*Action:* GitHub – remove user from org\n*Number of requests:* 1\n--\n*Type:* 1Password\n*Action:* 1Password - information/help\n*Number of requests:* 1\n--\n*Type:* API\n*Action:* API Key\n*Number of requests:* 1\n--\n*Type:* DNS\n*Action:* DNS/Domain\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Tools Information/help\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Refer to another team\n*Number of requests:* 1\n", - "operations-engineering-team", - ) + mock_slack_service.return_value.send_slack_support_stats_report.assert_called_with( + "On 2024-07-22 we received 8 Support Requests: \n\n--\n*Type:* GitHub\n*Action:* GitHub – add user to org\n*Number of requests:* 2\n--\n*Type:* GitHub\n*Action:* GitHub – remove user from org\n*Number of requests:* 1\n--\n*Type:* 1Password\n*Action:* 1Password - information/help\n*Number of requests:* 1\n--\n*Type:* API\n*Action:* API Key\n*Number of requests:* 1\n--\n*Type:* DNS\n*Action:* DNS/Domain\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Tools Information/help\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Refer to another team\n*Number of requests:* 1\n" ) From 1f17234a9757efc2122e491db12b4b7e1dbfb429 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:49:51 +0100 Subject: [PATCH 05/20] :fire: Removed old messaging technique --- services/slack_service.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 542eb9be6..afe28bc12 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -21,43 +21,6 @@ def __new__(cls, *args, **kwargs): def __init__(self, slack_token: str) -> None: self.slack_client = WebClient(slack_token) - def send_message_to_plaintext_channel_name(self, message, channel_name: str): - """ - Sends a message to a plaintext channel by name. - - Args: - message (str): The message to send. - channel_name (str): The name of the channel to send the message to. - """ - channel_id = self._lookup_channel_id(channel_name) - if channel_id is None: - logging.error("Could not find channel %s", channel_name) - else: - response = self.slack_client.chat_postMessage( - channel=channel_id, text=message) - if not response['ok']: - logging.error("Error sending message to channel %s: %s", - channel_name, response['error']) - else: - logging.info("Message sent to channel %s", channel_name) - - def _lookup_channel_id(self, channel_name, cursor=''): - channel_id = None - response = self.slack_client.conversations_list( - limit=200, cursor=cursor) - - if response['channels'] is not None: - for channel in response['channels']: - if channel['name'] == channel_name: - channel_id = channel['id'] - break - - if channel_id is None and response['response_metadata']['next_cursor'] != '': - channel_id = self._lookup_channel_id( - channel_name, cursor=response['response_metadata']['next_cursor']) - - return channel_id - def send_usage_alert_to_operations_engineering( self, period_in_days: int, usage_stats: UsageStats, usage_threshold: float, category: str ): From e765ae7f554e15e5ec978e2bf5fb0c2ba42c3e27 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:50:32 +0100 Subject: [PATCH 06/20] :sparkles: Added error handling to message posting function. --- services/slack_service.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index afe28bc12..78c90874a 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -3,7 +3,7 @@ from textwrap import dedent from urllib.parse import quote -from slack_sdk import WebClient +from slack_sdk import WebClient, SlackApiError from services.sentry_service import UsageStats @@ -195,25 +195,6 @@ def _create_block_with_message(self, message): } ] - def send_unowned_repos_slack_message(self, repositories: list): - self.slack_client.chat_postMessage( - channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": dedent(f""" - *Unowned Repositories Automation* - Repositories on the GitHub Organisation that have no team or collaborator: - {repositories} - """).strip("\n") - } - } - ] - ) - def get_all_slack_usernames(self): """Fetches all usernames and user email addresses from the Slack API. From c85efe2a2888d85c422d701483d91650986db31d Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:51:19 +0100 Subject: [PATCH 07/20] :recycle: Added configurable block type for message formatting. --- services/slack_service.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 78c90874a..b389c6bb3 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -179,15 +179,21 @@ def send_undelivered_emails_slack_message(self, email_addresses: list, organisat self._send_alert_to_operations_engineering(blocks) def _send_alert_to_operations_engineering(self, blocks: list[dict]): - self.slack_client.chat_postMessage(channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=blocks - ) + try: + self.slack_client.chat_postMessage( + channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, + mrkdown=True, + blocks=blocks + ) + except SlackApiError as e: + logging.error("Slack API error: {%s}", e.response['error']) + except Exception as e: + logging.error("Failed to send Slack alert: {%s}", str(e)) - def _create_block_with_message(self, message): + def _create_block_with_message(self, message, block_type="section"): return [ { - "type": "section", + "type": block_type, "text": { "type": "mrkdwn", "text": message From 618e4b9f5ae29b132d2c591d8db798fa0e17bbcf Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:52:05 +0100 Subject: [PATCH 08/20] :recycle: Added new messaging functions and corrected formatting of old ones. --- services/slack_service.py | 175 +++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 61 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index b389c6bb3..4b8f3cf47 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -82,99 +82,152 @@ def send_usage_alert_to_operations_engineering( ] ) + def send_slack_support_stats_report(self, support_statistics): + message = dedent(f""" + *Slack Support Stats Report* + Here an overview of our recent support statistics: + {support_statistics} + """.strip("/n")) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_dormant_user_list(self, user_list): + message = dedent(f""" + *Dormant User Report* + Here is a list of dormant GitHub users that have not been seen in Auth0 logs: + {user_list} + """.strip("/n")) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_pat_report_alert(self): + message = dedent(""" + Some expired PAT(s) have been detected. + Please review the current list here: + https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active + """).strip("/n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): + message = dedent(f""" + *New GitHub Joiner Metrics* + Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. + *Added by Operations Engineering:* + {new_members_added_by_oe} + *Added externally:* + {new_members_added_externally} + {percentage}% of the new joiners were added by operations engineering. + Please review the audit log for more details: {audit_log_url} + """).strip("/n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_new_github_owners_alert(self, new_owner, date_added, added_by, org, audit_log_url): + message = dedent(f""" + *New GitHub Owners Detected* + A new owner has been detected in the `{org}` GitHub org. + *New owner:* {new_owner} + *Date added:* {date_added} + *By who:* {added_by} + + Please review the audit log for more details: {audit_log_url} + + """).strip("/n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_low_github_licenses_alert(self, remaining_licenses): + message = dedent(f""" + *Low GitHub Licenses Remaining* + There are only {remaining_licenses} GitHub licenses remaining in the enterprise account. + Please add more licenses using the instructions outlined here: + https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html + """).strip("\n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_low_github_actions_quota_alert(self, percentage_used): + message = dedent(f""" + *Low GitHub Actions Quota* + {round(100 - percentage_used, 1)}% of the Github Actions minutes quota remains. + What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure + """).strip("\n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_low_gandi_funds_alert(self, remaining_funds, threshold): + message = dedent(f""" + *Low Gandi Funds Remaining* + :warning: We currently have £{remaining_funds} left out of £{threshold} + Please read the following Runbook for next steps: + https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates + """).strip("\n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + def send_unknown_user_alert_to_operations_engineering(self, users: list): - self.slack_client.chat_postMessage( - channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": dedent(f""" - *Dormants Users Automation* - Remove these users from the Dormants Users allow list: + message = dedent(f""" + *Dormant Users Automation* + Remove these users from the Dormant Users allow list: {users} """).strip("\n") - } - } - ] - ) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) def send_remove_users_from_github_alert_to_operations_engineering( self, number_of_users: int, organisation_name: str ): - self.slack_client.chat_postMessage( - channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": dedent(f""" - *Dormants Users Automation* + message = dedent(f""" + *Dormant Users Automation* Removed {number_of_users} users from the {organisation_name} GitHub Organisation. - See the GH Action for more info: https://github.com/ministryofjustice/operations-engineering + See the GH Action for more info: {self.OPERATION_ENGINEERING_REPOSITORY_URL} """).strip("\n") - } - } - ] - ) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) def send_unused_circleci_context_alert_to_operations_engineering(self, number_of_contexts: int): - self.slack_client.chat_postMessage( - channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": dedent(f""" + message = dedent(f""" *Unused CircleCI Contexts* A total of {number_of_contexts} unused CircleCI contexts have been detected. - Please see the GH Action for more information: https://github.com/ministryofjustice/operations-engineering + Please see the GH Action for more information: {self.OPERATION_ENGINEERING_REPOSITORY_URL} """).strip("\n") - } - } - ] - ) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) def send_undelivered_email_alert_to_operations_engineering( self, email_addresses: list, organisation_name: str ): - self.slack_client.chat_postMessage( - channel=self.OPERATIONS_ENGINEERING_ALERTS_CHANNEL_ID, - mrkdown=True, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": dedent(f""" - *Dormants Users Automation* + message = dedent(f""" + *Dormant Users Automation* Undelivered emails for {organisation_name} GitHub Organisation: {email_addresses} Remove these users manually """).strip("\n") - } - } - ] - ) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + + def send_unowned_repos_slack_message(self, repositories: list): + message = dedent(f""" + *Unowned Repositories Automation* + Repositories on the GitHub Organisation that have no team or collaborator: + {repositories} + """).strip("\n") + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) def send_remove_users_slack_message(self, number_of_users: int, organisation_name: str): - message = f"*Dormants Users Automation*\nRemoved {number_of_users} users from the {organisation_name} GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering" + message = f"*Dormant Users Automation*\nRemoved {number_of_users} users from the {organisation_name} GitHub Organisation.\nSee the GH Action for more info: {self.OPERATION_ENGINEERING_REPOSITORY_URL}" blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_unknown_users_slack_message(self, unknown_users: list): - message = f"*Dormants Users Automation*\nRemove these users from the Dormants Users allow list:\n{unknown_users}" + message = f"*Dormant Users Automation*\nRemove these users from the Dormant Users allow list:\n{unknown_users}" blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_undelivered_emails_slack_message(self, email_addresses: list, organisation_name: str): - message = f"*Dormants Users Automation*\nUndelivered emails for {organisation_name} GitHub Organisation:\n{email_addresses}\nRemove these users manually" + message = f"*Dormant Users Automation*\nUndelivered emails for {organisation_name} GitHub Organisation:\n{email_addresses}\nRemove these users manually" blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) From 186cec63e4dfc42830231e6e56f19771d04e7c13 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:52:26 +0100 Subject: [PATCH 09/20] :sparkles: Added repository URL as class variable --- services/slack_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/slack_service.py b/services/slack_service.py index 4b8f3cf47..8e68359b3 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -13,6 +13,7 @@ class SlackService: OPERATIONS_ENGINEERING_TEAM_CHANNEL_ID = "CPVD6398C" DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" SENTRY_QUOTA_MANAGEMENT_GUIDANCE = "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/services/sentryio/respond-to-sentry-usage-alert" + OPERATION_ENGINEERING_REPOSITORY_URL = "https://github.com/ministryofjustice/operations-engineering" # Added to stop TypeError on instantiation. See https://github.com/python/cpython/blob/d2340ef25721b6a72d45d4508c672c4be38c67d3/Objects/typeobject.c#L4444 def __new__(cls, *args, **kwargs): From a36a50743f9e55411e0858e085fd4272f351ecd2 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:52:54 +0100 Subject: [PATCH 10/20] :sparkles: Added test coverage for all new functionality in slack service. --- test/test_services/test_slack_service.py | 199 +++++++++++++++++++---- 1 file changed, 170 insertions(+), 29 deletions(-) diff --git a/test/test_services/test_slack_service.py b/test/test_services/test_slack_service.py index 9b6da300c..1332fbc54 100644 --- a/test/test_services/test_slack_service.py +++ b/test/test_services/test_slack_service.py @@ -41,29 +41,6 @@ def setUp(self, mock_web_client): 'channels': [self.channel], 'response_metadata': self.response_metadata} self.slack_service.slack_client = self.slack_client - def test_send_message_to_plaintext_channel_name(self): - self.slack_client.chat_postMessage.return_value = self.response - self.slack_service.send_message_to_plaintext_channel_name( - self.message, self.channel_name) - self.slack_client.chat_postMessage.assert_called_once_with( - channel=self.channel_id, text=self.message) - - def test_send_message_to_plaintext_channel_name_when_no_channel_name_exists(self): - response = {'channels': [], - 'response_metadata': self.response_metadata} - self.slack_client.conversations_list.return_value = response - self.slack_service.send_message_to_plaintext_channel_name( - self.message, self.channel_name) - self.slack_client.chat_postMessage.assert_not_called() - - def test_send_message_to_plaintext_channel_name_when_response_not_okay(self): - response = {'ok': False, "error": "some-error"} - self.slack_client.chat_postMessage.return_value = response - self.slack_service.send_message_to_plaintext_channel_name( - self.message, self.channel_name) - self.slack_client.chat_postMessage.assert_called_once_with( - channel=self.channel_id, text=self.message) - def test_lookup_channel_id(self): result = self.slack_service._lookup_channel_id(self.channel_name) self.slack_client.conversations_list.assert_called_once_with( @@ -209,7 +186,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nRemove these users from the Dormants Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' + "text": '*Dormant Users Automation*\nRemove these users from the Dormant Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' } } ] @@ -230,7 +207,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nRemoved 3 users from the some-org GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' + "text": '*Dormant Users Automation*\nRemoved 3 users from the some-org GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' } } ] @@ -253,7 +230,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\nRemove these users manually' + "text": '*Dormant Users Automation*\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\nRemove these users manually' } } ] @@ -289,6 +266,170 @@ def test_send_alert_to_operations_engineering(self): blocks=self.blocks ) + def test_send_slack_support_stats_report(self): + support_statistics = "Test support stats" + expected_message = ( + "\n*Slack Support Stats Report*\n" + "Here an overview of our recent support statistics:\n" + f"{support_statistics}\n" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_slack_support_stats_report(support_statistics) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_dormant_user_list(self): + user_list = "Test user list" + expected_message = ( + "\n*Dormant User Report*\n" + "Here is a list of dormant GitHub users that have not been seen in Auth0 logs:\n" + f"{user_list}\n" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_dormant_user_list(user_list) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_pat_report_alert(self): + expected_message = ( + "\nSome expired PAT(s) have been detected.\n" + "Please review the current list here:\n" + "https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active\n" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_pat_report_alert() + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_new_github_joiner_metrics_alert(self): + new_members_added_by_oe = "OE members" + new_members_added_externally = "External members" + percentage = 50 + total_new_members = 10 + org = "Test Org" + audit_log_url = "http://auditlog.url" + time_delta_in_days = 7 + expected_message = ( + f"\n*New GitHub Joiner Metrics*\n" + f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org.\n" + f"*Added by Operations Engineering:*\n{new_members_added_by_oe}\n" + f"*Added externally:*\n{new_members_added_externally}\n" + f"{percentage}% of the new joiners were added by operations engineering.\n" + f"Please review the audit log for more details: {audit_log_url}\n" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_new_github_joiner_metrics_alert( + new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days + ) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_new_github_owners_alert(self): + new_owner = "Test Owner" + date_added = "2024-07-22" + added_by = "Test Admin" + org = "Test Org" + audit_log_url = "http://auditlog.url" + expected_message = ( + f"\n*New GitHub Owners Detected*\n" + f"A new owner has been detected in the `{org}` GitHub org.\n" + f"*New owner:* {new_owner}\n" + f"*Date added:* {date_added}\n" + f"*By who:* {added_by}\n\n" + f"Please review the audit log for more details: {audit_log_url}\n\n" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_new_github_owners_alert(new_owner, date_added, added_by, org, audit_log_url) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_low_github_licenses_alert(self): + remaining_licenses = 5 + expected_message = ( + f"*Low GitHub Licenses Remaining*\n" + f"There are only {remaining_licenses} GitHub licenses remaining in the enterprise account.\n" + "Please add more licenses using the instructions outlined here:\n" + "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_low_github_licenses_alert(remaining_licenses) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_low_github_actions_quota_alert(self): + percentage_used = 90 + expected_message = ( + f"*Low GitHub Actions Quota*\n" + f"{100 - percentage_used}% of the Github Actions minutes quota remains.\n" + "What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_low_github_actions_quota_alert(percentage_used) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_low_gandi_funds_alert(self): + remaining_funds = 100 + threshold = 500 + expected_message = ( + f"*Low Gandi Funds Remaining*\n" + f":warning: We currently have £{remaining_funds} left out of £{threshold}\n" + "Please read the following Runbook for next steps:\n" + "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_low_gandi_funds_alert(remaining_funds, threshold) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_unknown_user_alert_to_operations_engineering(self): + users = ["user1", "user2"] + expected_message = ( + "*Dormant Users Automation*\n" + "Remove these users from the Dormant Users allow list:\n" + f"{users}" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_unknown_user_alert_to_operations_engineering(users) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_remove_users_from_github_alert(self): + number_of_users = 3 + organisation_name = "Test Org" + expected_message = ( + "*Dormant Users Automation*\n" + f"Removed {number_of_users} users from the {organisation_name} GitHub Organisation.\n" + "See the GH Action for more info: https://github.com/ministryofjustice/operations-engineering" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_remove_users_from_github_alert_to_operations_engineering(number_of_users, organisation_name) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + + def test_send_unused_circleci_context_alert(self): + number_of_contexts = 4 + expected_message = ( + "*Unused CircleCI Contexts*\n" + f"A total of {number_of_contexts} unused CircleCI contexts have been detected.\n" + "Please see the GH Action for more information: https://github.com/ministryofjustice/operations-engineering" + ) + blocks = self.slack_service._create_block_with_message(expected_message) + self.slack_service.send_unused_circleci_context_alert_to_operations_engineering(number_of_contexts) + self.mock_slack_client.return_value.chat_postMessage.assert_called_once_with( + channel="C033QBE511V", mrkdown=True, blocks=blocks + ) + def test_send_unknown_users_slack_message(self): self.slack_service.send_unknown_users_slack_message( ["some-user1", "some-user2", "some-user3"]) @@ -300,7 +441,7 @@ def test_send_unknown_users_slack_message(self): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nRemove these users from the Dormants Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' + "text": '*Dormant Users Automation*\nRemove these users from the Dormant Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' } } ] @@ -317,7 +458,7 @@ def test_send_remove_users_slack_message(self): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nRemoved 3 users from the some-org GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' + "text": '*Dormant Users Automation*\nRemoved 3 users from the some-org GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' } } ] @@ -338,7 +479,7 @@ def test_send_undelivered_emails_slack_message(self): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormants Users Automation*\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\nRemove these users manually' + "text": '*Dormant Users Automation*\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\nRemove these users manually' } } ] From 64ad117dba52907ae3cdfb3b48fdfa9d887867c2 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:54:25 +0100 Subject: [PATCH 11/20] :wrench: Corrected import for SlackApiError. --- services/slack_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/slack_service.py b/services/slack_service.py index 8e68359b3..a2b6ad78f 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -3,7 +3,8 @@ from textwrap import dedent from urllib.parse import quote -from slack_sdk import WebClient, SlackApiError +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError from services.sentry_service import UsageStats From db5c7ebfb9ade94223846607ecfacb70230916a1 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 15:59:12 +0100 Subject: [PATCH 12/20] :fire: removed channel lookup tests. --- test/test_services/test_slack_service.py | 33 ------------------------ 1 file changed, 33 deletions(-) diff --git a/test/test_services/test_slack_service.py b/test/test_services/test_slack_service.py index 1332fbc54..b26692580 100644 --- a/test/test_services/test_slack_service.py +++ b/test/test_services/test_slack_service.py @@ -41,39 +41,6 @@ def setUp(self, mock_web_client): 'channels': [self.channel], 'response_metadata': self.response_metadata} self.slack_service.slack_client = self.slack_client - def test_lookup_channel_id(self): - result = self.slack_service._lookup_channel_id(self.channel_name) - self.slack_client.conversations_list.assert_called_once_with( - limit=200, cursor='') - self.assertEqual(result, self.channel_id) - - def test_lookup_channel_id_when_no_matching_channels(self): - response = {'channels': [ - {'name': "other-channel", 'id': self.channel_id}], 'response_metadata': self.response_metadata} - self.slack_client.conversations_list.return_value = response - result = self.slack_service._lookup_channel_id(self.channel_name) - self.slack_client.conversations_list.assert_called_once_with( - limit=200, cursor='') - self.assertIsNone(result) - - def test_lookup_channel_id_when_channels_empty(self): - response = {'channels': [], - 'response_metadata': self.response_metadata} - self.slack_client.conversations_list.return_value = response - result = self.slack_service._lookup_channel_id(self.channel_name) - self.slack_client.conversations_list.assert_called_once_with( - limit=200, cursor='') - self.assertIsNone(result) - - def test_lookup_channel_id_when_no_channels(self): - response = {'channels': None, - 'response_metadata': self.response_metadata} - self.slack_client.conversations_list.return_value = response - result = self.slack_service._lookup_channel_id(self.channel_name) - self.slack_client.conversations_list.assert_called_once_with( - limit=200, cursor='') - self.assertIsNone(result) - @patch("slack_sdk.WebClient.__new__") class TestSlackServiceSendErrorUsageAlertToOperationsEngineering(unittest.TestCase): From a81f52571bd7a9e284092fca8686d903404b781a Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 16:13:35 +0100 Subject: [PATCH 13/20] :fire: Removed support stat reporting --- bin/support_stats_reporting.py | 128 ------------------ test/test_bin/test_support_stats_reporting.py | 106 --------------- 2 files changed, 234 deletions(-) delete mode 100644 bin/support_stats_reporting.py delete mode 100644 test/test_bin/test_support_stats_reporting.py diff --git a/bin/support_stats_reporting.py b/bin/support_stats_reporting.py deleted file mode 100644 index 35017f675..000000000 --- a/bin/support_stats_reporting.py +++ /dev/null @@ -1,128 +0,0 @@ -import os -from dataclasses import dataclass -from datetime import date, timedelta -from os.path import exists - -import pandas as pd -from collections import defaultdict - -from services.slack_service import SlackService - - -@dataclass -class SupportRequest: - request_type: str - request_action: str - request_date: str - - def __init__(self, request_type: str, request_action: str, request_date: str): - self.request_type = request_type - self.request_action = request_action - self.request_date = request_date - - def __hash__(self): - return hash(self.request_action) - - -def create_dataframe_from_csv(filename: str): - - dataframe = pd.read_csv(filename) - pd.options.display.max_rows = 9999 - - dataframe.set_index("Type") - - return dataframe - - -def get_previous_working_day(date_today): - diff = 1 - if date_today.weekday() == 0: - diff = 3 - else: - diff = 1 - - last_working_day = date_today - timedelta(days=diff) - str_last_working_day = str(last_working_day) - - return str_last_working_day - - -def get_environment_variables() -> tuple: - slack_token = os.getenv("ADMIN_SLACK_TOKEN") - if not slack_token: - raise ValueError("The env variable ADMIN_SLACK_TOKEN is empty or missing") - - return slack_token - - -def get_dict_of_requests_and_volume( - requests: list[SupportRequest], -) -> dict[SupportRequest, int]: - dict_of_requests = defaultdict(int) - - for request in requests: - dict_of_requests[request.request_action] += 1 - - return dict_of_requests - - -def craft_support_statistics( - yesterdays_support_requests: list[SupportRequest], date_today=date.today() -): - dict_of_requests_and_volume = get_dict_of_requests_and_volume( - yesterdays_support_requests - ) - - previous_support_day = get_previous_working_day(date_today) - - msg = f"On {previous_support_day} we received {len(yesterdays_support_requests)} Support Requests: \n\n" - yesterdays_support_requests = list(dict.fromkeys(yesterdays_support_requests)) - for request in yesterdays_support_requests: - number_of_requests = dict_of_requests_and_volume[request.request_action] - msg += f"--\n*Type:* {request.request_type}\n*Action:* {request.request_action}\n*Number of requests:* {number_of_requests}\n" - - return msg - - -def get_support_requests_from_csv(filepath: str) -> list[SupportRequest]: - if exists(filepath): - return get_list_of_support_requests(create_dataframe_from_csv(filepath)) - raise FileExistsError(f"File path {filepath} does not exist.") - - -def get_list_of_support_requests(data) -> list[SupportRequest]: - list_of_support_requests = [] - for _, row in data.iterrows(): - list_of_support_requests.append( - SupportRequest(row["Type"], row["Action"], row["Date"]) - ) - - return list_of_support_requests - - -def get_yesterdays_support_requests( - all_support_requests: list[SupportRequest], todays_date -) -> list[SupportRequest]: - yesterday = get_previous_working_day(todays_date) # yesterdays_date() - list_of_yesterdays_support_requests = [] - for request in all_support_requests: - if request.request_date == yesterday: - list_of_yesterdays_support_requests.append(request) - - return list_of_yesterdays_support_requests - - -def main(todays_date=date.today(), file_path="data/support_stats/support_stats.csv"): - slack_token = get_environment_variables() - slack_service = SlackService(str(slack_token)) - all_support_requests = get_support_requests_from_csv(file_path) - yesterdays_requests = get_yesterdays_support_requests( - all_support_requests, todays_date - ) - support_statistics = craft_support_statistics(yesterdays_requests, todays_date) - - slack_service.send_slack_support_stats_report(support_statistics) - - -if __name__ == "__main__": - main() diff --git a/test/test_bin/test_support_stats_reporting.py b/test/test_bin/test_support_stats_reporting.py deleted file mode 100644 index 5e8939b45..000000000 --- a/test/test_bin/test_support_stats_reporting.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from datetime import date -import unittest -from unittest.mock import patch, MagicMock -from bin.support_stats_reporting import ( - SupportRequest, - create_dataframe_from_csv, - get_yesterdays_support_requests, - get_previous_working_day, - get_environment_variables, - craft_support_statistics, - main, -) - - -class TestSupportRequestHashReturnsRequestActionHash(unittest.TestCase): - - def test_support_request_hash(self): - support_request1 = SupportRequest("Type A", "Action A", "2023-05-01") - support_request2 = SupportRequest("Type B", "Action A", "2023-05-02") - self.assertEqual(hash(support_request1), hash(support_request2)) - - -class TestCreateDataframeFromCsv(unittest.TestCase): - - @patch("pandas.read_csv") - def test_create_dataframe_from_csv(self, mock_read_csv): - mock_dataframe = MagicMock() - mock_read_csv.return_value = mock_dataframe - filepath = "test/fixtures/test_data.csv" - result = create_dataframe_from_csv(filepath) - mock_read_csv.assert_called_with(filepath) - self.assertEqual(result, mock_dataframe) - - -class TestGetPreviousWorkingDay(unittest.TestCase): - - def test_monday_returns_friday(self): - previous_day = get_previous_working_day(date_today=date(2024, 7, 8)) - self.assertEqual(previous_day, str(date(2024, 7, 5))) - - def test_tuesday_returns_monday(self): - previous_day = get_previous_working_day(date_today=date(2024, 7, 9)) - self.assertEqual(previous_day, str(date(2024, 7, 8))) - - -class TestGetYesterdaysSupportRequests(unittest.TestCase): - - def test_get_yesterdays_support_requests(self): - all_support_requests = [ - SupportRequest("Type A", "Action A", "2024-07-23"), - SupportRequest("Type B", "Action B", "2024-07-24"), - SupportRequest("Type C", "Action C", "2024-07-23"), - ] - expected_result = [ - SupportRequest("Type A", "Action A", "2024-07-23"), - SupportRequest("Type C", "Action C", "2024-07-23"), - ] - result = get_yesterdays_support_requests( - all_support_requests, todays_date=date(2024, 7, 24) - ) - self.assertEqual(result, expected_result) - - -class TestGetEnvironmentVariables(unittest.TestCase): - @patch.dict(os.environ, {"ADMIN_SLACK_TOKEN": "test_token"}) - def test_returns_variable(self): - slack_token = get_environment_variables() - self.assertEqual(slack_token, "test_token") - - def test_raises_error_when_no_slack_token(self): - self.assertRaises(ValueError, get_environment_variables) - - -class TestCraftMessageToSlack(unittest.TestCase): - def test_slack_message(self): - date_today = date(2024, 7, 16) - yesterdays_support_requests = [ - SupportRequest( - request_type="GitHub", - request_action="Add user to Org", - request_date="2024-07-15", - ) - ] - expected_message = ( - "On 2024-07-15 we received 1 Support Requests: \n\n" - "--\n*Type:* GitHub\n*Action:* Add user to Org\n*Number of requests:* 1\n" - ) - - result_message = craft_support_statistics(yesterdays_support_requests, date_today) - self.assertEqual(result_message, expected_message) - - -class TestMain(unittest.TestCase): - - @patch("services.slack_service.SlackService.__new__") - @patch.dict(os.environ, {"ADMIN_SLACK_TOKEN": "test_token"}) - def test_slack_message_sent_to_slack(self, mock_slack_service: MagicMock): - - todays_date = date(2024, 7, 23) - file_path = "test/fixtures/test_data.csv" - - main(todays_date, file_path) - - mock_slack_service.return_value.send_slack_support_stats_report.assert_called_with( - "On 2024-07-22 we received 8 Support Requests: \n\n--\n*Type:* GitHub\n*Action:* GitHub – add user to org\n*Number of requests:* 2\n--\n*Type:* GitHub\n*Action:* GitHub – remove user from org\n*Number of requests:* 1\n--\n*Type:* 1Password\n*Action:* 1Password - information/help\n*Number of requests:* 1\n--\n*Type:* API\n*Action:* API Key\n*Number of requests:* 1\n--\n*Type:* DNS\n*Action:* DNS/Domain\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Tools Information/help\n*Number of requests:* 1\n--\n*Type:* Other\n*Action:* Refer to another team\n*Number of requests:* 1\n" ) From db2d896e4ac120efb98cea47c1d5ddaeb5c2fb21 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Tue, 22 Oct 2024 16:25:28 +0100 Subject: [PATCH 14/20] :recycle: Formatting and unused import. --- services/slack_service.py | 2 -- test/test_bin/test_generate_pat_token_report.py | 1 - 2 files changed, 3 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index a2b6ad78f..bea007008 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -2,10 +2,8 @@ import time from textwrap import dedent from urllib.parse import quote - from slack_sdk import WebClient from slack_sdk.errors import SlackApiError - from services.sentry_service import UsageStats diff --git a/test/test_bin/test_generate_pat_token_report.py b/test/test_bin/test_generate_pat_token_report.py index 5ce327b23..a13718ecf 100644 --- a/test/test_bin/test_generate_pat_token_report.py +++ b/test/test_bin/test_generate_pat_token_report.py @@ -1,6 +1,5 @@ import unittest from unittest.mock import patch -from config.constants import SLACK_CHANNEL from bin.generate_pat_token_report import generate_pat_token_report, count_expired_tokens from services.github_service import GithubService From 387d7e09640329192825ee7c9e5f2f58166c525a Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Wed, 23 Oct 2024 12:12:32 +0100 Subject: [PATCH 15/20] :wrench: Fix formatting for dedent string. --- services/slack_service.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index bea007008..7a6ce91b4 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -110,16 +110,17 @@ def send_pat_report_alert(self): self._send_alert_to_operations_engineering(blocks) def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): - message = dedent(f""" - *New GitHub Joiner Metrics* - Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. - *Added by Operations Engineering:* - {new_members_added_by_oe} - *Added externally:* - {new_members_added_externally} - {percentage}% of the new joiners were added by operations engineering. - Please review the audit log for more details: {audit_log_url} - """).strip("/n") + message = dedent( + f""" + *New GitHub Joiner Metrics* + Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. + *Added by Operations Engineering:* + {new_members_added_by_oe} + *Added externally:* + {new_members_added_externally} + {percentage}% of the new joiners were added by operations engineering. + Please review the audit log for more details: {audit_log_url} + """).strip("/n") blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) From f61c5a4d1b67e0aeb6d724f32945198882eb1558 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Wed, 23 Oct 2024 12:57:48 +0100 Subject: [PATCH 16/20] :wrench: Corrected formatting for slack message --- services/slack_service.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 7a6ce91b4..9567b5a9a 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -112,14 +112,14 @@ def send_pat_report_alert(self): def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): message = dedent( f""" - *New GitHub Joiner Metrics* - Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. - *Added by Operations Engineering:* - {new_members_added_by_oe} - *Added externally:* - {new_members_added_externally} - {percentage}% of the new joiners were added by operations engineering. - Please review the audit log for more details: {audit_log_url} + *New GitHub Joiner Metrics* + Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. + *Added by Operations Engineering:* + {new_members_added_by_oe} + *Added externally:* + {new_members_added_externally} + {percentage}% of the new joiners were added by operations engineering. + Please review the audit log for more details: {audit_log_url} """).strip("/n") blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) From 4bf5314c3853b8b9395a537e96205e2864b2711c Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Mon, 28 Oct 2024 13:46:41 +0000 Subject: [PATCH 17/20] :wrench: Moved to old formatting method for multiline strings. --- services/slack_service.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 9567b5a9a..11692ae6c 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -110,17 +110,16 @@ def send_pat_report_alert(self): self._send_alert_to_operations_engineering(blocks) def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): - message = dedent( - f""" - *New GitHub Joiner Metrics* - Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. - *Added by Operations Engineering:* - {new_members_added_by_oe} - *Added externally:* - {new_members_added_externally} - {percentage}% of the new joiners were added by operations engineering. - Please review the audit log for more details: {audit_log_url} - """).strip("/n") + message = (f""" + *New GitHub Joiner Metrics*\n + Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org.\n\n + *Added by Operations Engineering:*\n + {new_members_added_by_oe}\n\n + *Added externally:*\n + {new_members_added_externally}\n\n + {percentage}% of the new joiners were added by operations engineering.\n\n + Please review the audit log for more details: {audit_log_url} + """) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) From 1f57c22265a59d5c4e3614b359d6985bcdd4698d Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Mon, 28 Oct 2024 13:54:36 +0000 Subject: [PATCH 18/20] :wrench: Using exact message from last know working format --- services/slack_service.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 11692ae6c..70198f60f 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -110,16 +110,15 @@ def send_pat_report_alert(self): self._send_alert_to_operations_engineering(blocks) def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): - message = (f""" - *New GitHub Joiner Metrics*\n - Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org.\n\n - *Added by Operations Engineering:*\n - {new_members_added_by_oe}\n\n - *Added externally:*\n - {new_members_added_externally}\n\n - {percentage}% of the new joiners were added by operations engineering.\n\n - Please review the audit log for more details: {audit_log_url} - """) + message = ( + f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. \n\n" + f"*Added by Operations Engineering:*\n" + f"{new_members_added_by_oe}\n\n" + f"*Added externally:*\n" + f"{new_members_added_externally}\n\n" + f"{percentage}% of the new joiners were added by operations engineering.\n\n" + f"Please review the audit log for more details: {audit_log_url}\n\n" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) From 2e4f24c6e0f0d5d12a427a264abce0963d378079 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Mon, 28 Oct 2024 14:27:14 +0000 Subject: [PATCH 19/20] :wrench: Corrected formatting for all slack messages and tests. --- services/slack_service.py | 138 +++++++++++------------ test/test_services/test_slack_service.py | 77 +++++++------ 2 files changed, 109 insertions(+), 106 deletions(-) diff --git a/services/slack_service.py b/services/slack_service.py index 70198f60f..25bc36c1e 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -83,135 +83,135 @@ def send_usage_alert_to_operations_engineering( ) def send_slack_support_stats_report(self, support_statistics): - message = dedent(f""" - *Slack Support Stats Report* - Here an overview of our recent support statistics: - {support_statistics} - """.strip("/n")) + message = ( + f"*Slack Support Stats Report*\n\n" + f"Here an overview of our recent support statistics:\n" + f"{support_statistics}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_dormant_user_list(self, user_list): - message = dedent(f""" - *Dormant User Report* - Here is a list of dormant GitHub users that have not been seen in Auth0 logs: - {user_list} - """.strip("/n")) + message = ( + f"*Dormant User Report*\n\n" + f"Here is a list of dormant GitHub users that have not been seen in Auth0 logs:\n" + f"{user_list}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_pat_report_alert(self): - message = dedent(""" - Some expired PAT(s) have been detected. - Please review the current list here: - https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active - """).strip("/n") + message = ( + "*Expired PAT Report*\n\n" + "Some expired PAT(s) have been detected.\n\n" + "Please review the current list here:\n" + "https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_new_github_joiner_metrics_alert(self, new_members_added_by_oe, new_members_added_externally, percentage, total_new_members, org, audit_log_url, time_delta_in_days): message = ( + f"*GitHub Joiner Metrics*\n\n" f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. \n\n" f"*Added by Operations Engineering:*\n" f"{new_members_added_by_oe}\n\n" f"*Added externally:*\n" f"{new_members_added_externally}\n\n" f"{percentage}% of the new joiners were added by operations engineering.\n\n" - f"Please review the audit log for more details: {audit_log_url}\n\n" + f"Please review the audit log for more details: {audit_log_url}" ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_new_github_owners_alert(self, new_owner, date_added, added_by, org, audit_log_url): - message = dedent(f""" - *New GitHub Owners Detected* - A new owner has been detected in the `{org}` GitHub org. - *New owner:* {new_owner} - *Date added:* {date_added} - *By who:* {added_by} - - Please review the audit log for more details: {audit_log_url} - - """).strip("/n") + message = ( + f"*New GitHub Owners Detected*\n\n" + f"A new owner has been detected in the `{org}` GitHub org.\n\n" + f"*New owner:* {new_owner}\n" + f"*Date added:* {date_added}\n" + f"*By who:* {added_by}\n\n" + f"Please review the audit log for more details: {audit_log_url}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_low_github_licenses_alert(self, remaining_licenses): - message = dedent(f""" - *Low GitHub Licenses Remaining* - There are only {remaining_licenses} GitHub licenses remaining in the enterprise account. - Please add more licenses using the instructions outlined here: - https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html - """).strip("\n") + message = ( + f"*Low GitHub Licenses Remaining*\n\n" + f"There are only {remaining_licenses} GitHub licenses remaining in the enterprise account.\n\n" + f"Please add more licenses using the instructions outlined here:" + f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_low_github_actions_quota_alert(self, percentage_used): - message = dedent(f""" - *Low GitHub Actions Quota* - {round(100 - percentage_used, 1)}% of the Github Actions minutes quota remains. - What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure - """).strip("\n") + message = ( + f"*Low GitHub Actions Quota*\n\n" + f"{round(100 - percentage_used, 1)}% of the Github Actions minutes quota remains.\n" + f"What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_low_gandi_funds_alert(self, remaining_funds, threshold): - message = dedent(f""" - *Low Gandi Funds Remaining* - :warning: We currently have £{remaining_funds} left out of £{threshold} - Please read the following Runbook for next steps: - https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates - """).strip("\n") + message = ( + f"*Low Gandi Funds Remaining*\n\n" + f":warning: We currently have £{remaining_funds} left out of £{threshold}\n\n" + f"Please read the following Runbook for next steps:" + f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_unknown_user_alert_to_operations_engineering(self, users: list): - message = dedent(f""" - *Dormant Users Automation* - Remove these users from the Dormant Users allow list: - {users} - """).strip("\n") + message = ( + f"*Dormant Users Automation*\n\n" + f"Remove these users from the Dormant Users allow list:\n" + f"{users}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_remove_users_from_github_alert_to_operations_engineering( self, number_of_users: int, organisation_name: str ): - message = dedent(f""" - *Dormant Users Automation* - Removed {number_of_users} users from the {organisation_name} GitHub Organisation. - See the GH Action for more info: {self.OPERATION_ENGINEERING_REPOSITORY_URL} - """).strip("\n") + message = ( + f"*Dormant Users Automation*\n\n" + f"Removed {number_of_users} users from the {organisation_name} GitHub Organisation.\n\n" + f"See the GH Action for more info: {self.OPERATION_ENGINEERING_REPOSITORY_URL}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_unused_circleci_context_alert_to_operations_engineering(self, number_of_contexts: int): - message = dedent(f""" - *Unused CircleCI Contexts* - A total of {number_of_contexts} unused CircleCI contexts have been detected. - Please see the GH Action for more information: {self.OPERATION_ENGINEERING_REPOSITORY_URL} - """).strip("\n") + message = ( + f"*Unused CircleCI Contexts*\n\n" + f"A total of {number_of_contexts} unused CircleCI contexts have been detected.\n\n" + f"Please see the GH Action for more information: {self.OPERATION_ENGINEERING_REPOSITORY_URL}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_undelivered_email_alert_to_operations_engineering( self, email_addresses: list, organisation_name: str ): - message = dedent(f""" - *Dormant Users Automation* - Undelivered emails for {organisation_name} GitHub Organisation: - {email_addresses} - Remove these users manually - """).strip("\n") + message = ( + f"*Dormant Users Automation*\n\n" + f"Undelivered emails for {organisation_name} GitHub Organisation:\n" + f"{email_addresses}\n\n" + f"Remove these users manually." + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) def send_unowned_repos_slack_message(self, repositories: list): - message = dedent(f""" - *Unowned Repositories Automation* - Repositories on the GitHub Organisation that have no team or collaborator: - {repositories} - """).strip("\n") + message = ( + f"*Unowned Repositories Automation*\n\n" + f"Repositories on the GitHub Organisation that have no team or collaborator:\n" + f"{repositories}" + ) blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) diff --git a/test/test_services/test_slack_service.py b/test/test_services/test_slack_service.py index b26692580..9ee00b311 100644 --- a/test/test_services/test_slack_service.py +++ b/test/test_services/test_slack_service.py @@ -153,7 +153,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormant Users Automation*\nRemove these users from the Dormant Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' + "text": '*Dormant Users Automation*\n\nRemove these users from the Dormant Users allow list:\n[\'some-user1\', \'some-user2\', \'some-user3\']' } } ] @@ -174,7 +174,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormant Users Automation*\nRemoved 3 users from the some-org GitHub Organisation.\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' + "text": '*Dormant Users Automation*\n\nRemoved 3 users from the some-org GitHub Organisation.\n\nSee the GH Action for more info: https://github.com/ministryofjustice/operations-engineering' } } ] @@ -197,7 +197,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Dormant Users Automation*\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\nRemove these users manually' + "text": '*Dormant Users Automation*\n\nUndelivered emails for some-org GitHub Organisation:\n[\'some-user1@domain.com\', \'some-user2@domain.com\', \'some-user3@domain.com\']\n\nRemove these users manually.' } } ] @@ -236,9 +236,9 @@ def test_send_alert_to_operations_engineering(self): def test_send_slack_support_stats_report(self): support_statistics = "Test support stats" expected_message = ( - "\n*Slack Support Stats Report*\n" + "*Slack Support Stats Report*\n\n" "Here an overview of our recent support statistics:\n" - f"{support_statistics}\n" + f"{support_statistics}" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_slack_support_stats_report(support_statistics) @@ -249,9 +249,9 @@ def test_send_slack_support_stats_report(self): def test_send_dormant_user_list(self): user_list = "Test user list" expected_message = ( - "\n*Dormant User Report*\n" + "*Dormant User Report*\n\n" "Here is a list of dormant GitHub users that have not been seen in Auth0 logs:\n" - f"{user_list}\n" + f"{user_list}" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_dormant_user_list(user_list) @@ -261,9 +261,10 @@ def test_send_dormant_user_list(self): def test_send_pat_report_alert(self): expected_message = ( - "\nSome expired PAT(s) have been detected.\n" + "*Expired PAT Report*\n\n" + "Some expired PAT(s) have been detected.\n\n" "Please review the current list here:\n" - "https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active\n" + "https://github.com/organizations/ministryofjustice/settings/personal-access-tokens/active" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_pat_report_alert() @@ -280,12 +281,14 @@ def test_send_new_github_joiner_metrics_alert(self): audit_log_url = "http://auditlog.url" time_delta_in_days = 7 expected_message = ( - f"\n*New GitHub Joiner Metrics*\n" - f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org.\n" - f"*Added by Operations Engineering:*\n{new_members_added_by_oe}\n" - f"*Added externally:*\n{new_members_added_externally}\n" - f"{percentage}% of the new joiners were added by operations engineering.\n" - f"Please review the audit log for more details: {audit_log_url}\n" + f"*GitHub Joiner Metrics*\n\n" + f"Here are the {total_new_members} new joiners added in the last {time_delta_in_days} days within the '{org}' GitHub org. \n\n" + f"*Added by Operations Engineering:*\n" + f"{new_members_added_by_oe}\n\n" + f"*Added externally:*\n" + f"{new_members_added_externally}\n\n" + f"{percentage}% of the new joiners were added by operations engineering.\n\n" + f"Please review the audit log for more details: {audit_log_url}" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_new_github_joiner_metrics_alert( @@ -302,12 +305,12 @@ def test_send_new_github_owners_alert(self): org = "Test Org" audit_log_url = "http://auditlog.url" expected_message = ( - f"\n*New GitHub Owners Detected*\n" - f"A new owner has been detected in the `{org}` GitHub org.\n" + f"*New GitHub Owners Detected*\n\n" + f"A new owner has been detected in the `{org}` GitHub org.\n\n" f"*New owner:* {new_owner}\n" f"*Date added:* {date_added}\n" f"*By who:* {added_by}\n\n" - f"Please review the audit log for more details: {audit_log_url}\n\n" + f"Please review the audit log for more details: {audit_log_url}" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_new_github_owners_alert(new_owner, date_added, added_by, org, audit_log_url) @@ -318,10 +321,10 @@ def test_send_new_github_owners_alert(self): def test_send_low_github_licenses_alert(self): remaining_licenses = 5 expected_message = ( - f"*Low GitHub Licenses Remaining*\n" - f"There are only {remaining_licenses} GitHub licenses remaining in the enterprise account.\n" - "Please add more licenses using the instructions outlined here:\n" - "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html" + f"*Low GitHub Licenses Remaining*\n\n" + f"There are only {remaining_licenses} GitHub licenses remaining in the enterprise account.\n\n" + f"Please add more licenses using the instructions outlined here:" + f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-seats-procedure.html" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_low_github_licenses_alert(remaining_licenses) @@ -332,9 +335,9 @@ def test_send_low_github_licenses_alert(self): def test_send_low_github_actions_quota_alert(self): percentage_used = 90 expected_message = ( - f"*Low GitHub Actions Quota*\n" - f"{100 - percentage_used}% of the Github Actions minutes quota remains.\n" - "What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" + f"*Low GitHub Actions Quota*\n\n" + f"{round(100 - percentage_used, 1)}% of the Github Actions minutes quota remains.\n" + f"What to do next: https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/low-github-actions-minutes-procedure.html#low-github-actions-minutes-procedure" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_low_github_actions_quota_alert(percentage_used) @@ -346,10 +349,10 @@ def test_send_low_gandi_funds_alert(self): remaining_funds = 100 threshold = 500 expected_message = ( - f"*Low Gandi Funds Remaining*\n" - f":warning: We currently have £{remaining_funds} left out of £{threshold}\n" - "Please read the following Runbook for next steps:\n" - "https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates" + f"*Low Gandi Funds Remaining*\n\n" + f":warning: We currently have £{remaining_funds} left out of £{threshold}\n\n" + f"Please read the following Runbook for next steps:" + f"https://runbooks.operations-engineering.service.justice.gov.uk/documentation/certificates/manual-ssl-certificate-processes.html#regenerating-certificates" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_low_gandi_funds_alert(remaining_funds, threshold) @@ -360,7 +363,7 @@ def test_send_low_gandi_funds_alert(self): def test_send_unknown_user_alert_to_operations_engineering(self): users = ["user1", "user2"] expected_message = ( - "*Dormant Users Automation*\n" + "*Dormant Users Automation*\n\n" "Remove these users from the Dormant Users allow list:\n" f"{users}" ) @@ -374,9 +377,9 @@ def test_send_remove_users_from_github_alert(self): number_of_users = 3 organisation_name = "Test Org" expected_message = ( - "*Dormant Users Automation*\n" - f"Removed {number_of_users} users from the {organisation_name} GitHub Organisation.\n" - "See the GH Action for more info: https://github.com/ministryofjustice/operations-engineering" + "*Dormant Users Automation*\n\n" + f"Removed {number_of_users} users from the {organisation_name} GitHub Organisation.\n\n" + f"See the GH Action for more info: https://github.com/ministryofjustice/operations-engineering" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_remove_users_from_github_alert_to_operations_engineering(number_of_users, organisation_name) @@ -387,9 +390,9 @@ def test_send_remove_users_from_github_alert(self): def test_send_unused_circleci_context_alert(self): number_of_contexts = 4 expected_message = ( - "*Unused CircleCI Contexts*\n" - f"A total of {number_of_contexts} unused CircleCI contexts have been detected.\n" - "Please see the GH Action for more information: https://github.com/ministryofjustice/operations-engineering" + "*Unused CircleCI Contexts*\n\n" + f"A total of {number_of_contexts} unused CircleCI contexts have been detected.\n\n" + f"Please see the GH Action for more information: https://github.com/ministryofjustice/operations-engineering" ) blocks = self.slack_service._create_block_with_message(expected_message) self.slack_service.send_unused_circleci_context_alert_to_operations_engineering(number_of_contexts) @@ -468,7 +471,7 @@ def test_downstream_services_called(self, mock_slack_client: MagicMock): "type": "section", "text": { "type": "mrkdwn", - "text": '*Unowned Repositories Automation*\nRepositories on the GitHub Organisation that have no team or collaborator:\n[\'some-repo1\', \'some-repo2\', \'some-repo3\']' + "text": '*Unowned Repositories Automation*\n\nRepositories on the GitHub Organisation that have no team or collaborator:\n[\'some-repo1\', \'some-repo2\', \'some-repo3\']' } } ] From 372fdd0276afc6c9fcdb8565aba6ae3024cc91e8 Mon Sep 17 00:00:00 2001 From: PepperMoJ Date: Mon, 28 Oct 2024 14:46:20 +0000 Subject: [PATCH 20/20] :recycle: Refactored POC alert script and tests to fit new Slack service sturcture --- bin/alarm_for_old_poc_repositories.py | 13 ++------ services/slack_service.py | 9 ++++++ .../test_alarm_for_old_poc_repositories.py | 32 ++++++------------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/bin/alarm_for_old_poc_repositories.py b/bin/alarm_for_old_poc_repositories.py index 45b078141..05c74169c 100644 --- a/bin/alarm_for_old_poc_repositories.py +++ b/bin/alarm_for_old_poc_repositories.py @@ -3,16 +3,7 @@ from services.github_service import GithubService from services.slack_service import SlackService -from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE, SLACK_CHANNEL - -def construct_message(repositories): - intro = "The following POC GitHub Repositories persist:\n\n" - - core_content = "\n".join([f"https://github.com/ministryofjustice/{repo} - {age} days old" for repo, age in repositories.items()]) - - action = "\n\nConsider if they are still required. If not, please archive them by removing them from the Terraform configuration: https://github.com/ministryofjustice/operations-engineering/tree/main/terraform/github/repositories/ministryofjustice" - - return intro + core_content + action +from config.constants import ENTERPRISE, MINISTRY_OF_JUSTICE def alert_for_old_poc_repositories(): @@ -33,7 +24,7 @@ def alert_for_old_poc_repositories(): old_poc_repositories = github_service.get_old_poc_repositories() if old_poc_repositories: - slack_service.send_message_to_plaintext_channel_name(construct_message(old_poc_repositories), SLACK_CHANNEL) + slack_service.send_alert_for_poc_repositories(old_poc_repositories) if __name__ == "__main__": diff --git a/services/slack_service.py b/services/slack_service.py index 25bc36c1e..93c9e3fe1 100644 --- a/services/slack_service.py +++ b/services/slack_service.py @@ -215,6 +215,15 @@ def send_unowned_repos_slack_message(self, repositories: list): blocks = self._create_block_with_message(message) self._send_alert_to_operations_engineering(blocks) + def send_alert_for_poc_repositories(self, repositories): + message = ( + "The following POC GitHub Repositories persist:\n\n" + + "\n".join([f"https://github.com/ministryofjustice/{repo} - {age} days old" for repo, age in repositories.items()]) + + "\n\nConsider if they are still required. If not, please archive them by removing them from the Terraform configuration: https://github.com/ministryofjustice/operations-engineering/tree/main/terraform/github/repositories/ministryofjustice" + ) + blocks = self._create_block_with_message(message) + self._send_alert_to_operations_engineering(blocks) + def send_remove_users_slack_message(self, number_of_users: int, organisation_name: str): message = f"*Dormant Users Automation*\nRemoved {number_of_users} users from the {organisation_name} GitHub Organisation.\nSee the GH Action for more info: {self.OPERATION_ENGINEERING_REPOSITORY_URL}" blocks = self._create_block_with_message(message) diff --git a/test/test_bin/test_alarm_for_old_poc_repositories.py b/test/test_bin/test_alarm_for_old_poc_repositories.py index f94a19b43..179c23890 100644 --- a/test/test_bin/test_alarm_for_old_poc_repositories.py +++ b/test/test_bin/test_alarm_for_old_poc_repositories.py @@ -1,68 +1,56 @@ import unittest from unittest.mock import patch, MagicMock - -from bin.alarm_for_old_poc_repositories import ( - construct_message, - alert_for_old_poc_repositories -) - from services.github_service import GithubService from services.slack_service import SlackService +from bin.alarm_for_old_poc_repositories import alert_for_old_poc_repositories class TestOldPOCGitHubRepositoriesAlerting(unittest.TestCase): - def test_construct_message(self): - test_payload = {"repo1": 51, "repo2": 60} - - self.assertEqual(construct_message(test_payload), "The following POC GitHub Repositories persist:\n\nhttps://github.com/ministryofjustice/repo1 - 51 days old\nhttps://github.com/ministryofjustice/repo2 - 60 days old\n\nConsider if they are still required. If not, please archive them by removing them from the Terraform configuration: https://github.com/ministryofjustice/operations-engineering/tree/main/terraform/github/repositories/ministryofjustice") - @patch("gql.transport.aiohttp.AIOHTTPTransport.__new__", new=MagicMock) @patch("gql.Client.__new__", new=MagicMock) @patch("github.Github.__new__") - @patch("bin.alarm_for_old_poc_repositories.construct_message") @patch.object(GithubService, "get_old_poc_repositories") - @patch.object(SlackService, "send_message_to_plaintext_channel_name") + @patch.object(SlackService, "send_alert_for_poc_repositories") @patch('os.environ') def test_alert_for_old_poc_repositories_if_found( self, mock_env, - mock_send_message_to_plaintext_channel_name, + mock_send_alert_for_poc_repositories, mock_get_old_poc_repositories, - mock_construct_message, _mock_github_client_core_api ): - mock_env.get.side_effect = lambda k: 'mock_token' if k in ['GH_TOKEN', 'ADMIN_SLACK_TOKEN'] else None mock_get_old_poc_repositories.return_value = {"repo1": 51, "repo2": 60} - mock_construct_message.return_value = "The following POC GitHub Repositories persist:\n\nhttps://github.com/ministryofjustice/repo1 - 51 days old\nhttps://github.com/ministryofjustice/repo2 - 60 days old\n\nConsider if they are still required. If not, please archive them by removing them from the Terraform configuration: https://github.com/ministryofjustice/operations-engineering/tree/main/terraform/github/repositories/ministryofjustice" alert_for_old_poc_repositories() mock_get_old_poc_repositories.assert_called_once() - mock_send_message_to_plaintext_channel_name.assert_called_once_with("The following POC GitHub Repositories persist:\n\nhttps://github.com/ministryofjustice/repo1 - 51 days old\nhttps://github.com/ministryofjustice/repo2 - 60 days old\n\nConsider if they are still required. If not, please archive them by removing them from the Terraform configuration: https://github.com/ministryofjustice/operations-engineering/tree/main/terraform/github/repositories/ministryofjustice", "operations-engineering-alerts") + mock_send_alert_for_poc_repositories.assert_called_once_with({ + "repo1": 51, + "repo2": 60 + }) @patch("gql.transport.aiohttp.AIOHTTPTransport.__new__", new=MagicMock) @patch("gql.Client.__new__", new=MagicMock) @patch("github.Github.__new__") @patch.object(GithubService, "get_old_poc_repositories") - @patch.object(SlackService, "send_message_to_plaintext_channel_name") + @patch.object(SlackService, "send_alert_for_poc_repositories") @patch('os.environ') def test_alert_for_old_poc_repositories_if_not_found( self, mock_env, - mock_send_message_to_plaintext_channel_name, + mock_send_alert_for_poc_repositories, mock_get_old_poc_repositories, _mock_github_client_core_api ): - mock_env.get.side_effect = lambda k: 'mock_token' if k in ['GH_TOKEN', 'ADMIN_SLACK_TOKEN'] else None mock_get_old_poc_repositories.return_value = {} alert_for_old_poc_repositories() mock_get_old_poc_repositories.assert_called_once() - assert not mock_send_message_to_plaintext_channel_name.called + mock_send_alert_for_poc_repositories.assert_not_called() if __name__ == '__main__':