Skip to content

Commit 4f9e886

Browse files
Merge branch 'stable' into 1906-automatically-determine-volumebindingmode-of-config-pvc
2 parents f84adef + e93c898 commit 4f9e886

12 files changed

Lines changed: 432 additions & 456 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/ocp.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,68 @@ def execInPod(core_v1_api: client.CoreV1Api, pod_name: str, namespace, command:
356356
logger.debug(f"stdout: \n----------------------------------------------------------------\n{stdout}\n----------------------------------------------------------------\n")
357357

358358
return stdout
359+
360+
361+
def updateGlobalPullSecret(dynClient: DynamicClient, registryUrl: str, username: str, password: str) -> dict:
362+
"""
363+
Update the global pull secret in openshift-config namespace with new registry credentials.
364+
365+
Args:
366+
dynClient: OpenShift Dynamic Client
367+
registryUrl: Registry URL (e.g., "myregistry.com:5000")
368+
username: Registry username
369+
password: Registry password
370+
371+
Returns:
372+
dict: Updated secret information
373+
"""
374+
import json
375+
import base64
376+
377+
logger.info(f"Updating global pull secret with credentials for {registryUrl}")
378+
379+
# Get the existing pull secret
380+
secretsAPI = dynClient.resources.get(api_version="v1", kind="Secret")
381+
try:
382+
pullSecret = secretsAPI.get(name="pull-secret", namespace="openshift-config")
383+
except NotFoundError:
384+
raise Exception("Global pull-secret not found in openshift-config namespace")
385+
386+
# Convert to dict to allow modifications
387+
secretDict = pullSecret.to_dict()
388+
389+
# Decode the existing dockerconfigjson
390+
dockerConfigJson = secretDict['data'].get(".dockerconfigjson", "")
391+
dockerConfig = json.loads(base64.b64decode(dockerConfigJson).decode('utf-8'))
392+
393+
# Create auth string (username:password base64 encoded)
394+
authString = base64.b64encode(f"{username}:{password}".encode('utf-8')).decode('utf-8')
395+
396+
# Add or update the registry credentials
397+
if "auths" not in dockerConfig:
398+
dockerConfig["auths"] = {}
399+
400+
dockerConfig["auths"][registryUrl] = {
401+
"username": username,
402+
"password": password,
403+
"email": username,
404+
"auth": authString
405+
}
406+
407+
# Encode back to base64
408+
updatedDockerConfig = base64.b64encode(json.dumps(dockerConfig).encode('utf-8')).decode('utf-8')
409+
410+
# Update the secret dict
411+
secretDict['data'][".dockerconfigjson"] = updatedDockerConfig
412+
413+
# Apply the updated secret
414+
updatedSecret = secretsAPI.apply(body=secretDict, namespace="openshift-config")
415+
416+
logger.info(f"Successfully updated global pull secret with credentials for {registryUrl}")
417+
418+
return {
419+
"name": updatedSecret.metadata.name,
420+
"namespace": updatedSecret.metadata.namespace,
421+
"registry": registryUrl,
422+
"changed": True
423+
}

0 commit comments

Comments
 (0)