From fd67c5826f584567175bab320143b6ec2c8200c3 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:27:36 -0300 Subject: [PATCH 01/11] [patch] create notifyProvisionRoks --- .secrets.baseline | 6 ++--- bin/mas-devops-notify-slack | 49 +++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 0ed7fabf..6a423840 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-10-27T10:20:07Z", + "generated_at": "2025-12-10T15:27:12Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -82,7 +82,7 @@ "hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e", "is_secret": false, "is_verified": false, - "line_number": 35, + "line_number": 42, "type": "Secret Keyword", "verified_result": null }, @@ -90,7 +90,7 @@ "hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91", "is_secret": false, "is_verified": false, - "line_number": 44, + "line_number": 51, "type": "Secret Keyword", "verified_result": null } diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index 13ea97dc..d8a64d97 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -15,19 +15,26 @@ import os import sys from mas.devops.slack import SlackUtil - -def notifyProvisionFyre(channel: str, rc: int) -> bool: - name = os.getenv("CLUSTER_NAME", None) - if name is None: +def _getClusterName() -> str: + name = os.getenv("CLUSTER_NAME", "") + if name == "": print("CLUSTER_NAME env var must be set") sys.exit(1) + return name - # Support optional metadata from standard IBM CD Toolchains environment variables - toolchainLink = "" +def _getToolchainLink() -> str: toolchainUrl = os.getenv("TOOLCHAIN_PIPELINERUN_URL", None) toolchainTriggerName = os.getenv("TOOLCHAIN_TRIGGER_NAME", None) if toolchainUrl is not None and toolchainTriggerName is not None: - toolchainLink = f" | <{toolchainUrl}|Pipeline Run>" + toolchainLink = f"<{toolchainUrl}|{toolchainTriggerName}>" + return toolchainLink + return "" + + +def notifyProvisionFyre(channel: str, rc: int) -> bool: + """Send Slack notification about Fyre OCP cluster provisioning status.""" + name = _getClusterName() + toolchainLink = _getToolchainLink() if rc == 0: url = os.getenv("OCP_CONSOLE_URL", None) @@ -54,6 +61,32 @@ def notifyProvisionFyre(channel: str, rc: int) -> bool: return response.data.get("ok", False) +def notifyProvisionRoks(channel: str, rc: int) -> bool: + """Send Slack notification about ROKS cluster provisioning status.""" + name = _getClusterName() + toolchainLink = _getToolchainLink() + + if rc == 0: + url = os.getenv("OCP_CONSOLE_URL", None) + if url is None: + print("OCP_CONSOLE_URL env var must be set") + sys.exit(1) + + message = [ + SlackUtil.buildHeader(f":glyph-ok: Your IBM Cloud ROKS cluster ({name}) is ready"), + SlackUtil.buildSection(f"{url}"), + SlackUtil.buildSection(f"{toolchainLink}") + ] + else: + message = [ + SlackUtil.buildHeader(f":glyph-fail: Your IBM Cloud ROKS cluster ({name}) failed to deploy"), + SlackUtil.buildSection(f"{toolchainLink}") + ] + + response = SlackUtil.postMessageBlocks(channel, message) + return response.data.get("ok", False) + + if __name__ == "__main__": # If SLACK_TOKEN or SLACK_CHANNEL env vars are not set then silently exit taking no action SLACK_TOKEN = os.getenv("SLACK_TOKEN", "") @@ -71,3 +104,5 @@ if __name__ == "__main__": if args.action == "ocp-provision-fyre": notifyProvisionFyre(SLACK_CHANNEL, args.rc) + elif args.action == "ocp-provision-roks": + notifyProvisionRoks(SLACK_CHANNEL, args.rc) From c25968f843c88b714e6391c4ae75cc4807f4a62c Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:15:22 -0300 Subject: [PATCH 02/11] [patch] create notification for roks --- bin/mas-devops-notify-slack | 13 ++++++++++--- src/mas/devops/slack.py | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index d8a64d97..6a07f796 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -29,7 +29,7 @@ def _getToolchainLink() -> str: toolchainLink = f"<{toolchainUrl}|{toolchainTriggerName}>" return toolchainLink return "" - + def notifyProvisionFyre(channel: str, rc: int) -> bool: """Send Slack notification about Fyre OCP cluster provisioning status.""" @@ -61,7 +61,7 @@ def notifyProvisionFyre(channel: str, rc: int) -> bool: return response.data.get("ok", False) -def notifyProvisionRoks(channel: str, rc: int) -> bool: +def notifyProvisionRoks(channel: str, rc: int, additionalDetails: str = "") -> bool: """Send Slack notification about ROKS cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -77,6 +77,8 @@ def notifyProvisionRoks(channel: str, rc: int) -> bool: SlackUtil.buildSection(f"{url}"), SlackUtil.buildSection(f"{toolchainLink}") ] + if additionalDetails != "": + message.append(SlackUtil.buildSection(additionalDetails)) else: message = [ SlackUtil.buildHeader(f":glyph-fail: Your IBM Cloud ROKS cluster ({name}) failed to deploy"), @@ -100,9 +102,14 @@ if __name__ == "__main__": # Primary Options parser.add_argument("--action", required=True) parser.add_argument("--rc", required=True, type=int) + parser.add_argument("--add-details") + args, unknown = parser.parse_known_args() if args.action == "ocp-provision-fyre": notifyProvisionFyre(SLACK_CHANNEL, args.rc) elif args.action == "ocp-provision-roks": - notifyProvisionRoks(SLACK_CHANNEL, args.rc) + if args.add_details: + notifyProvisionRoks(SLACK_CHANNEL, args.rc, args.add_details) + else: + notifyProvisionRoks(SLACK_CHANNEL, args.rc) \ No newline at end of file diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index c318d281..59d7f1ad 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -39,8 +39,8 @@ def client(cls) -> WebClient: # Post message to Slack # ----------------------------------------------------------------------------- - def postMessageBlocks(cls, channelName: str, messageBlocks: list, threadId: str = None) -> SlackResponse: - if threadId is None: + def postMessageBlocks(cls, channelName: str, messageBlocks: list, threadId: str = "") -> SlackResponse: + if threadId == "": logger.debug(f"Posting {len(messageBlocks)} block message to {channelName} in Slack") response = cls.client.chat_postMessage( channel=channelName, @@ -108,12 +108,12 @@ def postMessageText(cls, channelName, message, attachments=None, threadId=None): return response def createMessagePermalink( - cls, slackResponse: SlackResponse = None, channelId: str = None, messageTimestamp: str = None, domain: str = "ibm-mas" + cls, slackResponse: SlackResponse = None, channelId: str = "", messageTimestamp: str = "", domain: str = "ibm-mas" ) -> str: if slackResponse is not None: channelId = slackResponse["channel"] messageTimestamp = slackResponse["ts"] - elif channelId is None or messageTimestamp is None: + elif channelId == "" or messageTimestamp == "": raise Exception("Either channelId and messageTimestamp, or slackReponse params must be provided") return f"https://{domain}.slack.com/archives/{channelId}/p{messageTimestamp.replace('.', '')}" From f4d28171b189b85b64359ad6403d1222991b15ef Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:47:26 -0300 Subject: [PATCH 03/11] [patch] implement broadcast --- .secrets.baseline | 6 +++--- bin/mas-devops-notify-slack | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 6a423840..e8c4b2ce 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-12-10T15:27:12Z", + "generated_at": "2025-12-10T20:44:32Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -82,7 +82,7 @@ "hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e", "is_secret": false, "is_verified": false, - "line_number": 42, + "line_number": 54, "type": "Secret Keyword", "verified_result": null }, @@ -90,7 +90,7 @@ "hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91", "is_secret": false, "is_verified": false, - "line_number": 51, + "line_number": 63, "type": "Secret Keyword", "verified_result": null } diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index 6a07f796..743a4358 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -30,8 +30,20 @@ def _getToolchainLink() -> str: return toolchainLink return "" +def _broadcast(channels: str, message: list): + channelList = [ch.strip() for ch in channels.split(",")] + responses = [] + for channel in channelList: + try: + response = SlackUtil.postMessageBlocks(channel, message) + responses.append(response.data.get("ok", False)) + except Exception as e: + print(f"Failed to send message to channel {channel}: {e}") + responses.append(False) + return all(responses) if responses else True + -def notifyProvisionFyre(channel: str, rc: int) -> bool: +def notifyProvisionFyre(channels: str, rc: int) -> bool: """Send Slack notification about Fyre OCP cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -57,11 +69,10 @@ def notifyProvisionFyre(channel: str, rc: int) -> bool: SlackUtil.buildSection(f"{toolchainLink}") ] - response = SlackUtil.postMessageBlocks(channel, message) - return response.data.get("ok", False) + return _broadcast(channels, message) -def notifyProvisionRoks(channel: str, rc: int, additionalDetails: str = "") -> bool: +def notifyProvisionRoks(channels: str, rc: int, additionalDetails: str = "") -> bool: """Send Slack notification about ROKS cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -85,8 +96,7 @@ def notifyProvisionRoks(channel: str, rc: int, additionalDetails: str = "") -> b SlackUtil.buildSection(f"{toolchainLink}") ] - response = SlackUtil.postMessageBlocks(channel, message) - return response.data.get("ok", False) + return _broadcast(channels, message) if __name__ == "__main__": From 76a682d45de935bf6d05f93f76ca05b133011f79 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:22:27 -0300 Subject: [PATCH 04/11] [patch] handle edge case when additional channel is empty --- .secrets.baseline | 6 +++--- bin/mas-devops-notify-slack | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index e8c4b2ce..e320082d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-12-10T20:44:32Z", + "generated_at": "2025-12-11T13:22:20Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -82,7 +82,7 @@ "hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e", "is_secret": false, "is_verified": false, - "line_number": 54, + "line_number": 55, "type": "Secret Keyword", "verified_result": null }, @@ -90,7 +90,7 @@ "hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91", "is_secret": false, "is_verified": false, - "line_number": 63, + "line_number": 64, "type": "Secret Keyword", "verified_result": null } diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index 743a4358..50540590 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -35,8 +35,9 @@ def _broadcast(channels: str, message: list): responses = [] for channel in channelList: try: - response = SlackUtil.postMessageBlocks(channel, message) - responses.append(response.data.get("ok", False)) + if channel: + response = SlackUtil.postMessageBlocks(channel, message) + responses.append(response.data.get("ok", False)) except Exception as e: print(f"Failed to send message to channel {channel}: {e}") responses.append(False) From 624dd79e9a61705969bd094dd5861898acfcf5e8 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:30:22 -0300 Subject: [PATCH 05/11] [patch] move broadcast to slack utils --- .secrets.baseline | 6 +- bin/mas-devops-notify-slack | 49 ++++++------ src/mas/devops/slack.py | 154 ++++++++++++++++++++---------------- test/src/test_slack.py | 13 +++ 4 files changed, 125 insertions(+), 97 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index e320082d..4a87b27d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-12-11T13:22:20Z", + "generated_at": "2025-12-12T20:28:52Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -82,7 +82,7 @@ "hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e", "is_secret": false, "is_verified": false, - "line_number": 55, + "line_number": 42, "type": "Secret Keyword", "verified_result": null }, @@ -90,7 +90,7 @@ "hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91", "is_secret": false, "is_verified": false, - "line_number": 64, + "line_number": 51, "type": "Secret Keyword", "verified_result": null } diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index 50540590..ef9a1380 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -30,21 +30,8 @@ def _getToolchainLink() -> str: return toolchainLink return "" -def _broadcast(channels: str, message: list): - channelList = [ch.strip() for ch in channels.split(",")] - responses = [] - for channel in channelList: - try: - if channel: - response = SlackUtil.postMessageBlocks(channel, message) - responses.append(response.data.get("ok", False)) - except Exception as e: - print(f"Failed to send message to channel {channel}: {e}") - responses.append(False) - return all(responses) if responses else True - -def notifyProvisionFyre(channels: str, rc: int) -> bool: +def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = "") -> bool: """Send Slack notification about Fyre OCP cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -64,16 +51,21 @@ def notifyProvisionFyre(channels: str, rc: int) -> bool: SlackUtil.buildSection(f"- Username: `{username}`\n- Password: `{password}`"), SlackUtil.buildSection(f"{toolchainLink}") ] + if additionalMsg: + message.append(SlackUtil.buildSection(additionalMsg)) else: message = [ SlackUtil.buildHeader(f":glyph-fail: Your IBM DevIT Fyre OCP cluster ({name}) failed to deploy"), SlackUtil.buildSection(f"{toolchainLink}") ] - - return _broadcast(channels, message) + + response = SlackUtil.postMessageBlocks(channels, message) + if isinstance(response, list): + return all([res.data.get("ok", False) for res in response]) + return response.data.get("ok", False) -def notifyProvisionRoks(channels: str, rc: int, additionalDetails: str = "") -> bool: +def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str = "") -> bool: """Send Slack notification about ROKS cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -89,15 +81,18 @@ def notifyProvisionRoks(channels: str, rc: int, additionalDetails: str = "") -> SlackUtil.buildSection(f"{url}"), SlackUtil.buildSection(f"{toolchainLink}") ] - if additionalDetails != "": - message.append(SlackUtil.buildSection(additionalDetails)) + if additionalMsg: + message.append(SlackUtil.buildSection(additionalMsg)) else: message = [ SlackUtil.buildHeader(f":glyph-fail: Your IBM Cloud ROKS cluster ({name}) failed to deploy"), SlackUtil.buildSection(f"{toolchainLink}") ] - return _broadcast(channels, message) + response = SlackUtil.postMessageBlocks(channels, message) + if isinstance(response, list): + return all([res.data.get("ok", False) for res in response]) + return response.data.get("ok", False) if __name__ == "__main__": @@ -106,21 +101,21 @@ if __name__ == "__main__": SLACK_CHANNEL = os.getenv("SLACK_CHANNEL", "") if SLACK_TOKEN == "" or SLACK_CHANNEL == "": sys.exit(0) - + + # Parse comma-separated channel list + channelList = [ch.strip() for ch in SLACK_CHANNEL.split(",")] + # Initialize the properties we need parser = argparse.ArgumentParser() # Primary Options parser.add_argument("--action", required=True) parser.add_argument("--rc", required=True, type=int) - parser.add_argument("--add-details") + parser.add_argument("--msg", required=False, default="") args, unknown = parser.parse_known_args() if args.action == "ocp-provision-fyre": - notifyProvisionFyre(SLACK_CHANNEL, args.rc) + notifyProvisionFyre(channelList, args.rc, args.msg) elif args.action == "ocp-provision-roks": - if args.add_details: - notifyProvisionRoks(SLACK_CHANNEL, args.rc, args.add_details) - else: - notifyProvisionRoks(SLACK_CHANNEL, args.rc) \ No newline at end of file + notifyProvisionRoks(channelList, args.rc, args.msg) diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index 59d7f1ad..e027d096 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -39,73 +39,93 @@ def client(cls) -> WebClient: # Post message to Slack # ----------------------------------------------------------------------------- - def postMessageBlocks(cls, channelName: str, messageBlocks: list, threadId: str = "") -> SlackResponse: - if threadId == "": - logger.debug(f"Posting {len(messageBlocks)} block message to {channelName} in Slack") - response = cls.client.chat_postMessage( - channel=channelName, - blocks=messageBlocks, - text="Summary text unavailable", - mrkdwn=True, - parse="none", - unfurl_links=False, - unfurl_media=False, - link_names=True, - as_user=True, - ) - else: - logger.debug(f"Posting {len(messageBlocks)} block message to {channelName} on thread {threadId} in Slack") - response = cls.client.chat_postMessage( - channel=channelName, - thread_ts=threadId, - blocks=messageBlocks, - text="Summary text unavailable", - mrkdwn=True, - parse="none", - unfurl_links=False, - unfurl_media=False, - link_names=True, - as_user=True, - ) - - if not response["ok"]: - logger.warning(response.data) - logger.warning("Failed to call Slack API") - return response - - def postMessageText(cls, channelName, message, attachments=None, threadId=None): - if threadId is None: - logger.debug(f"Posting message to {channelName} in Slack") - response = cls.client.chat_postMessage( - channel=channelName, - text=message, - attachments=attachments, - mrkdwn=True, - parse="none", - unfurl_links=False, - unfurl_media=False, - link_names=True, - as_user=True, - ) - else: - logger.debug(f"Posting message to {channelName} on thread {threadId} in Slack") - response = cls.client.chat_postMessage( - channel=channelName, - thread_ts=threadId, - text=message, - attachments=attachments, - mrkdwn=True, - parse="none", - unfurl_links=False, - unfurl_media=False, - link_names=True, - as_user=True, - ) - - if not response["ok"]: - logger.warning(response.data) - logger.warning("Failed to call Slack API") - return response + def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = "") -> SlackResponse | list[SlackResponse]: + responses = [] + + if isinstance(channelList, str): + channelList = [channelList] + for channel in channelList: + logger.info(f"{channel}") + try: + if threadId == "": + logger.debug(f"Posting {len(messageBlocks)} block message to {channel} in Slack") + response = cls.client.chat_postMessage( + channel=channel, + blocks=messageBlocks, + text="Summary text unavailable", + mrkdwn=True, + parse="none", + unfurl_links=False, + unfurl_media=False, + link_names=True, + as_user=True, + ) + else: + logger.debug(f"Posting {len(messageBlocks)} block message to {channel} on thread {threadId} in Slack") + response = cls.client.chat_postMessage( + channel=channel, + thread_ts=threadId, + blocks=messageBlocks, + text="Summary text unavailable", + mrkdwn=True, + parse="none", + unfurl_links=False, + unfurl_media=False, + link_names=True, + as_user=True, + ) + + if not response["ok"]: + logger.warning(response.data) + logger.warning("Failed to call Slack API") + responses.append(response) + except Exception as e: + logger.error(f"Fail to send a message to {channel}: {e}") + raise + + return responses if len(responses) > 1 else responses[0] + + def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = "") -> SlackResponse | list[SlackResponse]: + responses: list[str] = [] + + if isinstance(channelList, str): + channelList = [channelList] + + for channel in channelList: + if threadId == "": + logger.debug(f"Posting message to {channel} in Slack") + response = cls.client.chat_postMessage( + channel=channel, + text=message, + attachments=attachments, + mrkdwn=True, + parse="none", + unfurl_links=False, + unfurl_media=False, + link_names=True, + as_user=True, + ) + else: + logger.debug(f"Posting message to {channel} on thread {threadId} in Slack") + response = cls.client.chat_postMessage( + channel=channel, + thread_ts=threadId, + text=message, + attachments=attachments, + mrkdwn=True, + parse="none", + unfurl_links=False, + unfurl_media=False, + link_names=True, + as_user=True, + ) + + if not response["ok"]: + logger.warning(response.data) + logger.warning("Failed to call Slack API") + responses.append(response) + + return responses if len(responses) > 1 else responses[0] def createMessagePermalink( cls, slackResponse: SlackResponse = None, channelId: str = "", messageTimestamp: str = "", domain: str = "ibm-mas" diff --git a/test/src/test_slack.py b/test/src/test_slack.py index f2f994db..6d363066 100644 --- a/test/src/test_slack.py +++ b/test/src/test_slack.py @@ -21,3 +21,16 @@ def testSendMessage(): assert response.data["ok"] is True assert "ts" in response.data + + +def testBroadcast(): + responses = SlackUtil.postMessageText(["#bot-test", "#bot-test"], "mas-devops unittest") + + for response in responses: + assert "channel" in response.data + assert response.data["channel"] == "C06453F9KFC" + + assert "ok" in response.data + assert response.data["ok"] is True + + assert "ts" in response.data From b6ee1166de9a4f4d703160d74d67e72befa50394 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:43:31 -0300 Subject: [PATCH 06/11] [patch] replace var types --- src/mas/devops/slack.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index e027d096..97a2454f 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -39,13 +39,12 @@ def client(cls) -> WebClient: # Post message to Slack # ----------------------------------------------------------------------------- - def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = "") -> SlackResponse | list[SlackResponse]: - responses = [] + def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = None) -> SlackResponse | list[SlackResponse]: + responses: list[SlackResponse] = [] if isinstance(channelList, str): channelList = [channelList] for channel in channelList: - logger.info(f"{channel}") try: if threadId == "": logger.debug(f"Posting {len(messageBlocks)} block message to {channel} in Slack") @@ -85,8 +84,8 @@ def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, th return responses if len(responses) > 1 else responses[0] - def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = "") -> SlackResponse | list[SlackResponse]: - responses: list[str] = [] + def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = None) -> SlackResponse | list[SlackResponse]: + responses: list[SlackResponse] = [] if isinstance(channelList, str): channelList = [channelList] @@ -128,7 +127,7 @@ def postMessageText(cls, channelList: str | list[str], message: str, attachments return responses if len(responses) > 1 else responses[0] def createMessagePermalink( - cls, slackResponse: SlackResponse = None, channelId: str = "", messageTimestamp: str = "", domain: str = "ibm-mas" + cls, slackResponse: SlackResponse = None, channelId: str = None, messageTimestamp: str = None, domain: str = "ibm-mas" ) -> str: if slackResponse is not None: channelId = slackResponse["channel"] From 77bed981fa3573cddad9a4e4f19da3d8e436cc27 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 15 Dec 2025 15:36:10 +0000 Subject: [PATCH 07/11] Update slack.py --- src/mas/devops/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index 97a2454f..b095ec30 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -132,7 +132,7 @@ def createMessagePermalink( if slackResponse is not None: channelId = slackResponse["channel"] messageTimestamp = slackResponse["ts"] - elif channelId == "" or messageTimestamp == "": + elif channelId is None or messageTimestamp is None: raise Exception("Either channelId and messageTimestamp, or slackReponse params must be provided") return f"https://{domain}.slack.com/archives/{channelId}/p{messageTimestamp.replace('.', '')}" From 8ca181d49f8abf95954d247f0b7247ffa1cb0f70 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 15 Dec 2025 15:37:49 +0000 Subject: [PATCH 08/11] Update test_slack.py --- test/src/test_slack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/src/test_slack.py b/test/src/test_slack.py index 6d363066..8c40d809 100644 --- a/test/src/test_slack.py +++ b/test/src/test_slack.py @@ -12,7 +12,7 @@ def testSendMessage(): - response = SlackUtil.postMessageText("#bot-test", "mas-devops unittest") + response = SlackUtil.postMessageText("#bot-test", "mas-devops postMessageTest() unittest") assert "channel" in response.data assert response.data["channel"] == "C06453F9KFC" @@ -24,8 +24,8 @@ def testSendMessage(): def testBroadcast(): - responses = SlackUtil.postMessageText(["#bot-test", "#bot-test"], "mas-devops unittest") - + responses = SlackUtil.postMessageText(["#bot-test", "#bot-test"], "mas-devops postMessageText() broadcast unittest") + assert len(responses) == 2 for response in responses: assert "channel" in response.data assert response.data["channel"] == "C06453F9KFC" From b539f6a91010abc256e79740e8b805aa61f01da6 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 15 Dec 2025 15:39:39 +0000 Subject: [PATCH 09/11] Update slack.py --- src/mas/devops/slack.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index b095ec30..8161e504 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -39,14 +39,14 @@ def client(cls) -> WebClient: # Post message to Slack # ----------------------------------------------------------------------------- - def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = None) -> SlackResponse | list[SlackResponse]: + def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str=None) -> SlackResponse | list[SlackResponse]: responses: list[SlackResponse] = [] if isinstance(channelList, str): channelList = [channelList] for channel in channelList: try: - if threadId == "": + if threadId is None: logger.debug(f"Posting {len(messageBlocks)} block message to {channel} in Slack") response = cls.client.chat_postMessage( channel=channel, @@ -84,14 +84,14 @@ def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, th return responses if len(responses) > 1 else responses[0] - def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = None) -> SlackResponse | list[SlackResponse]: + def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str=None) -> SlackResponse | list[SlackResponse]: responses: list[SlackResponse] = [] if isinstance(channelList, str): channelList = [channelList] for channel in channelList: - if threadId == "": + if threadId is None: logger.debug(f"Posting message to {channel} in Slack") response = cls.client.chat_postMessage( channel=channel, From 48b27b542825ba6ec02ad2edfc6950c9b5b77dee Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 15 Dec 2025 15:42:21 +0000 Subject: [PATCH 10/11] Update mas-devops-notify-slack --- bin/mas-devops-notify-slack | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index ef9a1380..96051145 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -15,6 +15,7 @@ import os import sys from mas.devops.slack import SlackUtil + def _getClusterName() -> str: name = os.getenv("CLUSTER_NAME", "") if name == "": @@ -22,6 +23,7 @@ def _getClusterName() -> str: sys.exit(1) return name + def _getToolchainLink() -> str: toolchainUrl = os.getenv("TOOLCHAIN_PIPELINERUN_URL", None) toolchainTriggerName = os.getenv("TOOLCHAIN_TRIGGER_NAME", None) @@ -31,7 +33,7 @@ def _getToolchainLink() -> str: return "" -def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = "") -> bool: +def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str=None) -> bool: """Send Slack notification about Fyre OCP cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -51,7 +53,7 @@ def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = "") - SlackUtil.buildSection(f"- Username: `{username}`\n- Password: `{password}`"), SlackUtil.buildSection(f"{toolchainLink}") ] - if additionalMsg: + if additionalMsg is not None: message.append(SlackUtil.buildSection(additionalMsg)) else: message = [ @@ -65,7 +67,7 @@ def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = "") - return response.data.get("ok", False) -def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str = "") -> bool: +def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str=None) -> bool: """Send Slack notification about ROKS cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -81,7 +83,7 @@ def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str = "") - SlackUtil.buildSection(f"{url}"), SlackUtil.buildSection(f"{toolchainLink}") ] - if additionalMsg: + if additionalMsg is not None: message.append(SlackUtil.buildSection(additionalMsg)) else: message = [ @@ -111,7 +113,7 @@ if __name__ == "__main__": # Primary Options parser.add_argument("--action", required=True) parser.add_argument("--rc", required=True, type=int) - parser.add_argument("--msg", required=False, default="") + parser.add_argument("--msg", required=False, default=None) args, unknown = parser.parse_known_args() From 67bbf3bcf1b9808f33ffb235209b499a41820c53 Mon Sep 17 00:00:00 2001 From: David Parker Date: Mon, 15 Dec 2025 15:57:50 +0000 Subject: [PATCH 11/11] Linting --- .secrets.baseline | 6 ++-- bin/mas-devops-create-initial-users-for-saas | 32 ++++++++------------ bin/mas-devops-notify-slack | 12 ++++---- bin/mas-devops-saas-job-cleaner | 1 - src/mas/devops/slack.py | 4 +-- 5 files changed, 23 insertions(+), 32 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 4a87b27d..21a8f1c5 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-12-12T20:28:52Z", + "generated_at": "2025-12-15T15:57:18Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -82,7 +82,7 @@ "hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e", "is_secret": false, "is_verified": false, - "line_number": 42, + "line_number": 44, "type": "Secret Keyword", "verified_result": null }, @@ -90,7 +90,7 @@ "hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91", "is_secret": false, "is_verified": false, - "line_number": 51, + "line_number": 53, "type": "Secret Keyword", "verified_result": null } diff --git a/bin/mas-devops-create-initial-users-for-saas b/bin/mas-devops-create-initial-users-for-saas index bac45ce9..7372f495 100644 --- a/bin/mas-devops-create-initial-users-for-saas +++ b/bin/mas-devops-create-initial-users-for-saas @@ -10,21 +10,18 @@ # # ***************************************************************************** +from mas.devops.users import MASUserUtils +from botocore.exceptions import ClientError +import boto3 +import sys +import json +import yaml from kubernetes import client, config from kubernetes.config.config_exception import ConfigException import argparse import logging import urllib3 urllib3.disable_warnings() -import yaml -import json -import sys - -import boto3 -from botocore.exceptions import ClientError - -from mas.devops.users import MASUserUtils - if __name__ == "__main__": @@ -38,7 +35,6 @@ if __name__ == "__main__": parser.add_argument("--admin-dashboard-port", required=False, default=443) parser.add_argument("--manage-api-port", required=False, default=443) - group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--initial-users-yaml-file") group.add_argument("--initial-users-secret-name") @@ -66,7 +62,6 @@ if __name__ == "__main__": admin_dashboard_port = args.admin_dashboard_port manage_api_port = args.manage_api_port - logger.info("Configuration:") logger.info("--------------") logger.info(f"mas_instance_id: {mas_instance_id}") @@ -88,7 +83,6 @@ if __name__ == "__main__": config.load_kube_config() logger.debug("Loaded kubeconfig file") - user_utils = MASUserUtils(mas_instance_id, mas_workspace_id, client.api_client.ApiClient(), coreapi_port=coreapi_port, admin_dashboard_port=admin_dashboard_port, manage_api_port=manage_api_port) if initial_users_secret_name is not None: @@ -100,7 +94,7 @@ if __name__ == "__main__": service_name='secretsmanager', ) try: - initial_users_secret = aws_sm_client.get_secret_value( # pragma: allowlist secret + initial_users_secret = aws_sm_client.get_secret_value( # pragma: allowlist secret SecretId=initial_users_secret_name ) except ClientError as e: @@ -109,7 +103,7 @@ if __name__ == "__main__": sys.exit(0) raise Exception(f"Failed to fetch secret {initial_users_secret_name}: {str(e)}") - + secret_json = json.loads(initial_users_secret['SecretString']) initial_users = user_utils.parse_initial_users_from_aws_secret_json(secret_json) elif initial_users_yaml_file is not None: @@ -117,8 +111,7 @@ if __name__ == "__main__": initial_users = yaml.safe_load(file) else: raise Exception("Something unexpected happened") - - + result = user_utils.create_initial_users_for_saas(initial_users) # if user details were sourced from an AWS SM secret, remove the completed entries from the secret @@ -133,14 +126,13 @@ if __name__ == "__main__": if has_updates: logger.info(f"Updating secret {initial_users_secret_name}") try: - aws_sm_client.update_secret( # pragma: allowlist secret + aws_sm_client.update_secret( # pragma: allowlist secret SecretId=initial_users_secret_name, SecretString=json.dumps(secret_json) ) except ClientError as e: raise Exception(f"Failed to update secret {initial_users_secret_name}: {str(e)}") - if len(result["failed"]) > 0: - failed_user_ids = list(map(lambda u : u["email"], result["failed"])) - raise Exception(f"Sync failed for the following user IDs {failed_user_ids}") \ No newline at end of file + failed_user_ids = list(map(lambda u: u["email"], result["failed"])) + raise Exception(f"Sync failed for the following user IDs {failed_user_ids}") diff --git a/bin/mas-devops-notify-slack b/bin/mas-devops-notify-slack index 96051145..04c71ce8 100644 --- a/bin/mas-devops-notify-slack +++ b/bin/mas-devops-notify-slack @@ -33,7 +33,7 @@ def _getToolchainLink() -> str: return "" -def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str=None) -> bool: +def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = None) -> bool: """Send Slack notification about Fyre OCP cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -60,14 +60,14 @@ def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str=None) - SlackUtil.buildHeader(f":glyph-fail: Your IBM DevIT Fyre OCP cluster ({name}) failed to deploy"), SlackUtil.buildSection(f"{toolchainLink}") ] - + response = SlackUtil.postMessageBlocks(channels, message) if isinstance(response, list): return all([res.data.get("ok", False) for res in response]) return response.data.get("ok", False) -def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str=None) -> bool: +def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str = None) -> bool: """Send Slack notification about ROKS cluster provisioning status.""" name = _getClusterName() toolchainLink = _getToolchainLink() @@ -103,10 +103,10 @@ if __name__ == "__main__": SLACK_CHANNEL = os.getenv("SLACK_CHANNEL", "") if SLACK_TOKEN == "" or SLACK_CHANNEL == "": sys.exit(0) - + # Parse comma-separated channel list channelList = [ch.strip() for ch in SLACK_CHANNEL.split(",")] - + # Initialize the properties we need parser = argparse.ArgumentParser() @@ -114,7 +114,7 @@ if __name__ == "__main__": parser.add_argument("--action", required=True) parser.add_argument("--rc", required=True, type=int) parser.add_argument("--msg", required=False, default=None) - + args, unknown = parser.parse_known_args() if args.action == "ocp-provision-fyre": diff --git a/bin/mas-devops-saas-job-cleaner b/bin/mas-devops-saas-job-cleaner index 549ce0da..374a7ef3 100644 --- a/bin/mas-devops-saas-job-cleaner +++ b/bin/mas-devops-saas-job-cleaner @@ -46,7 +46,6 @@ if __name__ == "__main__": ch.setFormatter(chFormatter) logger.addHandler(ch) - limit = args.limit label = args.label dry_run = args.dry_run diff --git a/src/mas/devops/slack.py b/src/mas/devops/slack.py index 8161e504..6aa39bc0 100644 --- a/src/mas/devops/slack.py +++ b/src/mas/devops/slack.py @@ -39,7 +39,7 @@ def client(cls) -> WebClient: # Post message to Slack # ----------------------------------------------------------------------------- - def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str=None) -> SlackResponse | list[SlackResponse]: + def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = None) -> SlackResponse | list[SlackResponse]: responses: list[SlackResponse] = [] if isinstance(channelList, str): @@ -84,7 +84,7 @@ def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, th return responses if len(responses) > 1 else responses[0] - def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str=None) -> SlackResponse | list[SlackResponse]: + def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = None) -> SlackResponse | list[SlackResponse]: responses: list[SlackResponse] = [] if isinstance(channelList, str):