Skip to content

Commit cd070d4

Browse files
authored
Merge branch 'stable' into update-251224
2 parents 7044dd2 + 25b9131 commit cd070d4

9 files changed

Lines changed: 273 additions & 415 deletions

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2025-10-27T10:20:07Z",
6+
"generated_at": "2025-12-15T15:57:18Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -82,15 +82,15 @@
8282
"hashed_secret": "053f5ed451647be0bbb6f67b80d6726808cad97e",
8383
"is_secret": false,
8484
"is_verified": false,
85-
"line_number": 35,
85+
"line_number": 44,
8686
"type": "Secret Keyword",
8787
"verified_result": null
8888
},
8989
{
9090
"hashed_secret": "4f75456d6c1887d41ed176f7ad3e2cfff3fdfd91",
9191
"is_secret": false,
9292
"is_verified": false,
93-
"line_number": 44,
93+
"line_number": 53,
9494
"type": "Secret Keyword",
9595
"verified_result": null
9696
}

bin/mas-devops-create-initial-users-for-saas

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,18 @@
1010
#
1111
# *****************************************************************************
1212

13+
from mas.devops.users import MASUserUtils
14+
from botocore.exceptions import ClientError
15+
import boto3
16+
import sys
17+
import json
18+
import yaml
1319
from kubernetes import client, config
1420
from kubernetes.config.config_exception import ConfigException
1521
import argparse
1622
import logging
1723
import urllib3
1824
urllib3.disable_warnings()
19-
import yaml
20-
import json
21-
import sys
22-
23-
import boto3
24-
from botocore.exceptions import ClientError
25-
26-
from mas.devops.users import MASUserUtils
27-
2825

2926

3027
if __name__ == "__main__":
@@ -38,7 +35,6 @@ if __name__ == "__main__":
3835
parser.add_argument("--admin-dashboard-port", required=False, default=443)
3936
parser.add_argument("--manage-api-port", required=False, default=443)
4037

41-
4238
group = parser.add_mutually_exclusive_group(required=True)
4339
group.add_argument("--initial-users-yaml-file")
4440
group.add_argument("--initial-users-secret-name")
@@ -66,7 +62,6 @@ if __name__ == "__main__":
6662
admin_dashboard_port = args.admin_dashboard_port
6763
manage_api_port = args.manage_api_port
6864

69-
7065
logger.info("Configuration:")
7166
logger.info("--------------")
7267
logger.info(f"mas_instance_id: {mas_instance_id}")
@@ -88,7 +83,6 @@ if __name__ == "__main__":
8883
config.load_kube_config()
8984
logger.debug("Loaded kubeconfig file")
9085

91-
9286
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)
9387

