Skip to content

Commit 3a735c8

Browse files
committed
update backupResources to allow labels and optional namespace
1 parent 79fb4a0 commit 3a735c8

5 files changed

Lines changed: 620 additions & 15 deletions

File tree

src/mas/devops/backup.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,22 @@ def extract_secrets_from_dict(data, secret_names=None):
111111
return secret_names
112112

113113

114-
def backupResources(dynClient: DynamicClient, namespace: str, kind: str, api_version: str, backup_path: str, name=None) -> tuple:
114+
def backupResources(dynClient: DynamicClient, kind: str, api_version: str, backup_path: str, namespace=None, name=None, labels=None) -> tuple:
115115
"""
116-
Backup resources of a given kind in a namespace.
116+
Backup resources of a given kind.
117117
If name is provided, backs up that specific resource.
118118
If name is None, backs up all resources of that kind.
119+
If namespace is None, backs up cluster-level resources.
120+
If labels is provided, filters resources by label selectors.
119121
120122
Args:
121123
dynClient: Kubernetes dynamic client
122-
namespace: Namespace to backup from
123-
kind: Resource kind (e.g., 'MongoCfg', 'Secret')
124+
kind: Resource kind (e.g., 'MongoCfg', 'Secret', 'ClusterRole')
124125
api_version: API version (e.g., 'config.mas.ibm.com/v1')
125126
backup_path: Path to save backup files
127+
namespace: Optional namespace to backup from (None for cluster-level resources)
126128
name: Optional specific resource name
129+
labels: Optional list of label selectors (e.g., ['app=myapp', 'env=prod'])
127130
128131
Returns:
129132
tuple: (backed_up_count: int, not_found_count: int, failed_count: int, discovered_secrets: set)
@@ -133,28 +136,49 @@ def backupResources(dynClient: DynamicClient, namespace: str, kind: str, api_ver
133136
not_found_count = 0
134137
failed_count = 0
135138

139+
# Build label selector string if labels provided
140+
label_selector = None
141+
if labels:
142+
label_selector = ','.join(labels)
143+
144+
# Determine scope description for logging
145+
scope_desc = f"namespace '{namespace}'" if namespace else "cluster-level"
146+
label_desc = f" with labels [{label_selector}]" if label_selector else ""
147+
136148
try:
137149
resourceAPI = dynClient.resources.get(api_version=api_version, kind=kind)
138150

139151
if name:
140152
# Backup specific named resource
141-
logger.info(f"Backing up {kind} '{name}' from namespace '{namespace}' (API version: {api_version})")
153+
logger.info(f"Backing up {kind} '{name}' from {scope_desc} (API version: {api_version}){label_desc}")
142154
try:
143-
resource = resourceAPI.get(name=name, namespace=namespace)
155+
if namespace:
156+
resource = resourceAPI.get(name=name, namespace=namespace)
157+
else:
158+
resource = resourceAPI.get(name=name)
159+
144160
if resource:
145161
resources_to_process = [resource]
146162
else:
147-
logger.info(f"{kind} '{name}' not found in namespace '{namespace}', skipping backup")
163+
logger.info(f"{kind} '{name}' not found in {scope_desc}, skipping backup")
148164
not_found_count = 1
149165
return (backed_up_count, not_found_count, failed_count, discovered_secrets)
150166
except NotFoundError:
151-
logger.error(f"{kind} '{name}' not found in namespace '{namespace}', skipping backup")
167+
logger.error(f"{kind} '{name}' not found in {scope_desc}, skipping backup")
152168
not_found_count = 1
153169
return (backed_up_count, not_found_count, failed_count, discovered_secrets)
154170
else:
155171
# Backup all resources of this kind
156-
logger.info(f"Backing up all {kind} resources from namespace '{namespace}' (API version: {api_version})")
157-
resources = resourceAPI.get(namespace=namespace)
172+
logger.info(f"Backing up all {kind} resources from {scope_desc} (API version: {api_version}){label_desc}")
173+
174+
# Build get parameters
175+
get_params = {}
176+
if namespace:
177+
get_params['namespace'] = namespace
178+
if label_selector:
179+
get_params['label_selector'] = label_selector
180+
181+
resources = resourceAPI.get(**get_params)
158182
resources_to_process = resources.items
159183

160184
# Process each resource
@@ -170,7 +194,9 @@ def backupResources(dynClient: DynamicClient, namespace: str, kind: str, api_ver
170194
discovered_secrets.update(secrets)
171195

172196
# Backup the resource
173-
resource_file_path = f"{backup_path}/{resource_name}.yaml"
197+
resource_backup_path = f"{backup_path}/resources/{kind.lower()}s"
198+
createBackupDirectories([resource_backup_path])
199+
resource_file_path = f"{resource_backup_path}/{resource_name}.yaml"
174200
filtered_resource = filterResourceData(resource_dict)
175201
if copyContentsToYamlFile(resource_file_path, filtered_resource):
176202
logger.info(f"Successfully backed up {kind} '{resource_name}' to '{resource_file_path}'")
@@ -182,16 +208,16 @@ def backupResources(dynClient: DynamicClient, namespace: str, kind: str, api_ver
182208
if backed_up_count > 0:
183209
logger.info(f"Successfully backed up {backed_up_count} {kind} resource(s)")
184210
elif not name:
185-
logger.info(f"No {kind} resources found in namespace '{namespace}'")
211+
logger.info(f"No {kind} resources found in {scope_desc}{label_desc}")
186212

187213
return (backed_up_count, not_found_count, failed_count, discovered_secrets)
188214

189215
except NotFoundError:
190216
if name:
191-
logger.info(f"{kind} '{name}' not found in namespace '{namespace}'")
217+
logger.info(f"{kind} '{name}' not found in {scope_desc}")
192218
not_found_count = 1
193219
else:
194-
logger.info(f"No {kind} resources found in namespace '{namespace}'")
220+
logger.info(f"No {kind} resources found in {scope_desc}{label_desc}")
195221
return (backed_up_count, not_found_count, failed_count, discovered_secrets)
196222
except Exception as e:
197223
logger.error(f"Error backing up {kind} resources: {e}")

src/mas/devops/mas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
verifyMasInstance,
1414
getMasChannel,
1515
updateIBMEntitlementKey,
16+
getMasPublicClusterIssuer,
1617
)

src/mas/devops/mas/suite.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,47 @@ def updateIBMEntitlementKey(dynClient: DynamicClient, namespace: str, icrUsernam
313313

314314
secret = secretsAPI.apply(body=secret, namespace=namespace)
315315
return secret
316+
317+
318+
def getMasPublicClusterIssuer(dynClient: DynamicClient, instanceId: str) -> str | None:
319+
"""
320+
Retrieve the Public Cluster Issuer for a MAS instance.
321+
322+
This function queries the Suite custom resource and attempts to retrieve the
323+
certificate issuer name from spec.certificateIssuer.name. If the keys don't exist,
324+
it returns the default issuer name.
325+
326+
Args:
327+
dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions.
328+
instanceId (str): The MAS instance identifier to use.
329+
330+
Returns:
331+
str: The name of the cluster issuer used for the passed in MAS Instance.
332+
Returns the default "mas-{instanceId}-core-public-issuer" if the suite
333+
doesn't specify a custom issuer, or None if the suite is not found.
334+
"""
335+
try:
336+
suitesAPI = dynClient.resources.get(api_version="core.mas.ibm.com/v1", kind="Suite")
337+
suite = suitesAPI.get(name=instanceId, namespace=f"mas-{instanceId}-core")
338+
339+
# Check if spec.certificateIssuer.name exists
340+
if hasattr(suite, 'spec') and hasattr(suite.spec, 'certificateIssuer') and hasattr(suite.spec.certificateIssuer, 'name'):
341+
issuerName = suite.spec.certificateIssuer.name
342+
logger.debug(f"Found custom certificate issuer: {issuerName}")
343+
return issuerName
344+
345+
# Keys don't exist, return default
346+
defaultIssuer = f"mas-{instanceId}-core-public-issuer"
347+
logger.debug(f"No custom certificate issuer found, using default: {defaultIssuer}")
348+
return defaultIssuer
349+
350+
except NotFoundError:
351+
logger.warning(f"Suite instance '{instanceId}' not found")
352+
return None
353+
except ResourceNotFoundError:
354+
# The MAS Suite CRD has not even been installed in the cluster
355+
logger.warning("MAS Suite CRD not found in the cluster")
356+
return None
357+
except UnauthorizedError as e:
358+
logger.error(f"Error: Unable to retrieve MAS instance due to failed authorization: {e}")
359+
return None

0 commit comments

Comments
 (0)