From 14fd9f0a0638bd5090ad293a50f4e3b535ddf20b Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Fri, 22 May 2026 12:43:40 +0530 Subject: [PATCH 01/13] [patch] add detect permission mode function for pre-upgrade checks --- src/mas/devops/mas/__init__.py | 1 + src/mas/devops/mas/suite.py | 90 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/mas/devops/mas/__init__.py b/src/mas/devops/mas/__init__.py index 333f109f..6e83a342 100644 --- a/src/mas/devops/mas/__init__.py +++ b/src/mas/devops/mas/__init__.py @@ -14,4 +14,5 @@ getMasChannel, updateIBMEntitlementKey, getMasPublicClusterIssuer, + getPermissionMode, ) diff --git a/src/mas/devops/mas/suite.py b/src/mas/devops/mas/suite.py index 01020be8..0042257a 100644 --- a/src/mas/devops/mas/suite.py +++ b/src/mas/devops/mas/suite.py @@ -357,3 +357,93 @@ def getMasPublicClusterIssuer(dynClient: DynamicClient, instanceId: str) -> str except UnauthorizedError as e: logger.error(f"Error: Unable to retrieve MAS instance due to failed authorization: {e}") return None + + +def getPermissionMode(dynClient: DynamicClient, instanceId: str) -> str | None: + """ + Detect the current RBAC permission mode for a MAS instance. + + This function determines whether MAS is installed with cluster-level permissions, + namespace-scoped permissions (essential + non-essential), or minimal essential-only + permissions by checking for the existence of RBAC resources in the cluster. + + RBAC Resource Distribution: + - Cluster mode: ClusterRoles + Essential Roles + - Namespaced mode: Essential Roles + Non-essential Roles + - Minimal mode: Essential Roles ONLY + + Detection Logic: + 1. Check for ClusterRoles → cluster mode + 2. Check for non-essential openshift-marketplace Role → namespaced mode + 3. No ClusterRole and no openshift-marketplace Role → minimal mode + + Args: + dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. + instanceId (str): The MAS instance identifier. + + Returns: + str: Permission mode - "cluster", "namespaced", or "minimal" + Returns None if unable to determine (e.g., no RBAC resources found) + """ + try: + # Step 1: Check for ClusterRoles (indicates cluster mode) + clusterRoleAPI = dynClient.resources.get(api_version="rbac.authorization.k8s.io/v1", kind="ClusterRole") + + # Look for MAS ClusterRoles with the instance ID pattern + clusterRoleName = f"mas:{instanceId}:core:coreapi" + try: + clusterRoleAPI.get(name=clusterRoleName) + logger.info(f"Found ClusterRole '{clusterRoleName}' - permission mode is 'cluster'") + return "cluster" + except NotFoundError: + logger.debug(f"ClusterRole '{clusterRoleName}' not found, checking for non-essential Roles") + + # Step 2: Check for non-essential openshift-marketplace Role (only exists in namespaced mode) + roleAPI = dynClient.resources.get(api_version="rbac.authorization.k8s.io/v1", kind="Role") + + # This role only exists in namespaced mode (applied via role-non-essential-core-coreapi-openshift-marketplace.yaml) + marketplaceRoleName = f"mas:{instanceId}:core:coreapi:openshift-marketplace" + marketplaceNamespace = "openshift-marketplace" + + try: + roleAPI.get(name=marketplaceRoleName, namespace=marketplaceNamespace) + logger.info(f"Found non-essential Role '{marketplaceRoleName}' in namespace '{marketplaceNamespace}' - permission mode is 'namespaced'") + return "namespaced" + except NotFoundError: + logger.debug("Non-essential openshift-marketplace Role not found, checking for essential roles") + + # Step 3: Verify minimal mode by checking for essential roles in mas-{instanceId}-core namespace + # Essential roles have pattern: mas:{instanceId}:core:suite:{app}:essential + coreNamespace = f"mas-{instanceId}-core" + + # Try to find at least one essential role to confirm minimal mode + # Check common apps that might be installed + essentialRolePatterns = [ + f"mas:{instanceId}:core:suite:manage:essential", + f"mas:{instanceId}:core:suite:iot:essential", + f"mas:{instanceId}:core:suite:monitor:essential", + f"mas:{instanceId}:core:suite:predict:essential", + f"mas:{instanceId}:core:suite:arcgis:essential", + f"mas:{instanceId}:core:suite:facilities:essential", + f"mas:{instanceId}:core:suite:optimizer:essential", + f"mas:{instanceId}:core:suite:visualinspection:essential" + ] + + for essentialRoleName in essentialRolePatterns: + try: + roleAPI.get(name=essentialRoleName, namespace=coreNamespace) + logger.info(f"Found essential Role '{essentialRoleName}' in namespace '{coreNamespace}' with no non-essential roles - permission mode is 'minimal'") + return "minimal" + except NotFoundError: + continue + + # If we couldn't find any RBAC resources, return None + logger.warning(f"Unable to determine permission mode for instance '{instanceId}' ") + return None + + except ResourceNotFoundError: + logger.warning("Required API resources not found in the cluster") + return None + except UnauthorizedError as e: + logger.error(f"Error: Unable to check permissions due to failed authorization: {e}") + return None From 0cba3f1edc107c62f94c404628d49ea7ac60dce8 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Wed, 27 May 2026 10:50:13 +0530 Subject: [PATCH 02/13] [patch] add rbac helper functions --- src/mas/devops/mas/apps.py | 35 ++++++++++++++++++ src/mas/devops/pre_install.py | 68 +++++++++++++++++++++++++++++++++++ src/mas/devops/utils.py | 60 +++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/mas/devops/mas/apps.py b/src/mas/devops/mas/apps.py index 8483ea7d..aab1cd71 100644 --- a/src/mas/devops/mas/apps.py +++ b/src/mas/devops/mas/apps.py @@ -216,3 +216,38 @@ def getAppsSubscriptionChannel(dynClient: DynamicClient, instanceId: str) -> lis except UnauthorizedError: logger.error("Error: Unable to get MAS app subscriptions due to failed authorization: {e}") return [] + + +def getInstalledAppsForRBAC(dynClient: DynamicClient, instanceId: str) -> list: + """ + Get list of installed apps for the given MAS instance for RBAC application. + Always includes 'core' since core RBAC is required. + + This is a convenience wrapper around getAppsSubscriptionChannel() that: + 1. Always includes 'core' in the list + 2. Extracts just the appId from the subscription data + 3. Handles errors gracefully + + Args: + dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. + instanceId (str): The MAS instance identifier. + + Returns: + list: List of app IDs including 'core' (e.g., ['core', 'manage', 'iot']) + """ + # Always include core for RBAC application + installedApps = ["core"] + + try: + appsWithSubscriptions = getAppsSubscriptionChannel(dynClient, instanceId) + logger.info(f"Apps with subscriptions detected for {instanceId}: {[app.get('appId') for app in appsWithSubscriptions]}") + + for app in appsWithSubscriptions: + appId = app.get("appId") + if appId: + installedApps.append(appId) + except Exception as e: + logger.warning(f"Could not query app subscriptions for {instanceId}: {e}") + # Return at least core if we can't query apps + + return installedApps diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index dd033a28..a4cb7b58 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -222,6 +222,74 @@ def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]: ] +def shouldApplyPreInstallRBAC( + dynClient: DynamicClient, + targetVersion: str, + permissionMode: str | None = None, + skipPreinstallRbac: bool = False +) -> bool: + """ + Evaluate if pre-install RBAC should be applied based on target version and user permissions. + + This function is used across install, update, and upgrade operations to determine + if RBAC resources should be applied before launching the pipeline. It will raise + an exception if permissions are missing and RBAC is required. + + Args: + dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. + targetVersion (str): Target MAS version (e.g., "9.2.0", "9.2.x", "9.2.0-pre.stable"). + permissionMode (str | None): Permission mode ("cluster", "namespaced", "minimal"). + If "minimal", RBAC is skipped. + skipPreinstallRbac (bool): If True, skip RBAC application (for install --skip-preinstall-rbac). + + Returns: + bool: True if RBAC should be applied, False if it should be skipped. + + Raises: + PermissionError: If user lacks required permissions and RBAC is needed. + """ + from .utils import isVersionEqualOrAfter + + # Extract base version for comparison + baseVersion = targetVersion.split("-")[0].replace(".x", ".0") if targetVersion else "" + + # Only apply for MAS >= 9.2.0 + if not baseVersion or not isVersionEqualOrAfter("9.2.0", baseVersion): + logger.info(f"Target version {targetVersion} is < 9.2.0, skipping pre-install RBAC") + return False + + # Skip for minimal mode - operator will apply essential roles + if permissionMode == "minimal": + logger.info("Minimal permission mode detected, skipping pre-install RBAC") + return False + + # Skip if explicitly requested (install --skip-preinstall-rbac) + if skipPreinstallRbac: + logger.info("Skipping pre-install RBAC as requested by --skip-preinstall-rbac flag") + return False + + # Check if user has cluster-admin permissions + permissionResults = permissionCheckForRBAC(dynClient) + hasPreInstallRBACAccess = all(result["allowed"] for result in permissionResults) + + if hasPreInstallRBACAccess: + logger.info(f"User has required permissions for pre-install RBAC (target version: {targetVersion})") + return True + + # No permissions - this is a blocking error + errorMsg = ( + f"Current user does not have cluster-admin permissions required to apply pre-install RBAC for MAS {targetVersion}. " + f"Permission mode '{permissionMode or 'cluster'}' requires the following permissions:\n" + ) + for result in permissionResults: + if not result["allowed"]: + errorMsg += f" - {result['verb']} {result['resource']} (group: {result.get('group', 'core')})\n" + + errorMsg += "\nPlease contact your OpenShift cluster administrator to apply the required RBAC, or use --skip-preinstall-rbac if RBAC was already applied." + + raise PermissionError(errorMsg) + + def permissionCheckForRBAC( dynClient: DynamicClient, checks: list[dict[str, str]] | None = None diff --git a/src/mas/devops/utils.py b/src/mas/devops/utils.py index 6ac821e4..3868a53b 100644 --- a/src/mas/devops/utils.py +++ b/src/mas/devops/utils.py @@ -74,3 +74,63 @@ def isVersionEqualOrAfter(_compare_to_version, _current_version): current_version = semver.VersionInfo.parse(strippedVersion) compareToVersion = semver.VersionInfo.parse(_compare_to_version) return current_version.compare(compareToVersion) >= 0 + + +def extractBaseVersion(version: str) -> str: + """ + Extract base version (major.minor) from a version string. + + This function normalizes version strings by removing pre-release identifiers, + build metadata, and patch versions to return just the major.minor version. + This is used for RBAC resource selection, as RBAC resources are organized + by major.minor version (e.g., 9.2/, 9.3/). + + Examples: + "9.2.0" -> "9.2" + "9.2.0-pre.stable+21734" -> "9.2" + "9.2.x-dev" -> "9.2" + "9.2.x" -> "9.2" + + Args: + version (str): Version string to parse. + + Returns: + str: Base version (major.minor), or empty string if version is None/empty. + """ + if not version: + return "" + + # Remove pre-release metadata (everything after first "-") + # Then remove build metadata (everything after first "+") + # Example: "9.2.0-pre.stable+21734" -> "9.2.0" + baseVersion = version.split("-")[0].split("+")[0] + + # Remove .x suffix if present + if '.x' in baseVersion: + baseVersion = baseVersion.replace(".x", "") + + # Extract major.minor (first two parts) + # Split "9.2.0" by "." -> ["9", "2", "0"] + # Take first two: "9" and "2" -> "9.2" + parts = baseVersion.split(".") + if len(parts) >= 2: + return f"{parts[0]}.{parts[1]}" + + return baseVersion + + +def isPreReleaseVersion(version: str) -> bool: + """ + Check if a version string represents a pre-release version. + + Pre-release versions contain "-pre" in the version string, such as + "9.2.0-pre.stable+21734" or "9.2.0-pre.m1dev86". + + Args: + version (str): Version string to check (e.g., "9.2.0-pre.stable+21734"). + + Returns: + bool: True if the version is a pre-release, False otherwise. + Returns False if version is None or empty. + """ + return "-pre" in version if version else False From 01d7aae3823978fa91d35bbf2465066e6d3d5362 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Wed, 27 May 2026 17:29:52 +0530 Subject: [PATCH 03/13] [patch] add test catalog for testing 9.2.0-pre.stable to 9.2.0-pre.dev update --- .../data/catalogs/v9-test-predev-amd64.yaml | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml diff --git a/src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml b/src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml new file mode 100644 index 00000000..c34878d7 --- /dev/null +++ b/src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml @@ -0,0 +1,134 @@ +--- +# Test catalog for RBAC testing with 9.2.0-pre.dev +# This catalog simulates a pre-release to pre-release transition for testing +# ----------------------------------------------------------------------------- + +catalog_digest: sha256:test_rbac_predev_catalog + +ocp_compatibility: +- "4.16" +- "4.17" +- "4.18" +- "4.19" +- "4.20" +- "4.21" + +# Dependencies (copied from v9-260527-amd64.yaml) +# ----------------------------------------------------------------------------- +ibm_licensing_version: 4.2.17 +common_svcs_version: 4.13.0 +common_svcs_version_1: 4.11.0 + +cp4d_platform_version: 5.2.0+20250709.170324 +ibm_zen_version: 6.2.0+20250530.152516.232 + +db2u_version: 7.3.1+20250821.161005.16793 +db2_channel_default: v110509.0 +events_version: 5.0.1 +uds_version: 2.0.12 +sls_version: 3.12.8 +tsm_version: 1.7.6 +dd_version: 1.1.23 +appconnect_version: 6.2.0 +wsl_version: 11.0.0+20250521.202913.73 +wsl_runtimes_version: 11.0.0+20250515.090949.21 +wml_version: 11.0.0+20250530.193146.282 +postgress_version: 5.16.0+20250827.110911.2626 + +ccs_build: 11.0.0+20250605.130237.468 +datarefinery_version: 11.0.0+20250513.203727.232 + +spark_version: 11.0.0+20250604.163055.2097 +cognos_version: 28.0.0+20250515.175459.10054 +couchdb_version: 1.0.13 +elasticsearch_version: 1.1.2667 +opensearch_version: 1.1.2494 + +# Dependencies - Opendata hub +# ----------------------------------------------------------------------------- +odh_version: 2.32.0 + +# Maximo Application Suite - TEST VERSIONS +# ----------------------------------------------------------------------------- +# Using 9.2.0-pre.dev for testing RBAC logic +mas_core_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version for testing + 9.1.x: 9.1.16 + 9.0.x: 9.0.24 + 8.10.x: 8.10.37 + 8.11.x: 8.11.34 + +mas_assist_version: + 9.1.x: 9.1.10 + 9.0.x: 9.0.16 + 8.10.x: 8.7.8 + 8.11.x: 8.8.7 + +mas_hputilities_version: + 9.1.x: "" + 9.0.x: "" + 8.10.x: 8.6.7 + 8.11.x: "" + +mas_iot_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.10 + 9.0.x: 9.0.19 + 8.10.x: 8.7.33 + 8.11.x: 8.8.30 + +mas_manage_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.17 + 9.0.x: 9.0.21 + 8.10.x: 8.7.28 + 8.11.x: 8.8.25 + +mas_monitor_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.10 + 9.0.x: 9.0.16 + 8.10.x: 8.9.14 + 8.11.x: 8.10.11 + +mas_optimizer_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.11 + 9.0.x: 9.0.14 + 8.10.x: 8.5.14 + 8.11.x: 8.6.11 + +mas_predict_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.7 + 9.0.x: 9.0.11 + 8.10.x: 8.10.7 + 8.11.x: 8.11.4 + +mas_visualinspection_version: + 9.2.x-feature: 9.2.0-pre.stable_12598 + 9.1.x: 9.1.12 + 9.0.x: 9.0.13 + 8.10.x: 8.10.7 + 8.11.x: 8.11.4 + +mas_facilities_version: + 9.2.x-feature: 9.2.0-pre.stable_16853 + 9.1.x: 9.1.10 + 9.0.x: "" + 8.10.x: "" + 8.11.x: "" + +# AI Service +# ----------------------------------------------------------------------------- +aiservice_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + 9.1.x: 9.1.14 + 9.0.x: 9.0.17 + 8.10.x: "" + 8.11.x: "" + +aiservice_tenant_version: + 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version + +# Made with Bob From a27ff8767632b44f00a1c372dd93d8b122ae350f Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Fri, 29 May 2026 08:04:11 +0530 Subject: [PATCH 04/13] [patch] rename test-predev catalog file --- ...v9-test-predev-amd64.yaml => v9-000000-test-predev-amd64.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/mas/devops/data/catalogs/{v9-test-predev-amd64.yaml => v9-000000-test-predev-amd64.yaml} (100%) diff --git a/src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml b/src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml similarity index 100% rename from src/mas/devops/data/catalogs/v9-test-predev-amd64.yaml rename to src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml From 6e4adfe8914b3ab08e7e4eb9badb2d910bf13d27 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Fri, 29 May 2026 08:29:38 +0530 Subject: [PATCH 05/13] fix import error for getInstalledAppsForRBAC --- src/mas/devops/mas/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mas/devops/mas/__init__.py b/src/mas/devops/mas/__init__.py index 6e83a342..a58e4d46 100644 --- a/src/mas/devops/mas/__init__.py +++ b/src/mas/devops/mas/__init__.py @@ -1,7 +1,8 @@ from .apps import ( # noqa: F401 verifyAppInstance, getAppsSubscriptionChannel, - waitForAppReady + waitForAppReady, + getInstalledAppsForRBAC ) from .suite import ( # noqa: F401 From 8955ab1ced8c1bb3c8115a3adf91a44bc2123197 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Fri, 29 May 2026 18:08:21 +0530 Subject: [PATCH 06/13] [patch] renamed shouldApplyPreInstallRBAC function --- src/mas/devops/pre_install.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index a4cb7b58..04c776d5 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -222,18 +222,18 @@ def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]: ] -def shouldApplyPreInstallRBAC( +def requiresPreInstallRBAC( dynClient: DynamicClient, targetVersion: str, permissionMode: str | None = None, skipPreinstallRbac: bool = False ) -> bool: """ - Evaluate if pre-install RBAC should be applied based on target version and user permissions. + Determine if pre-install RBAC is required and can be applied for the target version. This function is used across install, update, and upgrade operations to determine - if RBAC resources should be applied before launching the pipeline. It will raise - an exception if permissions are missing and RBAC is required. + if RBAC resources are required and should be applied before launching the pipeline. + It will raise an exception if permissions are missing and RBAC is required. Args: dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. From 00472ee16335b5d53b554d427b590204b54de2e8 Mon Sep 17 00:00:00 2001 From: Dixit Sathwara Date: Sat, 30 May 2026 15:39:09 +0530 Subject: [PATCH 07/13] [patch] update the permissionMode to adminMode --- src/mas/devops/pre_install.py | 38 ++++++++++++++++------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index dd033a28..f181d92a 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -66,7 +66,7 @@ def _get_selected_operator_dirs(selectedApps: set[str]) -> set[str]: return {appToOperatorDir[app] for app in selectedApps} -def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) -> bool: +def _should_apply_preinstall_mas_rbac_file(fileName: str, adminMode: str) -> bool: lowerName = path.basename(fileName).lower() if lowerName == "kustomization.yaml": @@ -75,10 +75,10 @@ def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) - if not (lowerName.endswith(".yml") or lowerName.endswith(".yaml")): return False - if permissionMode == "cluster": + if adminMode == "cluster": return lowerName.startswith("cluster-role-") - if permissionMode == "namespaced": + if adminMode == "namespaced": return lowerName.startswith("role-non-essential-") return False @@ -87,7 +87,7 @@ def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) - def _collect_preinstall_mas_rbac_files_from_source( sourceOperatorsRoot: str, masVersion: str, - permissionMode: str, + adminMode: str, operatorNames: set[str] | None = None ) -> list[str]: if not path.isdir(sourceOperatorsRoot): @@ -117,7 +117,7 @@ def _collect_preinstall_mas_rbac_files_from_source( if not path.isfile(manifestFile): continue - if _should_apply_preinstall_mas_rbac_file(manifestName, permissionMode): + if _should_apply_preinstall_mas_rbac_file(manifestName, adminMode): manifestFiles.append(manifestFile) return manifestFiles @@ -126,7 +126,7 @@ def _collect_preinstall_mas_rbac_files_from_source( def _discover_preinstall_mas_rbac_files( rbacRootDir: str | None, masVersion: str, - permissionMode: str, + adminMode: str, selectedApps: set[str] ) -> list[str]: if not rbacRootDir: @@ -151,7 +151,7 @@ def _discover_preinstall_mas_rbac_files( _collect_preinstall_mas_rbac_files_from_source( sourceOperatorsRoot=sourceRoot, masVersion=masVersion, - permissionMode=permissionMode, + adminMode=adminMode, operatorNames=operatorNames ) ) @@ -159,13 +159,9 @@ def _discover_preinstall_mas_rbac_files( return list(dict.fromkeys(manifestFiles)) -def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]: +def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, adminMode: str, selectedApps: set[str]) -> set[str]: - # Due to ingresscontroller role we need to apply the preinstall RBAC for the minimal permission mode - # if permissionMode == "minimal": - # return set() - - if permissionMode == "cluster": + if adminMode == "cluster": return set() namespaces = {f"mas-{masInstanceId}-core"} @@ -264,7 +260,7 @@ def applyPreInstallMASRBAC( dynClient: DynamicClient, masVersion: str, masInstanceId: str, - permissionMode: str, + adminMode: str, selectedApps: list[str] | None = None, rbacRootDir: str | None = None ) -> None: @@ -272,14 +268,14 @@ def applyPreInstallMASRBAC( rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT # Minimal mode - essential roles will be applied by each operator - if permissionMode == "minimal": - logger.info("Minimal permission mode - essential roles will be applied by each operator") + if adminMode == "minimal": + logger.info("Minimal admin mode - essential roles will be applied by each operator") return # For cluster mode, use ibm-mas operator only (apps not required) - if permissionMode == "cluster": + if adminMode == "cluster": validatedApps = {"core"} # Use core which maps to ibm-mas operator - logger.info("Cluster permission mode - using ibm-mas operator only") + logger.info("Cluster admin mode - using ibm-mas operator only") else: # For namespaced mode, validate and use selected apps validatedApps = _validate_selected_apps(selectedApps) @@ -290,13 +286,13 @@ def applyPreInstallMASRBAC( manifestFiles = _discover_preinstall_mas_rbac_files( rbacRootDir=rbacRootDir, masVersion=masVersion, - permissionMode=permissionMode, + adminMode=adminMode, selectedApps=validatedApps ) logger.info( f"Applying pre-install MAS RBAC from {rbacRootDir} for MAS {masVersion}, " - f"masInstanceId={masInstanceId}, permissionMode={permissionMode}, " + f"masInstanceId={masInstanceId}, adminMode={adminMode}, " f"selectedApps={sorted(validatedApps)}, " f"manifestCount={len(manifestFiles)}" ) @@ -308,7 +304,7 @@ def applyPreInstallMASRBAC( namespaceAPI = dynClient.resources.get(api_version="v1", kind="Namespace") requiredNamespaces = _get_preinstall_mas_rbac_namespaces( masInstanceId=masInstanceId, - permissionMode=permissionMode, + adminMode=adminMode, selectedApps=validatedApps ) From 47c3a1440fdfc6ad0fc74b1bbb67a92322368ee1 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Mon, 1 Jun 2026 10:52:16 +0530 Subject: [PATCH 08/13] [patch] update functions to remove references of skip-pre-install flag --- src/mas/devops/pre_install.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index d41e77a6..7a1c14e4 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -221,8 +221,7 @@ def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]: def requiresPreInstallRBAC( dynClient: DynamicClient, targetVersion: str, - permissionMode: str | None = None, - skipPreinstallRbac: bool = False + adminMode: str | None = None ) -> bool: """ Determine if pre-install RBAC is required and can be applied for the target version. @@ -234,9 +233,8 @@ def requiresPreInstallRBAC( Args: dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. targetVersion (str): Target MAS version (e.g., "9.2.0", "9.2.x", "9.2.0-pre.stable"). - permissionMode (str | None): Permission mode ("cluster", "namespaced", "minimal"). - If "minimal", RBAC is skipped. - skipPreinstallRbac (bool): If True, skip RBAC application (for install --skip-preinstall-rbac). + adminMode (str | None): Admin mode ("cluster", "namespaced", "minimal"). + If "minimal", RBAC is skipped. Returns: bool: True if RBAC should be applied, False if it should be skipped. @@ -255,13 +253,8 @@ def requiresPreInstallRBAC( return False # Skip for minimal mode - operator will apply essential roles - if permissionMode == "minimal": - logger.info("Minimal permission mode detected, skipping pre-install RBAC") - return False - - # Skip if explicitly requested (install --skip-preinstall-rbac) - if skipPreinstallRbac: - logger.info("Skipping pre-install RBAC as requested by --skip-preinstall-rbac flag") + if adminMode == "minimal": + logger.info("Minimal admin mode detected, skipping pre-install RBAC") return False # Check if user has cluster-admin permissions @@ -275,13 +268,13 @@ def requiresPreInstallRBAC( # No permissions - this is a blocking error errorMsg = ( f"Current user does not have cluster-admin permissions required to apply pre-install RBAC for MAS {targetVersion}. " - f"Permission mode '{permissionMode or 'cluster'}' requires the following permissions:\n" + f"Admin mode '{adminMode or 'cluster'}' requires the following permissions:\n" ) for result in permissionResults: if not result["allowed"]: errorMsg += f" - {result['verb']} {result['resource']} (group: {result.get('group', 'core')})\n" - errorMsg += "\nPlease contact your OpenShift cluster administrator to apply the required RBAC, or use --skip-preinstall-rbac if RBAC was already applied." + errorMsg += "\nPlease contact your OpenShift cluster administrator to apply the required RBAC." raise PermissionError(errorMsg) From 096f59f4f788d1e41bbe722d11bab2f5ef17074f Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Tue, 2 Jun 2026 13:46:16 +0530 Subject: [PATCH 09/13] [patch] remove functions which are not needed now --- src/mas/devops/pre_install.py | 61 ----------------------------------- 1 file changed, 61 deletions(-) diff --git a/src/mas/devops/pre_install.py b/src/mas/devops/pre_install.py index 7a1c14e4..f181d92a 100644 --- a/src/mas/devops/pre_install.py +++ b/src/mas/devops/pre_install.py @@ -218,67 +218,6 @@ def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]: ] -def requiresPreInstallRBAC( - dynClient: DynamicClient, - targetVersion: str, - adminMode: str | None = None -) -> bool: - """ - Determine if pre-install RBAC is required and can be applied for the target version. - - This function is used across install, update, and upgrade operations to determine - if RBAC resources are required and should be applied before launching the pipeline. - It will raise an exception if permissions are missing and RBAC is required. - - Args: - dynClient (DynamicClient): OpenShift dynamic client for cluster API interactions. - targetVersion (str): Target MAS version (e.g., "9.2.0", "9.2.x", "9.2.0-pre.stable"). - adminMode (str | None): Admin mode ("cluster", "namespaced", "minimal"). - If "minimal", RBAC is skipped. - - Returns: - bool: True if RBAC should be applied, False if it should be skipped. - - Raises: - PermissionError: If user lacks required permissions and RBAC is needed. - """ - from .utils import isVersionEqualOrAfter - - # Extract base version for comparison - baseVersion = targetVersion.split("-")[0].replace(".x", ".0") if targetVersion else "" - - # Only apply for MAS >= 9.2.0 - if not baseVersion or not isVersionEqualOrAfter("9.2.0", baseVersion): - logger.info(f"Target version {targetVersion} is < 9.2.0, skipping pre-install RBAC") - return False - - # Skip for minimal mode - operator will apply essential roles - if adminMode == "minimal": - logger.info("Minimal admin mode detected, skipping pre-install RBAC") - return False - - # Check if user has cluster-admin permissions - permissionResults = permissionCheckForRBAC(dynClient) - hasPreInstallRBACAccess = all(result["allowed"] for result in permissionResults) - - if hasPreInstallRBACAccess: - logger.info(f"User has required permissions for pre-install RBAC (target version: {targetVersion})") - return True - - # No permissions - this is a blocking error - errorMsg = ( - f"Current user does not have cluster-admin permissions required to apply pre-install RBAC for MAS {targetVersion}. " - f"Admin mode '{adminMode or 'cluster'}' requires the following permissions:\n" - ) - for result in permissionResults: - if not result["allowed"]: - errorMsg += f" - {result['verb']} {result['resource']} (group: {result.get('group', 'core')})\n" - - errorMsg += "\nPlease contact your OpenShift cluster administrator to apply the required RBAC." - - raise PermissionError(errorMsg) - - def permissionCheckForRBAC( dynClient: DynamicClient, checks: list[dict[str, str]] | None = None From 8c09807d6df5095b5d9d3e41dc7bdbdbe0ffc288 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Wed, 3 Jun 2026 17:28:54 +0530 Subject: [PATCH 10/13] [patch] remove extractversion wrapper as not needed --- src/mas/devops/utils.py | 43 ----------------------------------------- 1 file changed, 43 deletions(-) diff --git a/src/mas/devops/utils.py b/src/mas/devops/utils.py index 3868a53b..2f21c563 100644 --- a/src/mas/devops/utils.py +++ b/src/mas/devops/utils.py @@ -76,49 +76,6 @@ def isVersionEqualOrAfter(_compare_to_version, _current_version): return current_version.compare(compareToVersion) >= 0 -def extractBaseVersion(version: str) -> str: - """ - Extract base version (major.minor) from a version string. - - This function normalizes version strings by removing pre-release identifiers, - build metadata, and patch versions to return just the major.minor version. - This is used for RBAC resource selection, as RBAC resources are organized - by major.minor version (e.g., 9.2/, 9.3/). - - Examples: - "9.2.0" -> "9.2" - "9.2.0-pre.stable+21734" -> "9.2" - "9.2.x-dev" -> "9.2" - "9.2.x" -> "9.2" - - Args: - version (str): Version string to parse. - - Returns: - str: Base version (major.minor), or empty string if version is None/empty. - """ - if not version: - return "" - - # Remove pre-release metadata (everything after first "-") - # Then remove build metadata (everything after first "+") - # Example: "9.2.0-pre.stable+21734" -> "9.2.0" - baseVersion = version.split("-")[0].split("+")[0] - - # Remove .x suffix if present - if '.x' in baseVersion: - baseVersion = baseVersion.replace(".x", "") - - # Extract major.minor (first two parts) - # Split "9.2.0" by "." -> ["9", "2", "0"] - # Take first two: "9" and "2" -> "9.2" - parts = baseVersion.split(".") - if len(parts) >= 2: - return f"{parts[0]}.{parts[1]}" - - return baseVersion - - def isPreReleaseVersion(version: str) -> bool: """ Check if a version string represents a pre-release version. From 535c2c71db08eddea9aa2311318a5a8fd43cab20 Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Wed, 3 Jun 2026 17:45:53 +0530 Subject: [PATCH 11/13] [patch] Remove test 00000-test-predev file --- .../catalogs/v9-000000-test-predev-amd64.yaml | 134 ------------------ 1 file changed, 134 deletions(-) delete mode 100644 src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml diff --git a/src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml b/src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml deleted file mode 100644 index c34878d7..00000000 --- a/src/mas/devops/data/catalogs/v9-000000-test-predev-amd64.yaml +++ /dev/null @@ -1,134 +0,0 @@ ---- -# Test catalog for RBAC testing with 9.2.0-pre.dev -# This catalog simulates a pre-release to pre-release transition for testing -# ----------------------------------------------------------------------------- - -catalog_digest: sha256:test_rbac_predev_catalog - -ocp_compatibility: -- "4.16" -- "4.17" -- "4.18" -- "4.19" -- "4.20" -- "4.21" - -# Dependencies (copied from v9-260527-amd64.yaml) -# ----------------------------------------------------------------------------- -ibm_licensing_version: 4.2.17 -common_svcs_version: 4.13.0 -common_svcs_version_1: 4.11.0 - -cp4d_platform_version: 5.2.0+20250709.170324 -ibm_zen_version: 6.2.0+20250530.152516.232 - -db2u_version: 7.3.1+20250821.161005.16793 -db2_channel_default: v110509.0 -events_version: 5.0.1 -uds_version: 2.0.12 -sls_version: 3.12.8 -tsm_version: 1.7.6 -dd_version: 1.1.23 -appconnect_version: 6.2.0 -wsl_version: 11.0.0+20250521.202913.73 -wsl_runtimes_version: 11.0.0+20250515.090949.21 -wml_version: 11.0.0+20250530.193146.282 -postgress_version: 5.16.0+20250827.110911.2626 - -ccs_build: 11.0.0+20250605.130237.468 -datarefinery_version: 11.0.0+20250513.203727.232 - -spark_version: 11.0.0+20250604.163055.2097 -cognos_version: 28.0.0+20250515.175459.10054 -couchdb_version: 1.0.13 -elasticsearch_version: 1.1.2667 -opensearch_version: 1.1.2494 - -# Dependencies - Opendata hub -# ----------------------------------------------------------------------------- -odh_version: 2.32.0 - -# Maximo Application Suite - TEST VERSIONS -# ----------------------------------------------------------------------------- -# Using 9.2.0-pre.dev for testing RBAC logic -mas_core_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version for testing - 9.1.x: 9.1.16 - 9.0.x: 9.0.24 - 8.10.x: 8.10.37 - 8.11.x: 8.11.34 - -mas_assist_version: - 9.1.x: 9.1.10 - 9.0.x: 9.0.16 - 8.10.x: 8.7.8 - 8.11.x: 8.8.7 - -mas_hputilities_version: - 9.1.x: "" - 9.0.x: "" - 8.10.x: 8.6.7 - 8.11.x: "" - -mas_iot_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.10 - 9.0.x: 9.0.19 - 8.10.x: 8.7.33 - 8.11.x: 8.8.30 - -mas_manage_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.17 - 9.0.x: 9.0.21 - 8.10.x: 8.7.28 - 8.11.x: 8.8.25 - -mas_monitor_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.10 - 9.0.x: 9.0.16 - 8.10.x: 8.9.14 - 8.11.x: 8.10.11 - -mas_optimizer_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.11 - 9.0.x: 9.0.14 - 8.10.x: 8.5.14 - 8.11.x: 8.6.11 - -mas_predict_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.7 - 9.0.x: 9.0.11 - 8.10.x: 8.10.7 - 8.11.x: 8.11.4 - -mas_visualinspection_version: - 9.2.x-feature: 9.2.0-pre.stable_12598 - 9.1.x: 9.1.12 - 9.0.x: 9.0.13 - 8.10.x: 8.10.7 - 8.11.x: 8.11.4 - -mas_facilities_version: - 9.2.x-feature: 9.2.0-pre.stable_16853 - 9.1.x: 9.1.10 - 9.0.x: "" - 8.10.x: "" - 8.11.x: "" - -# AI Service -# ----------------------------------------------------------------------------- -aiservice_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - 9.1.x: 9.1.14 - 9.0.x: 9.0.17 - 8.10.x: "" - 8.11.x: "" - -aiservice_tenant_version: - 9.2.x-feature: 9.2.0-pre.dev # TEST: Pre-release version - -# Made with Bob From 44063cc5d9b5f6d17b3d98589b3717d1b1fa6edf Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Wed, 3 Jun 2026 20:05:34 +0530 Subject: [PATCH 12/13] [patch] remove prerelease function --- src/mas/devops/utils.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/mas/devops/utils.py b/src/mas/devops/utils.py index 2f21c563..6ac821e4 100644 --- a/src/mas/devops/utils.py +++ b/src/mas/devops/utils.py @@ -74,20 +74,3 @@ def isVersionEqualOrAfter(_compare_to_version, _current_version): current_version = semver.VersionInfo.parse(strippedVersion) compareToVersion = semver.VersionInfo.parse(_compare_to_version) return current_version.compare(compareToVersion) >= 0 - - -def isPreReleaseVersion(version: str) -> bool: - """ - Check if a version string represents a pre-release version. - - Pre-release versions contain "-pre" in the version string, such as - "9.2.0-pre.stable+21734" or "9.2.0-pre.m1dev86". - - Args: - version (str): Version string to check (e.g., "9.2.0-pre.stable+21734"). - - Returns: - bool: True if the version is a pre-release, False otherwise. - Returns False if version is None or empty. - """ - return "-pre" in version if version else False From 10c8ffcf7ef6c01d65b9dd938792bb16381416fb Mon Sep 17 00:00:00 2001 From: Aneri Thakkar Date: Mon, 8 Jun 2026 10:15:09 +0530 Subject: [PATCH 13/13] [patch] rename to getInstallApps instead of getInstalledAppsForRBAC --- src/mas/devops/mas/__init__.py | 2 +- src/mas/devops/mas/apps.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mas/devops/mas/__init__.py b/src/mas/devops/mas/__init__.py index a58e4d46..57a72a20 100644 --- a/src/mas/devops/mas/__init__.py +++ b/src/mas/devops/mas/__init__.py @@ -2,7 +2,7 @@ verifyAppInstance, getAppsSubscriptionChannel, waitForAppReady, - getInstalledAppsForRBAC + getInstalledApps ) from .suite import ( # noqa: F401 diff --git a/src/mas/devops/mas/apps.py b/src/mas/devops/mas/apps.py index aab1cd71..3324a1b9 100644 --- a/src/mas/devops/mas/apps.py +++ b/src/mas/devops/mas/apps.py @@ -218,7 +218,7 @@ def getAppsSubscriptionChannel(dynClient: DynamicClient, instanceId: str) -> lis return [] -def getInstalledAppsForRBAC(dynClient: DynamicClient, instanceId: str) -> list: +def getInstalledApps(dynClient: DynamicClient, instanceId: str) -> list: """ Get list of installed apps for the given MAS instance for RBAC application. Always includes 'core' since core RBAC is required.