9488
if initial_users_secret_name is not None:
@@ -100,7 +94,7 @@ if __name__ == "__main__":
10094
service_name='secretsmanager',
10195
)
10296
try:
103-
initial_users_secret = aws_sm_client.get_secret_value( # pragma: allowlist secret
97+
initial_users_secret = aws_sm_client.get_secret_value( # pragma: allowlist secret
10498
SecretId=initial_users_secret_name
10599
)
106100
except ClientError as e:
@@ -109,16 +103,15 @@ if __name__ == "__main__":
109103
sys.exit(0)
110104

111105
raise Exception(f"Failed to fetch secret {initial_users_secret_name}: {str(e)}")
112-
106+
113107
secret_json = json.loads(initial_users_secret['SecretString'])
114108
initial_users = user_utils.parse_initial_users_from_aws_secret_json(secret_json)
115109
elif initial_users_yaml_file is not None:
116110
with open(initial_users_yaml_file, 'r') as file:
117111
initial_users = yaml.safe_load(file)
118112
else:
119113
raise Exception("Something unexpected happened")
120-
121-
114+
122115
result = user_utils.create_initial_users_for_saas(initial_users)
123116

124117
# if user details were sourced from an AWS SM secret, remove the completed entries from the secret
@@ -133,14 +126,13 @@ if __name__ == "__main__":
133126
if has_updates:
134127
logger.info(f"Updating secret {initial_users_secret_name}")
135128
try:
136-
aws_sm_client.update_secret( # pragma: allowlist secret
129+
aws_sm_client.update_secret( # pragma: allowlist secret
137130
SecretId=initial_users_secret_name,
138131
SecretString=json.dumps(secret_json)
139132
)
140133
except ClientError as e:
141134
raise Exception(f"Failed to update secret {initial_users_secret_name}: {str(e)}")
142-
143135

144136
if len(result["failed"]) > 0:
145-
failed_user_ids = list(map(lambda u : u["email"], result["failed"]))
146-
raise Exception(f"Sync failed for the following user IDs {failed_user_ids}")
137+
failed_user_ids = list(map(lambda u: u["email"], result["failed"]))
138+
raise Exception(f"Sync failed for the following user IDs {failed_user_ids}")

bin/mas-devops-notify-slack

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,27 @@ import sys
1616
from mas.devops.slack import SlackUtil
1717

1818

19-
def notifyProvisionFyre(channel: str, rc: int) -> bool:
20-
name = os.getenv("CLUSTER_NAME", None)
21-
if name is None:
19+
def _getClusterName() -> str:
20+
name = os.getenv("CLUSTER_NAME", "")
21+
if name == "":
2222
print("CLUSTER_NAME env var must be set")
2323
sys.exit(1)
24+
return name
2425

25-
# Support optional metadata from standard IBM CD Toolchains environment variables
26-
toolchainLink = ""
26+
27+
def _getToolchainLink() -> str:
2728
toolchainUrl = os.getenv("TOOLCHAIN_PIPELINERUN_URL", None)
2829
toolchainTriggerName = os.getenv("TOOLCHAIN_TRIGGER_NAME", None)
2930
if toolchainUrl is not None and toolchainTriggerName is not None:
30-
toolchainLink = f" | <{toolchainUrl}|Pipeline Run>"
31+
toolchainLink = f"<{toolchainUrl}|{toolchainTriggerName}>"
32+
return toolchainLink
33+
return ""
34+
35+
36+
def notifyProvisionFyre(channels: list[str], rc: int, additionalMsg: str = None) -> bool:
37+
"""Send Slack notification about Fyre OCP cluster provisioning status."""
38+
name = _getClusterName()
39+
toolchainLink = _getToolchainLink()
3140

3241
if rc == 0:
3342
url = os.getenv("OCP_CONSOLE_URL", None)
@@ -44,13 +53,47 @@ def notifyProvisionFyre(channel: str, rc: int) -> bool:
4453
SlackUtil.buildSection(f"- Username: `{username}`\n- Password: `{password}`"),
4554
SlackUtil.buildSection(f"<https://beta.fyre.ibm.com/development/vms|Fyre Dashboard>{toolchainLink}")
4655
]
56+
if additionalMsg is not None:
57+
message.append(SlackUtil.buildSection(additionalMsg))
4758
else:
4859
message = [
4960
SlackUtil.buildHeader(f":glyph-fail: Your IBM DevIT Fyre OCP cluster ({name}) failed to deploy"),
5061
SlackUtil.buildSection(f"<https://beta.fyre.ibm.com/development/vms|Fyre Dashboard>{toolchainLink}")
5162
]
5263

53-
response = SlackUtil.postMessageBlocks(channel, message)
64+
response = SlackUtil.postMessageBlocks(channels, message)
65+
if isinstance(response, list):
66+
return all([res.data.get("ok", False) for res in response])
67+
return response.data.get("ok", False)
68+
69+
70+
def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str = None) -> bool:
71+
"""Send Slack notification about ROKS cluster provisioning status."""
72+
name = _getClusterName()
73+
toolchainLink = _getToolchainLink()
74+
75+
if rc == 0:
76+
url = os.getenv("OCP_CONSOLE_URL", None)
77+
if url is None:
78+
print("OCP_CONSOLE_URL env var must be set")
79+
sys.exit(1)
80+
81+
message = [
82+
SlackUtil.buildHeader(f":glyph-ok: Your IBM Cloud ROKS cluster ({name}) is ready"),
83+
SlackUtil.buildSection(f"{url}"),
84+
SlackUtil.buildSection(f"<https://cloud.ibm.com/kubernetes/clusters|IBM Cloud Dashboard>{toolchainLink}")
85+
]
86+
if additionalMsg is not None:
87+
message.append(SlackUtil.buildSection(additionalMsg))
88+
else:
89+
message = [
90+
SlackUtil.buildHeader(f":glyph-fail: Your IBM Cloud ROKS cluster ({name}) failed to deploy"),
91+
SlackUtil.buildSection(f"<https://cloud.ibm.com/kubernetes/clusters|IBM Cloud Dashboard>{toolchainLink}")
92+
]
93+
94+
response = SlackUtil.postMessageBlocks(channels, message)
95+
if isinstance(response, list):
96+
return all([res.data.get("ok", False) for res in response])
5497
return response.data.get("ok", False)
5598

