diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index a9433f55fa..3846a11e29 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -68,7 +68,7 @@ testCLI, launchInstallPipeline, ) -from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC +from mas.devops.pre_install import applyPreInstallMASRBAC, requiresPreInstallRBAC logger = logging.getLogger(__name__) @@ -118,51 +118,52 @@ def getSelectedApps(self) -> list[str]: return selectedApps def evaluatePreInstallRBACAccess(self) -> None: + """ + Evaluate if pre-install RBAC should be applied using requiresPreInstallRBAC(). + Sets self.applyPreInstallMASRBAC flag based on the result. + """ self.applyPreInstallMASRBAC = False - if not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): - return - - if self.mas_permission_mode == "minimal": - return - - if self.skip_preinstall_rbac: - return - - permissionResults = permissionCheckForRBAC(self.dynamicClient) - hasPreInstallRBACAccess = all(result["allowed"] for result in permissionResults) - - if hasPreInstallRBACAccess: - self.applyPreInstallMASRBAC = True - return - - if self.isInteractiveMode: - self.printDescription( - [ - "", - f"You selected the '{self.mas_permission_mode}' permission mode.", - "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", - "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.", - "Ask your OpenShift administrator to run 'mas pre-install' for this MAS instance, MAS channel, permission mode, and selected apps.", - "If that has already been done, you can continue the installation without applying it again.", - ] - ) + try: + # Use centralized function to check if RBAC should be applied + if requiresPreInstallRBAC(self.dynamicClient, self.getParam("mas_channel"), self.mas_permission_mode, self.skip_preinstall_rbac): + self.applyPreInstallMASRBAC = True + logger.info("Pre-install RBAC will be applied during installation") + else: + logger.info("Pre-install RBAC will be skipped during installation") - if not self.yesOrNo("Has your OpenShift administrator already run 'mas pre-install' for this installation"): + except PermissionError as e: + # User doesn't have RBAC permissions - prompt or fail + logger.warning(f"Permission error when checking RBAC requirements: {e}") + if not self.isInteractiveMode: + # Non-interactive mode - fail with clear message self.fatalError( - "Installation aborted. Ask your OpenShift administrator to run 'mas pre-install' for this installation and then run 'mas install' again with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'." + f"You selected the '{self.mas_permission_mode}' permission mode.\n" + "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.\n" + "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.\n" + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {self.getParam('mas_instance_id')} --mas-channel {self.getParam('mas_channel')}' " + "and then rerun 'mas install' with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'." ) - else: - self.fatalError( - "\n".join( + else: + # Interactive mode - ask if admin already applied RBAC + self.printDescription( [ + "", f"You selected the '{self.mas_permission_mode}' permission mode.", "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.", - "Ask your OpenShift administrator to run 'mas pre-install' for this installation and then rerun 'mas install' with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'.", + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {self.getParam('mas_instance_id')} --mas-channel {self.getParam('mas_channel')}'.", + "If that has already been done, you can continue the installation without applying it again.", ] ) - ) + + if not self.yesOrNo("Has your OpenShift administrator already run 'mas pre-install' for this installation"): + self.fatalError( + f"Installation aborted. Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {self.getParam('mas_instance_id')} --mas-channel {self.getParam('mas_channel')}' " + "and then run 'mas install' again with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'." + ) + # User confirmed RBAC was already applied, continue with installation + logger.info("User confirmed pre-install RBAC was already applied by administrator, continuing with installation") @logMethodCall def validateCatalogSource(self): diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 446d4b3d04..ac9548ec8e 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -22,9 +22,11 @@ from .argParser import updateArgParser from mas.devops.data import getCatalog, getNewestCatalogTag from mas.devops.ocp import createNamespace, getConsoleURL, getClusterVersion, isClusterVersionInRange -from mas.devops.mas import listMasInstances, getCurrentCatalog +from mas.devops.mas import listMasInstances, getCurrentCatalog, getMasChannel, getInstalledAppsForRBAC from mas.devops.aiservice import listAiServiceInstances from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchUpdatePipeline, prepareUpdateSecrets +from mas.devops.pre_install import applyPreInstallMASRBAC, requiresPreInstallRBAC +from mas.devops.utils import isVersionEqualOrAfter, extractBaseVersion, isPreReleaseVersion from ..install.settings import AdditionalConfigsMixin logger = logging.getLogger(__name__) @@ -32,6 +34,62 @@ class UpdateApp(BaseApp, AdditionalConfigsMixin): + def shouldApplyRBACForInstance(self, instanceId, currentVersion, targetCatalog) -> bool: + """ + Determine if RBAC should be applied for a specific MAS instance during update. + Returns True if the instance is transitioning from pre-release to GA version. + + This method checks: + 1. Current version is a pre-release (e.g., "9.2.0-pre.stable+21734") + 2. Target catalog contains a GA version (e.g., "9.2.0") for the instance's channel + 3. The GA version is >= 9.2.0 (when RBAC was introduced) + + Args: + instanceId: The MAS instance ID + currentVersion: The current reconciled version (e.g., "9.2.0-pre.stable+21734") + targetCatalog: The target catalog dictionary containing version mappings + + Returns: + bool: True if RBAC should be applied for pre-release → GA transition, False otherwise + """ + if not currentVersion or not targetCatalog: + return False + + # Check if current version is a pre-release + if not isPreReleaseVersion(currentVersion): + return False + + # Get the channel this instance is subscribed to + channel = getMasChannel(self.dynamicClient, instanceId) + if not channel: + logger.warning(f"Could not determine channel for instance {instanceId}") + return False + + # Get the target version from the catalog for this channel + targetVersion = targetCatalog.get("mas_core_version", {}).get(channel) + if not targetVersion: + logger.warning(f"No target version found in catalog for channel {channel}") + return False + + # Check if target version is GA (not pre-release) + # COMMENTED OUT FOR TESTING: Force RBAC application even for pre-release targets + # if isPreReleaseVersion(targetVersion): + # logger.info(f"Instance {instanceId} will stay on pre-release (current: {currentVersion}, target: {targetVersion}). Skipping RBAC.") + # return False + logger.info(f"TEST MODE: Forcing RBAC application (current: {currentVersion}, target: {targetVersion})") + + # Extract base version from target version + baseVersion = extractBaseVersion(targetVersion) + fullVersion = f"{baseVersion}.0" + + # Only apply RBAC if target GA version is >= 9.2.0 + if isVersionEqualOrAfter("9.2.0", fullVersion): + logger.info(f"Instance {instanceId} will transition from pre-release to GA (current: {currentVersion}, target: {targetVersion}).") + return True + + logger.info(f"Instance {instanceId} target version {targetVersion} is < 9.2.0. Skipping RBAC.") + return False + def update(self, argv): """ Update MAS instance @@ -40,6 +98,8 @@ def update(self, argv): self.noConfirm = self.args.no_confirm self.devMode = self.args.dev_mode self.db2LicenseFileLocal = None + self.skipPreinstallRbac = self.args.skip_preinstall_rbac if hasattr(self.args, "skip_preinstall_rbac") else False + self.noConfirm = self.args.no_confirm if hasattr(self.args, "no_confirm") else False if self.args.mas_catalog_version: # Non-interactive mode @@ -81,7 +141,7 @@ def update(self, argv): self.setParam(key, value) # Arguments that we don't need to do anything with - elif key in ["no_confirm", "help"]: + elif key in ["no_confirm", "help", "skip_preinstall_rbac"]: pass # Db2 License file has special handling @@ -213,6 +273,111 @@ def update(self, argv): h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator installation failed") self.fatalError("Installation failed") + # Apply pre-install RBAC for instances transitioning from pre-release to GA + # This handles the 9.2.0-pre.stable → 9.2.0 transition + try: + masInstances = listMasInstances(self.dynamicClient) + instancesNeedingRBAC = [] + + for instance in masInstances: + instanceId = instance["metadata"]["name"] + currentVersion = instance.get("status", {}).get("versions", {}).get("reconciled", "") + + if self.shouldApplyRBACForInstance(instanceId, currentVersion, self.chosenCatalog): + channel = getMasChannel(self.dynamicClient, instanceId) + targetVersion = self.chosenCatalog.get("mas_core_version", {}).get(channel, "") + instancesNeedingRBAC.append({"id": instanceId, "currentVersion": currentVersion, "targetVersion": targetVersion, "channel": channel}) + + if instancesNeedingRBAC: + print() + print_formatted_text( + HTML(f"Detected {len(instancesNeedingRBAC)} MAS instance(s) on pre-release versions that will transition to GA.") + ) + print_formatted_text(HTML("Applying RBAC for target GA version before catalog update...")) + print() + + for instanceInfo in instancesNeedingRBAC: + instanceId = instanceInfo["id"] + currentVersion = instanceInfo["currentVersion"] + targetVersion = instanceInfo["targetVersion"] + + # Extract base version from TARGET version for RBAC + baseVersion = extractBaseVersion(targetVersion) + + # Ensure version has patch number for semver validation (9.2 -> 9.2.0) + fullVersion = f"{baseVersion}.0" if baseVersion.count(".") == 1 else baseVersion + + # For pre-release to 9.2 GA transition, default to cluster mode + permissionMode = "cluster" + logger.info( + f"Using cluster mode for pre-release to GA transition for instance {instanceId} (current: {currentVersion}, target: {targetVersion})" + ) + + # Check permissions and apply RBAC + # requiresPreInstallRBAC checks permissions and raises PermissionError if user lacks access + try: + if requiresPreInstallRBAC(self.dynamicClient, fullVersion, permissionMode, self.skipPreinstallRbac): + # Get installed apps for this instance + selectedApps = getInstalledAppsForRBAC(self.dynamicClient, instanceId) + + with Halo( + text=f"Applying pre-install RBAC for {instanceId} (v{baseVersion}, mode: {permissionMode})", spinner=self.spinner + ) as h: + applyPreInstallMASRBAC( + dynClient=self.dynamicClient, + masVersion=baseVersion, + masInstanceId=instanceId, + permissionMode=permissionMode, + selectedApps=selectedApps, + ) + h.stop_and_persist( + symbol=self.successIcon, text=f"Pre-install RBAC applied for {instanceId} (v{baseVersion}, mode: {permissionMode})" + ) + except PermissionError as e: + # User doesn't have RBAC permissions - prompt or fail + logger.warning(f"Permission error when checking RBAC requirements: {e}") + if self.noConfirm: + # Non-interactive mode - fail with clear message + self.fatalError( + f"Instance {instanceId} is transitioning from pre-release to GA version {baseVersion}.\n" + f"The pre-install RBAC required for '{permissionMode}' permission mode has not been applied by your current cluster login.\n" + "This step must be completed by an OpenShift cluster administrator before MAS update can continue.\n" + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {baseVersion}' " + "and then rerun 'mas update' with --skip-preinstall-rbac." + ) + else: + # Interactive mode - ask if admin already applied RBAC + self.printDescription( + [ + "", + f"Instance {instanceId} is transitioning from pre-release to GA version {baseVersion}.", + f"The pre-install RBAC required for '{permissionMode}' permission mode has not been applied by your current cluster login.", + "This step must be completed by an OpenShift cluster administrator before MAS update can continue.", + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {baseVersion}'.", + "If that has already been done, you can continue the update without applying it again.", + ] + ) + + if not self.yesOrNo(f"Has your OpenShift administrator already run 'mas pre-install' for instance {instanceId}"): + self.fatalError( + f"Update aborted for instance {instanceId}. Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {baseVersion}' " + "and then run 'mas update' again with --skip-preinstall-rbac." + ) + # User confirmed RBAC was already applied, continue with update + logger.info( + f"User confirmed pre-install RBAC was already applied by administrator for instance {instanceId}, continuing with update" + ) + + print() + print_formatted_text(HTML("✓ Pre-install RBAC applied successfully for all instances transitioning to GA")) + print() + else: + logger.info("No MAS instances require RBAC update (not transitioning from pre-release to GA)") + + except Exception as e: + logger.error(f"Error while applying pre-install RBAC: {e}") + self.printWarning(f"Failed to apply pre-install RBAC: {e}\n" f"Continuing with update, but RBAC may need to be applied manually.") + with Halo(text=f"Preparing namespace ({pipelinesNamespace})", spinner=self.spinner) as h: createNamespace(self.dynamicClient, pipelinesNamespace) preparePipelinesNamespace(dynClient=self.dynamicClient) diff --git a/python/src/mas/cli/update/argParser.py b/python/src/mas/cli/update/argParser.py index b072602b8a..61314bb1df 100644 --- a/python/src/mas/cli/update/argParser.py +++ b/python/src/mas/cli/update/argParser.py @@ -201,6 +201,13 @@ def parse_args(self, args=None, namespace=None): # type: ignore[override] default=False, help="Skips the 'pre-update-check' and 'post-update-verify' tasks in the update pipeline", ) +otherArgGroup.add_argument( + "--skip-preinstall-rbac", + required=False, + action="store_true", + default=False, + help="Skip CLI application of pre-install MAS RBAC. Use this when an OpenShift administrator has already applied the required RBAC.", +) otherArgGroup.add_argument("--slack-token", required=False, help="Slack bot token for sending pipeline status notifications") otherArgGroup.add_argument("--slack-channel", required=False, help="Slack channel(s) for pipeline notifications (comma-separated for multiple channels)") otherArgGroup.add_argument( diff --git a/python/src/mas/cli/upgrade/app.py b/python/src/mas/cli/upgrade/app.py index 40a4dd7b8c..e50f871023 100644 --- a/python/src/mas/cli/upgrade/app.py +++ b/python/src/mas/cli/upgrade/app.py @@ -23,10 +23,18 @@ from .settings import UpgradeSettingsMixin from mas.devops.ocp import createNamespace -from mas.devops.mas import listMasInstances, getMasChannel, getAppsSubscriptionChannel, getWorkspaceId, verifyAppInstance, getPermissionMode -from mas.devops.utils import isVersionEqualOrAfter +from mas.devops.mas import ( + listMasInstances, + getMasChannel, + getAppsSubscriptionChannel, + getWorkspaceId, + verifyAppInstance, + getPermissionMode, + getInstalledAppsForRBAC, +) +from mas.devops.utils import isVersionEqualOrAfter, extractBaseVersion from mas.devops.tekton import installOpenShiftPipelines, updateTektonDefinitions, launchUpgradePipeline -from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC +from mas.devops.pre_install import applyPreInstallMASRBAC, requiresPreInstallRBAC logger = logging.getLogger(__name__) @@ -130,46 +138,6 @@ def validateKafkaForCivilUpgrade(self, instanceId): logger.warning(f"Could not query ManageWorkspace CR for Civil component check: {e}") # Don't fail the upgrade if we can't query - let ansible handle it - def evaluatePreInstallRBACForUpgrade(self, instanceId, targetChannel) -> bool: - """ - Evaluate if pre-install RBAC should be applied for upgrade. - Returns True if RBAC should be applied, False otherwise. - """ - # Only apply for MAS >= 9.2.0 - if not isVersionEqualOrAfter("9.2.0", targetChannel): - return False - - # Check if user has cluster-admin permissions - permissionResults = permissionCheckForRBAC(self.dynamicClient) - hasPreInstallRBACAccess = all(result["allowed"] for result in permissionResults) - - if hasPreInstallRBACAccess: - return True - - # If no permissions, warn user but don't block upgrade - logger.warning( - "Current user does not have cluster-admin permissions to apply pre-install RBAC. " "Assuming RBAC was already applied for this MAS version." - ) - return False - - def getInstalledAppsForUpgrade(self, instanceId) -> list[str]: - """ - Get list of installed apps that will be upgraded. - Always includes 'core' since core RBAC is required for all upgrades. - getAppsSubscriptionChannel() only returns apps (not core), so we add core explicitly. - """ - # Always include core for RBAC application - installedApps = ["core"] - - appsWithSubscriptions = getAppsSubscriptionChannel(self.dynamicClient, instanceId) - logger.info(f"Apps with subscriptions detected: {[app.get('appId') for app in appsWithSubscriptions]}") - - for app in appsWithSubscriptions: - appId = app.get("appId") - if appId: - installedApps.append(appId) - return installedApps - def upgrade(self, argv): """ Upgrade MAS instance @@ -181,6 +149,7 @@ def upgrade(self, argv): self.licenseAccepted = args.accept_license self.nextChannel = args.next_channel self.devMode = args.dev_mode + self.skipPreinstallRbac = args.skip_preinstall_rbac # Set image_pull_policy if provided if args.image_pull_policy and args.image_pull_policy != "": @@ -329,19 +298,6 @@ def upgrade(self, argv): # Current channel is 9.2+, detect permission mode detectedMode = getPermissionMode(self.dynamicClient, instanceId) - if detectedMode == "minimal": - self.fatalError( - f"Cannot upgrade MAS instance '{instanceId}' with 'minimal' permission mode.\n\n" - f"The instance is currently installed with 'minimal' mode, which does not grant\n" - f"the ibm-mas service account sufficient permissions to manage application resources during upgrade.\n\n" - f"To proceed with the upgrade:\n" - f"1. Temporarily increase permissions by re-applying RBAC with cluster or namespaced mode\n" - f"2. Run the upgrade\n" - f"3. After upgrade completes, you can switch back to minimal mode if desired\n\n" - ) - - print_formatted_text(HTML(f" Permission mode check passed (mode: {detectedMode})")) - print() elif currentChannel and currentChannel.startswith("9.1") and self.nextChannel and self.nextChannel.startswith("9.2"): # Upgrading from 9.1 to 9.2: default to cluster mode (9.1 had no permission modes) logger.info("Upgrading from 9.1.x to 9.2.x: defaulting to cluster mode (9.1.x had no permission modes)") @@ -372,20 +328,56 @@ def upgrade(self, argv): self.fatalError("Installation failed") # Apply pre-install RBAC for target version (only for nextChannel >= 9.2) - if detectedMode and self.evaluatePreInstallRBACForUpgrade(instanceId, self.nextChannel): - with Halo(text="Applying pre-install MAS RBAC for target version", spinner=self.spinner) as h: - targetVersion = ".".join(self.nextChannel.split(".")[:2]) # Extract "9.2" from "9.2-feature" - # get list of installed apps that needs to be upgraded - selectedApps = self.getInstalledAppsForUpgrade(instanceId) - # Use detected permission mode for RBAC application - applyPreInstallMASRBAC( - dynClient=self.dynamicClient, - masVersion=targetVersion, - masInstanceId=instanceId, - permissionMode=detectedMode, - selectedApps=selectedApps, + # requiresPreInstallRBAC checks permissions and raises PermissionError if user lacks access + try: + if detectedMode and requiresPreInstallRBAC(self.dynamicClient, self.nextChannel, detectedMode, self.skipPreinstallRbac): + with Halo(text="Applying pre-install MAS RBAC for target version", spinner=self.spinner) as h: + targetVersion = extractBaseVersion(self.nextChannel) # Extract "9.2" from "9.2.x" or "9.2-feature" + # get list of installed apps that needs to be upgraded + selectedApps = getInstalledAppsForRBAC(self.dynamicClient, instanceId) + # Use detected permission mode for RBAC application + applyPreInstallMASRBAC( + dynClient=self.dynamicClient, + masVersion=targetVersion, + masInstanceId=instanceId, + permissionMode=detectedMode, + selectedApps=selectedApps, + ) + h.stop_and_persist( + symbol=self.successIcon, text=f"Pre-install MAS RBAC applied for target version {targetVersion} (mode: {detectedMode})" + ) + except PermissionError as e: + # User doesn't have RBAC permissions - prompt or fail + logger.warning(f"Permission error when checking RBAC requirements: {e}") + if self.noConfirm: + # Non-interactive mode - fail with clear message + self.fatalError( + f"You are upgrading to MAS {self.nextChannel} with '{detectedMode}' permission mode.\n" + "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.\n" + "This step must be completed by an OpenShift cluster administrator before MAS upgrade can continue.\n" + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {self.nextChannel}' for this upgrade " + "and then rerun 'mas upgrade' with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'." ) - h.stop_and_persist(symbol=self.successIcon, text=f"Pre-install MAS RBAC applied for target version {targetVersion} (mode: {detectedMode})") + else: + # Interactive mode - ask if admin already applied RBAC + self.printDescription( + [ + "", + f"You are upgrading to MAS {self.nextChannel} with '{detectedMode}' permission mode.", + "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", + "This step must be completed by an OpenShift cluster administrator before MAS upgrade can continue.", + f"Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {self.nextChannel}' for this upgrade.", + "If that has already been done, you can continue the upgrade without applying it again.", + ] + ) + + if not self.yesOrNo("Has your OpenShift administrator already run 'mas pre-install' for this upgrade"): + self.fatalError( + f"Upgrade aborted. Ask your OpenShift administrator to run 'mas pre-install --mas-instance-id {instanceId} --mas-channel {self.nextChannel}' " + "and then run 'mas upgrade' again with --skip-preinstall-rbac using the same permission mode that was used in 'mas pre-install'." + ) + # User confirmed RBAC was already applied, continue with upgrade + logger.info("User confirmed pre-install RBAC was already applied by administrator, continuing with upgrade") with Halo(text=f"Preparing namespace ({pipelinesNamespace})", spinner=self.spinner) as h: createNamespace(self.dynamicClient, pipelinesNamespace) diff --git a/python/src/mas/cli/upgrade/argParser.py b/python/src/mas/cli/upgrade/argParser.py index f3d28f39ac..ee645602a6 100644 --- a/python/src/mas/cli/upgrade/argParser.py +++ b/python/src/mas/cli/upgrade/argParser.py @@ -50,6 +50,13 @@ help="Launch the upgrade without prompting for confirmation", ) otherArgGroup.add_argument("--accept-license", action="store_true", default=False, help="Accept all license terms without prompting") +otherArgGroup.add_argument( + "--skip-preinstall-rbac", + required=False, + action="store_true", + default=False, + help="Skip CLI application of pre-install MAS RBAC. Use this when an OpenShift administrator has already applied the required RBAC.", +) otherArgGroup.add_argument( "--dev-mode", required=False,