Skip to content
Merged
6 changes: 3 additions & 3 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "^.secrets.baseline$",
"lines": null
},
"generated_at": "2025-10-27T10:20:07Z",
"generated_at": "2025-12-15T15:57:18Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -82,15 +82,15 @@
"hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e",
"is_secret": false,
"is_verified": false,
"line_number": 35,
"line_number": 44,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91",
"is_secret": false,
"is_verified": false,
"line_number": 44,
"line_number": 53,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
32 changes: 12 additions & 20 deletions bin/mas-devops-create-initial-users-for-saas
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand All @@ -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")
Expand Down Expand Up @@ -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}")
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -109,16 +103,15 @@ 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:
with open(initial_users_yaml_file, 'r') as file:
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
Expand All @@ -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}")
failed_user_ids = list(map(lambda u: u["email"], result["failed"]))
raise Exception(f"Sync failed for the following user IDs {failed_user_ids}")
66 changes: 58 additions & 8 deletions bin/mas-devops-notify-slack
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@ 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(channels: list[str], rc: int, additionalMsg: str = None) -> bool:
"""Send Slack notification about Fyre OCP cluster provisioning status."""
name = _getClusterName()
toolchainLink = _getToolchainLink()

if rc == 0:
url = os.getenv("OCP_CONSOLE_URL", None)
Expand All @@ -44,13 +53,47 @@ def notifyProvisionFyre(channel: str, rc: int) -> bool:
SlackUtil.buildSection(f"- Username: `{username}`\n- Password: `{password}`"),
SlackUtil.buildSection(f"<https://beta.fyre.ibm.com/development/vms|Fyre Dashboard>{toolchainLink}")
]
if additionalMsg is not None:
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"<https://beta.fyre.ibm.com/development/vms|Fyre Dashboard>{toolchainLink}")
]

response = SlackUtil.postMessageBlocks(channel, 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: list[str], rc: int, additionalMsg: str = None) -> 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"<https://cloud.ibm.com/kubernetes/clusters|IBM Cloud Dashboard>{toolchainLink}")
]
if additionalMsg is not None:
message.append(SlackUtil.buildSection(additionalMsg))
else:
message = [
SlackUtil.buildHeader(f":glyph-fail: Your IBM Cloud ROKS cluster ({name}) failed to deploy"),
SlackUtil.buildSection(f"<https://cloud.ibm.com/kubernetes/clusters|IBM Cloud Dashboard>{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)


Expand All @@ -61,13 +104,20 @@ if __name__ == "__main__":
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("--msg", required=False, default=None)

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":
notifyProvisionRoks(channelList, args.rc, args.msg)
1 change: 0 additions & 1 deletion bin/mas-devops-saas-job-cleaner
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ if __name__ == "__main__":
ch.setFormatter(chFormatter)
logger.addHandler(ch)


limit = args.limit
label = args.label
dry_run = args.dry_run
Expand Down
153 changes: 86 additions & 67 deletions src/mas/devops/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,73 +39,92 @@ def client(cls) -> WebClient:

# Post message to Slack
# -----------------------------------------------------------------------------
def postMessageBlocks(cls, channelName: str, messageBlocks: list, threadId: str = None) -> SlackResponse:
if threadId is None:
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 = None) -> SlackResponse | list[SlackResponse]:
responses: list[SlackResponse] = []

if isinstance(channelList, str):
channelList = [channelList]
for channel in channelList:
try:
if threadId is None:
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 = None) -> SlackResponse | list[SlackResponse]:
responses: list[SlackResponse] = []

if isinstance(channelList, str):
channelList = [channelList]

for channel in channelList:
if threadId is None:
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 = None, messageTimestamp: str = None, domain: str = "ibm-mas"
Expand Down
Loading