5699

@@ -61,13 +104,20 @@ if __name__ == "__main__":
61104
if SLACK_TOKEN == "" or SLACK_CHANNEL == "":
62105
sys.exit(0)
63106

107+
# Parse comma-separated channel list
108+
channelList = [ch.strip() for ch in SLACK_CHANNEL.split(",")]
109+
64110
# Initialize the properties we need
65111
parser = argparse.ArgumentParser()
66112

67113
# Primary Options
68114
parser.add_argument("--action", required=True)
69115
parser.add_argument("--rc", required=True, type=int)
116+
parser.add_argument("--msg", required=False, default=None)
117+
70118
args, unknown = parser.parse_known_args()
71119

72120
if args.action == "ocp-provision-fyre":
73-
notifyProvisionFyre(SLACK_CHANNEL, args.rc)
121+
notifyProvisionFyre(channelList, args.rc, args.msg)
122+
elif args.action == "ocp-provision-roks":
123+
notifyProvisionRoks(channelList, args.rc, args.msg)

bin/mas-devops-saas-job-cleaner

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ if __name__ == "__main__":
4646
ch.setFormatter(chFormatter)
4747
logger.addHandler(ch)
4848

49-
5049
limit = args.limit
5150
label = args.label
5251
dry_run = args.dry_run

src/mas/devops/slack.py

Lines changed: 86 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -39,73 +39,92 @@ def client(cls) -> WebClient:
3939

4040
# Post message to Slack
4141
# -----------------------------------------------------------------------------
42-
def postMessageBlocks(cls, channelName: str, messageBlocks: list, threadId: str = None) -> SlackResponse:
43-
if threadId is None:
44-
logger.debug(f"Posting {len(messageBlocks)} block message to {channelName} in Slack")
45-
response = cls.client.chat_postMessage(
46-
channel=channelName,
47-
blocks=messageBlocks,
48-
text="Summary text unavailable",
49-
mrkdwn=True,
50-
parse="none",
51-
unfurl_links=False,
52-
unfurl_media=False,
53-
link_names=True,
54-
as_user=True,
55-
)
56-
else:
57-
logger.debug(f"Posting {len(messageBlocks)} block message to {channelName} on thread {threadId} in Slack")
58-
response = cls.client.chat_postMessage(
59-
channel=channelName,
60-
thread_ts=threadId,
61-
blocks=messageBlocks,
62-
text="Summary text unavailable",
63-
mrkdwn=True,
64-
parse="none",
65-
unfurl_links=False,
66-
unfurl_media=False,
67-
link_names=True,
68-
as_user=True,
69-
)
70-
71-
if not response["ok"]:
72-
logger.warning(response.data)
73-
logger.warning("Failed to call Slack API")
74-
return response
75-
76-
def postMessageText(cls, channelName, message, attachments=None, threadId=None):
77-
if threadId is None:
78-
logger.debug(f"Posting message to {channelName} in Slack")
79-
response = cls.client.chat_postMessage(
80-
channel=channelName,
81-
text=message,
82-
attachments=attachments,
83-
mrkdwn=True,
84-
parse="none",
85-
unfurl_links=False,
86-
unfurl_media=False,
87-
link_names=True,
88-
as_user=True,
89-
)
90-
else:
91-
logger.debug(f"Posting message to {channelName} on thread {threadId} in Slack")
92-
response = cls.client.chat_postMessage(
93-
channel=channelName,
94-
thread_ts=threadId,
95-
text=message,
96-
attachments=attachments,
97-
mrkdwn=True,
98-
parse="none",
99-
unfurl_links=False,
100-
unfurl_media=False,
101-
link_names=True,
102-
as_user=True,
103-
)
104-
105-
if not response["ok"]:
106-
logger.warning(response.data)
107-
logger.warning("Failed to call Slack API")
108-
return response
42+
def postMessageBlocks(cls, channelList: str | list[str], messageBlocks: list, threadId: str = None) -> SlackResponse | list[SlackResponse]:
43+
responses: list[SlackResponse] = []
44+
45+
if isinstance(channelList, str):
46+
channelList = [channelList]
47+
for channel in channelList:
48+
try:
49+
if threadId is None:
50+
logger.debug(f"Posting {len(messageBlocks)} block message to {channel} in Slack")
51+
response = cls.client.chat_postMessage(
52+
channel=channel,
53+
blocks=messageBlocks,
54+
text="Summary text unavailable",
55+
mrkdwn=True,
56+
parse="none",
57+
unfurl_links=False,
58+
unfurl_media=False,
59+
link_names=True,
60+
as_user=True,
61+
)
62+
else:
63+
logger.debug(f"Posting {len(messageBlocks)} block message to {channel} on thread {threadId} in Slack")
64+
response = cls.client.chat_postMessage(
65+
channel=channel,
66+
thread_ts=threadId,
67+
blocks=messageBlocks,
68+
text="Summary text unavailable",
69+
mrkdwn=True,
70+
parse="none",
71+
unfurl_links=False,
72+
unfurl_media=False,
73+
link_names=True,
74+
as_user=True,
75+
)
76+
77+
if not response["ok"]:
78+
logger.warning(response.data)
79+
logger.warning("Failed to call Slack API")
80+
responses.append(response)
81+
except Exception as e:
82+
logger.error(f"Fail to send a message to {channel}: {e}")
83+
raise
84+
85+
return responses if len(responses) > 1 else responses[0]
86+
87+
def postMessageText(cls, channelList: str | list[str], message: str, attachments=None, threadId: str = None) -> SlackResponse | list[SlackResponse]:
88+
responses: list[SlackResponse] = []
89+
90+
if isinstance(channelList, str):
91+
channelList = [channelList]
92+
93+
for channel in channelList:
94+
if threadId is None:
95+
logger.debug(f"Posting message to {channel} in Slack")
96+
response = cls.client.chat_postMessage(
97+
channel=channel,
98+
text=message,
99+
attachments=attachments,
100+
mrkdwn=True,
101+
parse="none",
102+
unfurl_links=False,
103+
unfurl_media=False,
104+
link_names=True,
105+
as_user=True,
106+
)
107+
else:
108+
logger.debug(f"Posting message to {channel} on thread {threadId} in Slack")
109+
response = cls.client.chat_postMessage(
110+
channel=channel,
111+
thread_ts=threadId,
112+
text=message,
113+
attachments=attachments,
114+
mrkdwn=True,
115+
parse="none",
116+
unfurl_links=False,
117+
unfurl_media=False,
118+
link_names=True,
119+
as_user=True,
120+
)
121+
122+
if not response["ok"]:
123+
logger.warning(response.data)
124+
logger.warning("Failed to call Slack API")
125+
responses.append(response)
126+
127+
return responses if len(responses) > 1 else responses[0]
109128

110129
def createMessagePermalink(
111130
cls, slackResponse: SlackResponse = None, channelId: str = None, messageTimestamp: str = None, domain: str = "ibm-mas"

0 commit comments

Comments
 (0)