diff --git a/image/cli/Dockerfile b/image/cli/Dockerfile index 00a6ab6443f..2cae56b63d9 100644 --- a/image/cli/Dockerfile +++ b/image/cli/Dockerfile @@ -22,8 +22,9 @@ ENV ANSIBLE_COLLECTIONS_PATH=/opt/app-root/lib64/python3.12/site-packages/ansibl # 3. Install Python packages # 4. Install Ansible collections -# 5. Disable ibmcloud cli's new version check -# 6. Set file permissions to be developer (hack) friendly +# 5. Install Pre-Install RBAC files +# 6. Disable ibmcloud cli's new version check +# 7. Set file permissions to be developer (hack) friendly COPY install /tmp/install RUN --mount=type=secret,id=ARTIFACTORY_TOKEN \ --mount=type=secret,id=ARTIFACTORY_GENERIC_RELEASE_URL \ @@ -37,6 +38,7 @@ RUN --mount=type=secret,id=ARTIFACTORY_TOKEN \ ls /tmp/install && \ bash /tmp/install/install-python-packages.sh && \ bash /tmp/install/install-ansible-collections.sh && \ + bash /tmp/install/pre-install-rbac.sh && \ bash /tmp/install/permissions-updates.sh && \ ibmcloud config --check-version=false && \ ln -s /opt/app-root/lib/python3.12/site-packages /mascli/site-packages && \ diff --git a/image/cli/app-root/src/.bashrc b/image/cli/app-root/src/.bashrc index ad1aa32df23..00a04b16e4d 100644 --- a/image/cli/app-root/src/.bashrc +++ b/image/cli/app-root/src/.bashrc @@ -44,6 +44,7 @@ if [ $arch != "s390x" ] && [ $arch != "ppc64le" ]; then echo " - ${TEXT_BOLD}${COLOR_GREEN}mas provision-rosa${TEXT_RESET} to provision an OCP cluster on AWS Red Hat OpenShift Service (ROSA)" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas provision-fyre${TEXT_RESET} to provision an OCP cluster on IBM DevIT Fyre (internal)" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas setup-rbac${TEXT_RESET} to setup RBAC resources for MAS installation in a cluster" + echo " - ${TEXT_BOLD}${COLOR_GREEN}mas pre-install${TEXT_RESET} to set up pre-install RBAC for MAS installation in a cluster" echo "AI Service (Standalone) Management:" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-install${TEXT_RESET} to install a new AI Service instance" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-upgrade${TEXT_RESET} to upgrade a existing AI Service instance" diff --git a/image/cli/install/pre-install-rbac.sh b/image/cli/install/pre-install-rbac.sh new file mode 100644 index 00000000000..babeba1933c --- /dev/null +++ b/image/cli/install/pre-install-rbac.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +set -e + +# This script clones the pre-install repository and copies operator RBAC files +# into the CLI image during build time in a single RBAC root. +# +# Structures in pre-install: +# catalogs/maximo-operator-catalog/operators//rbac//*.yml +# openshift-platform/operators//rbac//*.yml +# +# Structure in CLI image: +# /opt/app-root/rbac/maximo-operator-catalog/operators//rbac//*.yml +# /opt/app-root/rbac/openshift-platform/operators//rbac//*.yml +export GITHUB_REF_NAME="${GITHUB_REF_NAME:-ds.rbac}" +export GITHUB_REF_TYPE="${GITHUB_REF_TYPE:-branch}" +echo "========================================" +echo "Installing Operator RBAC Files" +echo "========================================" +echo "GitHub reference = ${GITHUB_REF_TYPE}/${GITHUB_REF_NAME}" +echo "Contents of /tmp/install/:" +ls -l /tmp/install/ +echo "" + +# Destination root directory in CLI image +RBAC_DEST="/opt/app-root/rbac" + +# Create destination directory +mkdir -p "$RBAC_DEST" + +# If the local tar.gz file is present, extract and use it +# Otherwise, clone from GitHub +if [[ -e /tmp/install/pre-install.tar.gz ]]; then + echo "Installing local build of pre-install from archive" + cd /tmp/install + tar -xzf pre-install.tar.gz + PREINSTALL_SOURCE="/tmp/install/pre-install" +else + # Clone pre-install repository + echo "Cloning pre-install repository from GitHub..." + + # Determine which branch/tag to use + if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then + PREINSTALL_BRANCH="${GITHUB_REF_NAME}" + echo "Attempting to clone matching branch: ${PREINSTALL_BRANCH}" + else + # For tag builds, use main branch + PREINSTALL_BRANCH="main" + echo "Using main branch for tag build" + fi + + # Clone the repository + cd /tmp/install + if git clone --depth 1 --branch "${PREINSTALL_BRANCH}" https://github.com/ibm-mas/pre-install.git 2>/dev/null; then + echo "Successfully cloned pre-install repository (branch: ${PREINSTALL_BRANCH})" + else + echo "Branch ${PREINSTALL_BRANCH} not found, falling back to main branch" + git clone --depth 1 --branch main https://github.com/ibm-mas/pre-install.git + fi + + PREINSTALL_SOURCE="/tmp/install/pre-install" +fi + +MAXIMO_OPERATORS_SOURCE="$PREINSTALL_SOURCE/catalogs/maximo-operator-catalog/operators" +OPENSHIFT_PLATFORM_OPERATORS_SOURCE="$PREINSTALL_SOURCE/openshift-platform/operators" + +echo "Copying RBAC files into $RBAC_DEST" + +VERSIONS_COPIED=() +COPIED_SOURCE_ROOTS=() + +copy_operator_rbac() { + local SOURCE_ROOT="$1" + local DEST_ROOT="$2" + + if [ ! -d "$SOURCE_ROOT" ]; then + echo "Skipping missing source: $SOURCE_ROOT" + return + fi + + COPIED_SOURCE_ROOTS+=("$SOURCE_ROOT") + + for OPERATOR_DIR in "$SOURCE_ROOT"/*/; do + if [ -d "$OPERATOR_DIR" ] && [ -d "$OPERATOR_DIR/rbac" ]; then + OPERATOR_NAME=$(basename "$OPERATOR_DIR") + DEST_PATH="$DEST_ROOT/$OPERATOR_NAME/rbac" + mkdir -p "$DEST_PATH" + + if compgen -G "$OPERATOR_DIR/rbac/*" > /dev/null; then + cp -r "$OPERATOR_DIR/rbac"/* "$DEST_PATH/" + fi + + for VERSION_DIR in "$OPERATOR_DIR/rbac"/*/; do + if [ -d "$VERSION_DIR" ]; then + VERSION_NAME=$(basename "$VERSION_DIR") + if [[ "$VERSION_NAME" =~ ^[0-9]+\.[0-9]+$ ]]; then + VERSIONS_COPIED+=("$VERSION_NAME") + fi + fi + done + fi + done +} + +copy_operator_rbac "$MAXIMO_OPERATORS_SOURCE" "$RBAC_DEST/maximo-operator-catalog/operators" +copy_operator_rbac "$OPENSHIFT_PLATFORM_OPERATORS_SOURCE" "$RBAC_DEST/openshift-platform/operators" + +VERSIONS_COPIED=($(printf "%s\n" "${VERSIONS_COPIED[@]}" | sort -u)) +echo "RBAC files copied successfully from: ${COPIED_SOURCE_ROOTS[*]}" +echo "RBAC files copied successfully for versions: ${VERSIONS_COPIED[*]}" diff --git a/image/cli/mascli/functions/internal/save_config b/image/cli/mascli/functions/internal/save_config index f1c91dfe4f5..6d119e1a1cc 100644 --- a/image/cli/mascli/functions/internal/save_config +++ b/image/cli/mascli/functions/internal/save_config @@ -42,6 +42,8 @@ export MAS_DOMAIN=$MAS_DOMAIN export CLUSTER_ISSUER_SELECTION=$CLUSTER_ISSUER_SELECTION export MAS_CLUSTER_ISSUER=$MAS_CLUSTER_ISSUER +export MAS_PERMISSION_MODE=$MAS_PERMISSION_MODE + export MAS_ROUTING_MODE=$MAS_ROUTING_MODE export MAS_INGRESS_CONTROLLER_NAME=$MAS_INGRESS_CONTROLLER_NAME export MAS_CONFIGURE_INGRESS=$MAS_CONFIGURE_INGRESS diff --git a/image/cli/mascli/mas b/image/cli/mascli/mas index 7baf0fb45b7..4a9065e12c6 100755 --- a/image/cli/mascli/mas +++ b/image/cli/mascli/mas @@ -827,6 +827,15 @@ case $1 in mas-cli setup-rbac "$@" ;; + pre-install) + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + echo "!! pre-install !!" >> $LOGFILE + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + # Take the first parameter off (it will be "pre-install") + shift + # Run the new Python-based pre-install command + mas-cli pre-install "$@" + ;; gitops) echo "${TEXT_UNDERLINE}IBM Maximo Application Suite GitOps Functions (v${VERSION})${TEXT_RESET}" diff --git a/python/src/mas-cli b/python/src/mas-cli index cb858a39e15..8013c82d2e4 100644 --- a/python/src/mas-cli +++ b/python/src/mas-cli @@ -23,6 +23,7 @@ from mas.cli.backup.app import BackupApp from mas.cli.restore.app import RestoreApp from mas.cli.mirror.app import MirrorApp from mas.cli.setup_rbac.app import SetupRBACApp +from mas.cli.pre_install.app import SetupPreinstallRBACApp from prompt_toolkit import HTML, print_formatted_text from urllib3.exceptions import MaxRetryError @@ -50,6 +51,7 @@ def usage(): + " - mas-cli uninstall Remove MAS from the cluster\n" # noqa: W503 + " - mas-cli mirror Mirror container images \n" # noqa: W503 + " - mas-cli setup-rbac Set up RBAC resources for MAS installation\n" # noqa: W503 + + " - mas-cli pre-install Set up pre-install RBAC for MAS\n" # noqa: W503 )) print_formatted_text(HTML("For usage information run mas-cli [action] --help\n")) @@ -89,6 +91,9 @@ if __name__ == '__main__': elif function == "setup-rbac": app = SetupRBACApp() app.setupRBAC(argv[2:]) + elif function == "pre-install": + app = SetupPreinstallRBACApp() + app.setupPreinstallRBAC(argv[2:]) elif function in ["-h", "--help"]: usage() exit(0) diff --git a/python/src/mas/cli/aiservice/install/app.py b/python/src/mas/cli/aiservice/install/app.py index 7f2303283af..4346904b480 100644 --- a/python/src/mas/cli/aiservice/install/app.py +++ b/python/src/mas/cli/aiservice/install/app.py @@ -60,6 +60,8 @@ testCLI, launchInstallPipeline ) +from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC +from mas.devops.utils import isVersionEqualOrAfter logger = logging.getLogger(__name__) @@ -74,6 +76,72 @@ def wrapper(self, *args, **kwargs): class AiServiceInstallApp(BaseApp, aiServiceInstallArgBuilderMixin, aiServiceInstallSummarizerMixin, InstallSettingsMixin, ConfigGeneratorMixin): + + def evaluatePreInstallRBACAccess(self) -> None: + self.applyPreInstallMASRBAC = False + + if not isVersionEqualOrAfter('9.2.0', self.getParam("aiservice_channel")): + return + + if self.getParam("skip_preinstall_rbac") == "true": + 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.getParam('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 AI Service installation can continue.", + "Ask your OpenShift administrator to run 'mas pre-install' for this AI Service instance.", + "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 AI Service installation"): + self.fatalError("Installation aborted. Ask your OpenShift administrator to run 'mas pre-install' for this AI Service installation and then run 'mas aiservice-install' again with --skip-preinstall-rbac.") + else: + self.fatalError( + "\n".join([ + f"You selected the '{self.getParam('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 AI Service installation can continue.", + "Ask your OpenShift administrator to run 'mas pre-install' for this installation and then rerun 'mas aiservice-install' with --skip-preinstall-rbac." + ]) + ) + + def configPermissionMode(self) -> None: + if self.showAdvancedOptions: + self.printH1("Configure Permission Mode") + self.printDescription([ + "Choose how AI Service should be installed with respect to permissions:", + "", + " 1. cluster - Install with ClusterRoles (default)", + " - AI Service has cluster-level access to manage its resources across the cluster", + " - CLI pre-installs ClusterRoles to grant delegated admin permissions to AI Service service accounts", + "", + " 2. namespaced - Install with namespace-scoped Roles only", + " - No ClusterRoles are installed in this mode", + " - CLI pre-installs namespace-scoped Roles in prepared namespaces to grant delegated admin permissions", + " - AI Service can manage resources only in namespaces prepared by the OpenShift admin", + "", + " 3. minimal - Install with essential namespace-scoped Roles only", + " - No ClusterRoles are installed in this mode", + " - Only essential permissions required for AI Service are applied", + " - AI Service can manage only the resources covered by these essential permissions" + ]) + + permissionModeInt = self.promptForInt("Permission Mode", default=1, min=1, max=3) + permissionModeMap = {1: "cluster", 2: "namespaced", 3: "minimal"} + self.setParam("permission_mode", permissionModeMap[permissionModeInt]) + elif self.getParam("permission_mode") == "": + self.setParam("permission_mode", "cluster") + @logMethodCall def processCatalogChoice(self) -> list: self.catalogDigest = self.chosenCatalog["catalog_digest"] @@ -175,6 +243,9 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None: self.configMongoDb() self.setDB2DefaultChannel() self.setDB2DefaultSettings() + # Permission mode prompt (especially in dev mode) + if isVersionEqualOrAfter('9.2.0', self.getParam("aiservice_channel")): + self.configPermissionMode() @logMethodCall def nonInteractiveMode(self) -> None: @@ -334,7 +405,7 @@ def nonInteractiveMode(self) -> None: self.fatalError(f"Unsupported format for {key} ({value}). Expected int:int:boolean") # Arguments that we don't need to do anything with - elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_grafana_install", "no_confirm", "help", "advanced", "simplified"]: + elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_preinstall_rbac", "skip_grafana_install", "no_confirm", "help", "advanced", "simplified"]: pass elif key == "manual_certificates": @@ -370,6 +441,13 @@ def nonInteractiveMode(self) -> None: self.validateCatalogSource() self.licensePrompt() + if self.getParam("permission_mode") != "" and not isVersionEqualOrAfter('9.2.0', self.getParam("aiservice_channel")): + self.fatalError("--permission-mode is supported only for AI Service releases aligned to MAS 9.2.0 and later") + + # Set default permission_mode for 9.2.0+ if not provided + if isVersionEqualOrAfter('9.2.0', self.getParam("aiservice_channel")) and self.getParam("permission_mode") == "": + self.setParam("permission_mode", "cluster") + @logMethodCall def install(self, argv): """ @@ -409,6 +487,9 @@ def install(self, argv): if args.skip_pre_check: self.setParam("skip_pre_check", "true") + if hasattr(args, 'skip_preinstall_rbac') and args.skip_preinstall_rbac: + self.setParam("skip_preinstall_rbac", "true") + if instanceId is None: self.printH1("Set Target OpenShift Cluster") # Connect to the target cluster @@ -450,6 +531,8 @@ def install(self, argv): else: self.nonInteractiveMode() + self.evaluatePreInstallRBACAccess() + # Set up the sls license file self.slsLicenseFile() @@ -511,6 +594,17 @@ def install(self, argv): h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") + if self.applyPreInstallMASRBAC: + with Halo(text=f"Setting up pre-install RBAC for AI Service instance {self.getParam('aiservice_instance_id')}...", spinner=self.spinner) as h: + applyPreInstallMASRBAC( + dynClient=self.dynamicClient, + masVersion=".".join(self.getParam("aiservice_channel").split(".")[:2]), + masInstanceId=self.getParam("aiservice_instance_id"), + permissionMode=self.getParam("permission_mode"), + selectedApps=["aiservice"] + ) + h.stop_and_persist(symbol=self.successIcon, text=f"Pre-install RBAC for AI Service is ready for {self.getParam('aiservice_instance_id')}") + with Halo(text='Testing availability of MAS CLI image in cluster', spinner=self.spinner) as h: testCLI() h.stop_and_persist(symbol=self.successIcon, text="MAS CLI image deployment test completed") diff --git a/python/src/mas/cli/aiservice/install/argBuilder.py b/python/src/mas/cli/aiservice/install/argBuilder.py index 843a67db70c..86f10ec0009 100644 --- a/python/src/mas/cli/aiservice/install/argBuilder.py +++ b/python/src/mas/cli/aiservice/install/argBuilder.py @@ -96,6 +96,10 @@ def buildCommand(self) -> str: command += f" --dev-mode{newline}" if self.getParam('skip_pre_check') is True: command += f" --skip-pre-check{newline}" + if self.getParam('permission_mode') != "": + command += f" --permission-mode \"{self.getParam('permission_mode')}\"{newline}" + if self.getParam('skip_preinstall_rbac') != "": + command += f" --skip-preinstall-rbac{newline}" if self.getParam('image_pull_policy') != "": command += f" --image-pull-policy {self.getParam('image_pull_policy')}{newline}" if self.getParam('service_account_name') != "": diff --git a/python/src/mas/cli/aiservice/install/argParser.py b/python/src/mas/cli/aiservice/install/argParser.py index 3537e926041..3373147f12d 100644 --- a/python/src/mas/cli/aiservice/install/argParser.py +++ b/python/src/mas/cli/aiservice/install/argParser.py @@ -435,6 +435,20 @@ def isValidFile(parser, arg) -> str: required=False, help="Provide the name of the Issuer to configure AI Service to issue certificates", ) +aiserviceAdvancedArgGroup.add_argument( + "--permission-mode", + dest="permission_mode", + required=False, + choices=["cluster", "namespaced", "minimal"], + help="The permission mode used to determine which pre-install RBAC manifests are applied for AI Service (MAS 9.2+ advanced option)" +) +aiserviceAdvancedArgGroup.add_argument( + "--skip-preinstall-rbac", + dest="skip_preinstall_rbac", + required=False, + action="store_true", + help="Skip pre-install RBAC setup (non-interactive mode only)" +) aiserviceAdvancedArgGroup.add_argument( "--enable-ipv6", dest="enable_ipv6", diff --git a/python/src/mas/cli/aiservice/install/params.py b/python/src/mas/cli/aiservice/install/params.py index e2050315db0..93846bae812 100644 --- a/python/src/mas/cli/aiservice/install/params.py +++ b/python/src/mas/cli/aiservice/install/params.py @@ -104,6 +104,9 @@ # Certificate Issuer "aiservice_certificate_issuer", + # permission mode + "permission_mode", + # Enable IPv6 networking "enable_ipv6", diff --git a/python/src/mas/cli/aiservice/install/summarizer.py b/python/src/mas/cli/aiservice/install/summarizer.py index b32a4b905a8..804e4344721 100644 --- a/python/src/mas/cli/aiservice/install/summarizer.py +++ b/python/src/mas/cli/aiservice/install/summarizer.py @@ -46,6 +46,9 @@ def aiServiceSummary(self) -> None: self.printParamSummary("Release", "aiservice_channel") self.printParamSummary("Instance ID", "aiservice_instance_id") self.printParamSummary("Environment Type", "environment_type") + if self.getParam("permission_mode") not in [None, ""]: + self.printParamSummary("Permission Mode", "permission_mode") + self.printSummary("Skip Pre-Install RBAC", "Yes" if self.getParam('skip_preinstall_rbac') == "true" else "No") if "aiservice_certificate_issuer" in self.params: self.printParamSummary("Certificate Issuer", "aiservice_certificate_issuer") diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index d3eca69f49a..f0199c5cd64 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -71,6 +71,7 @@ testCLI, launchInstallPipeline ) +from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC logger = logging.getLogger(__name__) @@ -85,6 +86,75 @@ def wrapper(self, *args, **kwargs): class InstallApp(BaseApp, InstallSettingsMixin, InstallSummarizerMixin, ConfigGeneratorMixin, installArgBuilderMixin): + + def getSelectedApps(self) -> list[str]: + selectedApps = ["core"] + if self.installAssist: + selectedApps.append("assist") + if self.installIoT: + selectedApps.append("iot") + if self.installManage: + selectedApps.append("manage") + if self.installMonitor: + selectedApps.append("monitor") + if self.installPredict: + selectedApps.append("predict") + if self.installInspection: + selectedApps.append("visualinspection") + if self.installOptimizer: + selectedApps.append("optimizer") + if self.installFacilities: + selectedApps.append("facilities") + if self.installAIService: + selectedApps.append("aiservice") + if self.installArcgis: + selectedApps.append("arcgis") + + return selectedApps + + def evaluatePreInstallRBACAccess(self) -> None: + self.applyPreInstallMASRBAC = False + + if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + return + + # TODO: Sort out the openshift-ingress exception properly. + # For now, keep continue pre-install RBAC for minimal mode here. + # if self.getParam("mas_permission_mode") == "minimal": + # return + + if self.getParam("skip_preinstall_rbac") == "true": + 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.getParam('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 version, permission mode, and selected apps.", + "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("Installation aborted. Ask your OpenShift administrator to run 'mas pre-install' for this installation and then run mas install again with --skip-preinstall-rbac.") + else: + self.fatalError( + "\n".join([ + f"You selected the '{self.getParam('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." + ]) + ) + @logMethodCall def validateCatalogSource(self): # Check supported OCP versions - but we can only do this in non-development mode because in development mode @@ -632,6 +702,76 @@ def configOperationMode(self): self.setParam("aiservice_rhoai_model_deployment_type", "serverless") self.setParam("rhoai", "false") + @logMethodCall + def configPermissionMode(self): + if isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + if self.showAdvancedOptions: + self.printH1("Configure Permission Mode") + self.printDescription([ + "Choose how MAS should be installed with respect to permissions:", + "", + " 1. cluster - Install with ClusterRoles (default)", + " - MAS has cluster-level access to manage its applications and resources across the cluster", + " - CLI pre-installs ClusterRoles to grant delegated admin permissions to MAS service accounts", + "", + " 2. namespaced - Install with namespace-scoped Roles only", + " - No ClusterRoles are installed in this mode", + " - CLI pre-installs namespace-scoped Roles in prepared namespaces to grant delegated admin permissions", + " - MAS can manage applications only in namespaces prepared by the OpenShift admin", + " - DNS integration is not available in this mode. If you use a custom domain, you need to configure DNS manually.", + "", + " 3. minimal - Install with essential namespace-scoped Roles only", + " - No ClusterRoles are installed in this mode", + " - Only essential permissions required for MAS applications are applied", + " - MAS UI/API cannot manage application lifecycle; OpenShift admins must manage apps outside MAS", + " - DNS integration is not available in this mode. If you use a custom domain, you need to configure DNS manually." + ]) + + permissionModeInt = self.promptForInt("Permission Mode", default=1, min=1, max=3) + permissionModeMap = {1: "cluster", 2: "namespaced", 3: "minimal"} + self.setParam("mas_permission_mode", permissionModeMap[permissionModeInt]) + + if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: + self.setParam("mas_issuer_kind", "Issuer") + else: + self.printDescription([ + "Select the issuer kind used by MAS for certificates:", + "", + " 1. Issuer", + " - MAS uses a namespace-scoped issuer resource for certificates", + " - You can not get CLI-managed DNS integration", + "", + " 2. ClusterIssuer", + " - MAS uses a cluster-scoped clusterissuer resource for certificates" + ]) + issuerKindChoice = self.promptForInt("Certificate issuer kind", min=1, max=2, default=2) + self.setParam("mas_issuer_kind", "ClusterIssuer" if issuerKindChoice == 2 else "Issuer") + elif self.getParam("mas_permission_mode") == "": + self.setParam("mas_permission_mode", "cluster") + self.setParam("mas_issuer_kind", "ClusterIssuer") + + def _handleDNSIntegrationRestriction(self): + if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + return False + + if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: + self.printDescription([ + f"You are using the {self.getParam('mas_permission_mode')} permission mode.", + "DNS integration is not available in this mode.", + "If you use a custom domain, you need to configure DNS manually." + ]) + return True + + if self.getParam("mas_issuer_kind") == "Issuer": + self.printDescription([ + "You selected Issuer as the certificate issuer kind.", + "DNS integration is not available when the certificate issuer kind is Issuer.", + "If you use a custom domain, you need to configure DNS manually." + ]) + return True + + return False + def _getMasDomainForDisplay(self): masDomain = self.getParam("mas_domain") if not masDomain: @@ -874,37 +1014,43 @@ def configDNSAndCerts(self): self.printH1("Configure Domain & Certificate Management") configureDomainAndCertMgmt = self.yesOrNo('Configure domain & certificate management') if configureDomainAndCertMgmt: + dnsIntegrationRestricted = self._handleDNSIntegrationRestriction() configureDomain = self.yesOrNo('Configure custom domain') if configureDomain: self.promptForString("MAS top-level domain", "mas_domain") - self.printDescription([ - "", - "DNS Integrations:", - " 1. Cloudflare", - " 2. IBM Cloud Internet Services", - " 3. AWS Route 53", - " 4. None (I will set up DNS myself)" - ]) - - dnsProvider = self.promptForInt("DNS Provider", min=1, max=4) - if dnsProvider == 1: - self.configDNSAndCertsCloudflare() - elif dnsProvider == 2: - self.configDNSAndCertsCIS() - elif dnsProvider == 3: - self.configDNSAndCertsRoute53() - elif dnsProvider == 4: - # Use MAS default self-signed cluster issuer with a custom domain + if dnsIntegrationRestricted: self.setParam("dns_provider", "") self.setParam("mas_cluster_issuer", "") - - if dnsProvider in [1, 2]: + else: self.printDescription([ - "By default, DNS CNAME records will be created pointing to the domain of the cluster ingress (ingress.config.openshift.io/cluster).", - "CloudFlare and CIS DNS integrations support the ability to provide an alternative domain, which may be necessary if you are using OpenShift Container Platform in a non-standard networking configuration." + "", + "DNS Integrations:", + " 1. Cloudflare", + " 2. IBM Cloud Internet Services", + " 3. AWS Route 53", + " 4. None (I will set up DNS myself)" ]) - self.promptForString("Cluster Ingress Domain Override", "ocp_ingress") + + dnsProvider = self.promptForInt("DNS Provider", min=1, max=4) + + if dnsProvider == 1: + self.configDNSAndCertsCloudflare() + elif dnsProvider == 2: + self.configDNSAndCertsCIS() + elif dnsProvider == 3: + self.configDNSAndCertsRoute53() + elif dnsProvider == 4: + # Use MAS default self-signed cluster issuer with a custom domain + self.setParam("dns_provider", "") + self.setParam("mas_cluster_issuer", "") + + if dnsProvider in [1, 2]: + self.printDescription([ + "By default, DNS CNAME records will be created pointing to the domain of the cluster ingress (ingress.config.openshift.io/cluster).", + "CloudFlare and CIS DNS integrations support the ability to provide an alternative domain, which may be necessary if you are using OpenShift Container Platform in a non-standard networking configuration." + ]) + self.promptForString("Cluster Ingress Domain Override", "ocp_ingress") else: # Use MAS default self-signed cluster issuer with the default domain @@ -1549,6 +1695,8 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None: self.configICRCredentials() # MAS Core + self.configPermissionMode() + self.evaluatePreInstallRBACAccess() self.configCertManager() self.configMAS() @@ -1802,7 +1950,7 @@ def nonInteractiveMode(self) -> None: self.fatalError(f"Unsupported format for {key} ({value}). Expected int:int:boolean") # Arguments that we don't need to do anything with - elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_grafana_install", "no_confirm", "help", "advanced", "simplified", "mas_configure_ingress"]: + elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_preinstall_rbac", "skip_grafana_install", "no_confirm", "help", "advanced", "simplified", "mas_configure_ingress"]: pass elif key == "manual_certificates": @@ -1908,6 +2056,54 @@ def nonInteractiveMode(self) -> None: self.licensePrompt() self.setParam("db2u_kind", "db2ucluster") + if self.getParam("mas_issuer_kind") != "" and not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + self.fatalError(f"--mas-issuer-kind is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") + + if self.getParam("mas_permission_mode") != "": + if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + self.fatalError(f"--permission-mode is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") + else: + if self.getParam("mas_issuer_kind") == "": + if self.getParam("mas_permission_mode") == "cluster": + self.setParam("mas_issuer_kind", "ClusterIssuer") + else: + self.setParam("mas_issuer_kind", "Issuer") + + if self.getParam("mas_issuer_kind") == "ClusterIssuer" and self.getParam("mas_permission_mode") != "cluster": + self.fatalError( + "\n".join([ + "Invalid configuration for certificate issuer kind 'ClusterIssuer'", + "ClusterIssuer can only be used when --permission-mode cluster is selected." + ]) + ) + + if self.getParam("dns_provider") != "": + if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: + self.fatalError( + "\n".join([ + f"Invalid configuration for permission mode '{self.getParam('mas_permission_mode')}'", + "DNS integration is not available in this mode.", + "Remove DNS integration option --dns-provider, or switch to --permission-mode cluster and use --mas-issuer-kind ClusterIssuer.", + ]) + ) + + if ( + self.getParam("mas_permission_mode") == "cluster" and + self.getParam("mas_issuer_kind") == "Issuer" + ): + self.fatalError( + "\n".join([ + "Invalid configuration for certificate issuer kind 'Issuer'", + "DNS integration is not available when --mas-issuer-kind Issuer is selected.", + "Remove DNS integration option --dns-provider, or use --mas-issuer-kind ClusterIssuer.", + ]) + ) + elif isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + self.setParam("mas_permission_mode", "cluster") + if self.getParam("mas_issuer_kind") == "": + self.setParam("mas_issuer_kind", "ClusterIssuer") + + self.evaluatePreInstallRBACAccess() self.setDB2DefaultChannel() # Version before 9.1 cannot have empty components @@ -1969,6 +2165,9 @@ def install(self, argv): if args.skip_pre_check: self.setParam("skip_pre_check", "true") + if hasattr(args, 'skip_preinstall_rbac') and args.skip_preinstall_rbac: + self.setParam("skip_preinstall_rbac", "true") + if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: self.setParam("mas_configure_ingress", "true") @@ -2152,6 +2351,17 @@ def install(self, argv): h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator installation failed") self.fatalError("Installation failed") + if self.applyPreInstallMASRBAC: + with Halo(text='Applying pre-install MAS RBAC', spinner=self.spinner) as h: + applyPreInstallMASRBAC( + dynClient=self.dynamicClient, + masVersion=".".join(self.getParam("mas_channel").split(".")[:2]), + masInstanceId=self.getParam("mas_instance_id"), + permissionMode=self.getParam("mas_permission_mode"), + selectedApps=self.getSelectedApps() + ) + h.stop_and_persist(symbol=self.successIcon, text="Pre-install MAS RBAC applied") + # Enable console plugin for OCP 4.21+ with Halo(text='Enabling Pipelines console plugin', spinner=self.spinner) as h: if enablePipelinesConsolePlugin(self.dynamicClient): diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index b602365df90..7cd61b733d5 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -92,6 +92,9 @@ def buildCommand(self) -> str: if self.operationalMode == 2: command += f" --non-prod{newline}" + if self.getParam('mas_permission_mode') != "": + command += f" --permission-mode {self.getParam('mas_permission_mode')}{newline}" + if self.getParam('mas_trust_default_cas').lower() == "false": command += f" --disable-ca-trust{newline}" @@ -128,6 +131,9 @@ def buildCommand(self) -> str: if self.getParam('mas_cluster_issuer') != "": command += f" --mas-cluster-issuer \"{self.getParam('mas_cluster_issuer')}\"{newline}" + if self.getParam('mas_issuer_kind') != "": + command += f" --mas-issuer-kind \"{self.getParam('mas_issuer_kind')}\"{newline}" + if self.getParam('mas_enable_walkme').lower() == "false": command += f" --disable-walkme{newline}" @@ -595,6 +601,8 @@ def buildCommand(self) -> str: command += f" --dev-mode{newline}" if self.getParam('skip_pre_check') is True: command += f" --skip-pre-check{newline}" + if self.getParam('skip_preinstall_rbac') == "true": + command += f" --skip-preinstall-rbac{newline}" if self.getParam('image_pull_policy') != "": command += f" --image-pull-policy {self.getParam('image_pull_policy')}{newline}" if self.getParam('service_account_name') != "": diff --git a/python/src/mas/cli/install/argParser.py b/python/src/mas/cli/install/argParser.py index 16a72ad46aa..79f2b472fb9 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -258,6 +258,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Provide the name of the ClusterIssuer to configure MAS to issue certificates", ) +masAdvancedArgGroup.add_argument( + "--mas-issuer-kind", + dest="mas_issuer_kind", + required=False, + choices=["Issuer", "ClusterIssuer"], + help="Specify the certificate issuer kind to configure Mas Certificate", +) + masAdvancedArgGroup.add_argument( "--enable-ipv6", dest="enable_ipv6", @@ -267,6 +275,15 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: const="true" ) +masAdvancedArgGroup.add_argument( + "--permission-mode", + dest="mas_permission_mode", + required=False, + help="Permission mode for MAS installation: 'cluster' (with ClusterRoles, default), 'namespaced' (without ClusterRoles, limited to pre-created namespaces), 'minimal' (essential roles only, no app lifecycle management)", + choices=["cluster", "namespaced", "minimal"], + default=None +) + # DNS Integration - IBM CIS # ----------------------------------------------------------------------------- cisArgGroup = installArgParser.add_argument_group("DNS Integration - CIS") @@ -1634,6 +1651,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: action="store_true", help="Disable the 'pre-install-check' at the start of the install 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( "--no-confirm", required=False, diff --git a/python/src/mas/cli/install/params.py b/python/src/mas/cli/install/params.py index 219debb7ce7..c44424da6a7 100644 --- a/python/src/mas/cli/install/params.py +++ b/python/src/mas/cli/install/params.py @@ -44,6 +44,8 @@ "mas_app_settings_default_jms", "mas_app_settings_persistent_volumes_flag", "mas_app_settings_demodata", + "mas_permission_mode", + "mas_issuer_kind", "mas_app_settings_customization_archive_name", "mas_app_settings_customization_archive_url", "mas_app_settings_customization_archive_username", diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index 416f55fc459..a109dacbda7 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -47,6 +47,14 @@ def masSummary(self) -> None: print() self.printSummary("Operational Mode", operationalModeNames[self.operationalMode]) + if self.getParam("mas_permission_mode") != "": + self.printParamSummary("Permission Mode", "mas_permission_mode") + if self.getParam("mas_issuer_kind") != "": + self.printParamSummary("Mas Certificate Issuer Kind", "mas_issuer_kind") + self.printSummary( + "Apply Pre-Install MAS RBAC", + "No" if self.getParam("skip_preinstall_rbac") == "true" else "Yes" + ) if self.isAirgap(): self.printSummary("Install Mode", "Disconnected Install") else: diff --git a/python/src/mas/cli/pre_install/__init__.py b/python/src/mas/cli/pre_install/__init__.py new file mode 100644 index 00000000000..10b45d523a5 --- /dev/null +++ b/python/src/mas/cli/pre_install/__init__.py @@ -0,0 +1,9 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** diff --git a/python/src/mas/cli/pre_install/app.py b/python/src/mas/cli/pre_install/app.py new file mode 100644 index 00000000000..377d6e1d97c --- /dev/null +++ b/python/src/mas/cli/pre_install/app.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import logging +from halo import Halo +from prompt_toolkit import prompt +from prompt_toolkit.completion import WordCompleter + +from ..cli import BaseApp +from ..validators import InstanceIDFormatValidator +from .argParser import setupPreinstallRBACArgParser +from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC +from mas.devops.utils import isVersionEqualOrAfter + +logger = logging.getLogger(__name__) + +VALID_PREINSTALL_APPS = { + "core", + "aiservice", + "arcgis", + "facilities", + "iot", + "manage", + "monitor", + "optimizer", + "predict", + "visualinspection" +} + + +class SetupPreinstallRBACApp(BaseApp): + + def promptForMASInstanceId(self) -> None: + self.printH2("MAS Instance") + self.promptForString("Instance ID", "mas_instance_id", validator=InstanceIDFormatValidator()) + + def promptForMASVersion(self) -> None: + self.printH2("MAS Version") + self.printDescription([ + "Enter the MAS version in x.y.z format.", + "For example: 9.2.0" + ]) + self.promptForString("MAS Version", "mas_version", default="9.2.0") + + def promptForPermissionMode(self) -> None: + self.printH2("Permission Mode") + self.printDescription([ + "Choose the permission mode for which pre-install RBAC should be set up:", + "", + " 1. cluster", + " 2. namespaced", + " 3. minimal" # we do not require pre install for minimal, but we need it for the one role ingresscontroller + ]) + permissionModeInt = self.promptForInt("Permission Mode", default=1, min=1, max=3) + permissionModeMap = {1: "cluster", 2: "namespaced", 3: "minimal"} + self.setParam("permission_mode", permissionModeMap[permissionModeInt]) + + def promptForApps(self) -> None: + self.printH2("MAS Applications") + self.printDescription([ + "Enter a comma-separated list of MAS applications to determine which pre-install RBAC manifests are set up.", + "For example: core,manage,iot" + ]) + appCompleter = WordCompleter([ + "core", + "aiservice", + "arcgis", + "facilities", + "iot", + "manage", + "monitor", + "optimizer", + "predict", + "visualinspection" + ], ignore_case=True) + apps = prompt("Apps: ", completer=appCompleter).strip() + if apps == "": + self.fatalError("Apps must be set") + self.setParam("apps", apps) + + def setupPreinstallRBAC(self, argv): + """ + Set up pre-install RBAC for MAS. + """ + self.args = setupPreinstallRBACArgParser.parse_args(args=argv) + self.noConfirm = self.args.no_confirm + self.interactive_mode = not all([ + self.args.mas_instance_id, + self.args.mas_version, + self.args.permission_mode, + self.args.apps + ]) + + self.printH1("Set Target OpenShift Cluster") + self.connect() + + if self.interactive_mode: + + if self.args.mas_instance_id is not None: + self.setParam("mas_instance_id", self.args.mas_instance_id) + else: + self.promptForMASInstanceId() + + if self.args.mas_version is not None: + self.setParam("mas_version", self.args.mas_version.strip()) + else: + self.promptForMASVersion() + else: + requiredParams = ["mas_instance_id", "mas_version", "permission_mode", "apps"] + for key in requiredParams: + value = getattr(self.args, key) + if value is None or (isinstance(value, str) and value.strip() == ""): + self.fatalError(f"{key} must be set") + self.setParam(key, value.strip() if isinstance(value, str) else value) + + instanceId = self.getParam("mas_instance_id") + masVersion = self.getParam("mas_version").strip() + + masVersionParts = masVersion.split(".") + if len(masVersionParts) != 3 or not all(part.isdigit() for part in masVersionParts): + self.fatalError("MAS version must be provided in x.y.z format, for example 9.2.0") + + masVersionForComparison = masVersion + if not isVersionEqualOrAfter("9.2.0", masVersionForComparison): + self.fatalError("mas pre-install is supported only for MAS version 9.2.0 and later") + + masVersion = ".".join(masVersionParts[:2]) + + if self.interactive_mode: + if self.args.permission_mode is not None: + self.setParam("permission_mode", self.args.permission_mode.strip()) + else: + self.promptForPermissionMode() + + if self.args.apps is not None: + self.setParam("apps", self.args.apps.strip()) + else: + self.promptForApps() + + permissionMode = self.getParam("permission_mode").strip() + selectedApps = [app.strip().lower() for app in self.getParam("apps").split(",") if app.strip()] + invalidApps = sorted({app for app in selectedApps if app not in VALID_PREINSTALL_APPS}) + if invalidApps: + self.fatalError( + f"Unsupported app value(s): {', '.join(invalidApps)}. " + f"Supported apps are: {', '.join(sorted(VALID_PREINSTALL_APPS))}" + ) + + permissionResults = permissionCheckForRBAC(self.dynamicClient) + hasAdminPermissions = all(result["allowed"] for result in permissionResults) + if not hasAdminPermissions: + self.fatalError("You do not have the appropriate permissions to set up pre-install RBAC for MAS. Only a cluster administrator can perform this action.") + + self.printH1("MAS Pre-Install") + self.printDescription([ + "This will set up pre-install RBAC for MAS.", + "", + "This command is supported only for MAS version 9.2 and later.", + "The RBAC that is applied is determined by the selected permission mode and apps." + ]) + self.printSummary("Instance ID", instanceId) + self.printSummary("MAS Version", masVersion) + self.printSummary("Permission Mode", permissionMode) + self.printSummary("Selected Apps", ", ".join(selectedApps)) + + continueWithSetup = True + if not self.noConfirm: + print() + self.printDescription([ + "Please carefully review your choices above before the RBAC setup begins." + ]) + continueWithSetup = self.yesOrNo("Proceed with these settings") + + if not continueWithSetup: + self.fatalError("Pre-install RBAC setup aborted") + + with Halo(text=f"Setting up pre-install RBAC for MAS instance {instanceId}...", spinner=self.spinner) as h: + applyPreInstallMASRBAC( + dynClient=self.dynamicClient, + masVersion=masVersion, + masInstanceId=instanceId, + permissionMode=permissionMode, + selectedApps=selectedApps + ) + h.stop_and_persist(symbol=self.successIcon, text=f"Pre-install RBAC for MAS is ready for {instanceId}") + + self.printDescription([ + "The pre-install RBAC for MAS has been set up." + ]) diff --git a/python/src/mas/cli/pre_install/argParser.py b/python/src/mas/cli/pre_install/argParser.py new file mode 100644 index 00000000000..17ea0c32be1 --- /dev/null +++ b/python/src/mas/cli/pre_install/argParser.py @@ -0,0 +1,80 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import argparse + +from .. import __version__ as packageVersion +from ..cli import getHelpFormatter + +setupPreinstallRBACArgParser = argparse.ArgumentParser( + prog="mas pre-install", + description="\n".join([ + f"IBM Maximo Application Suite Admin CLI v{packageVersion}", + "Set up pre-install RBAC for MAS.", + "Available only for MAS version 9.2.0 and later.", + ]), + epilog="", + formatter_class=getHelpFormatter(), + add_help=False +) + +targetArgGroup = setupPreinstallRBACArgParser.add_argument_group( + "Target Cluster Arguments", + "Specify the target cluster and MAS instance for which pre-install RBAC should be set up." +) + +targetArgGroup.add_argument( + "-i", "--mas-instance-id", + dest="mas_instance_id", + required=False, + help="The MAS instance ID for which pre-install RBAC will be set up" +) + +targetArgGroup.add_argument( + "--mas-version", + dest="mas_version", + required=False, + help="The MAS version in x.y.z format used to select pre-install RBAC manifests, for example 9.2.0" +) + +targetArgGroup.add_argument( + "--permission-mode", + dest="permission_mode", + required=False, + choices=["cluster", "namespaced", "minimal"], + help="The permission mode used to determine which pre-install RBAC manifests are set up" +) + +targetArgGroup.add_argument( + "--apps", + dest="apps", + required=False, + help="Comma-separated list of apps used to filter which pre-install RBAC manifests are set up, for example core,manage,iot" +) + +otherArgGroup = setupPreinstallRBACArgParser.add_argument_group( + "More", + "Additional options for pre-install." +) + +otherArgGroup.add_argument( + "--no-confirm", + required=False, + action="store_true", + default=False, + help="Proceed without prompting for cluster confirmation" +) + +setupPreinstallRBACArgParser.add_argument( + "-h", "--help", + action="help", + default=False, + help="Show this help message and exit" +) diff --git a/python/test/install/test_dev_mode.py b/python/test/install/test_dev_mode.py index 153c7951dc8..5e0d2e1aac0 100644 --- a/python/test/install/test_dev_mode.py +++ b/python/test/install/test_dev_mode.py @@ -97,18 +97,18 @@ def test_install_master_dev_mode(tmpdir): '.*Install Visual Inspection.*': lambda msg: 'n', '.*Install.*Real Estate and Facilities.*': lambda msg: 'n', '.*Install AI Service.*': lambda msg: 'n', - # 10a. Grafana configuration + # 11. Grafana configuration '.*Install Grafana.*': lambda msg: 'y', - # 11. MongoDB configuration + # 12. MongoDB configuration '.*Create MongoDb cluster.*': lambda msg: 'y', - # 12. Db2 configuration + # 13. Db2 configuration '.*Create system Db2 instance.*': lambda msg: 'y', '.*Re-use System Db2 instance for Manage application.*': lambda msg: 'n', '.*Create Manage dedicated Db2 instance.*': lambda msg: 'y', - # 13. Kafka configuration + # 14. Kafka configuration '.*Create system Kafka instance.*': lambda msg: 'y', '.*Kafka version.*': lambda msg: '3.8.0', - # 14. Final confirmation + # 15. Final confirmation '.*Use additional configurations.*': lambda msg: 'n', ".*Proceed with these settings.*": lambda msg: 'y', } @@ -178,18 +178,18 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): '.*Install Visual Inspection.*': lambda msg: 'n', '.*Install.*Real Estate and Facilities.*': lambda msg: 'n', '.*Install AI Service.*': lambda msg: 'n', - # 10a. Grafana configuration + # 11. Grafana configuration '.*Install Grafana.*': lambda msg: 'y', - # 11. MongoDB configuration + # 12. MongoDB configuration '.*Create MongoDb cluster.*': lambda msg: 'y', - # 12. Db2 configuration + # 13. Db2 configuration '.*Create system Db2 instance.*': lambda msg: 'y', '.*Re-use System Db2 instance for Manage application.*': lambda msg: 'n', '.*Create Manage dedicated Db2 instance.*': lambda msg: 'y', - # 13. Kafka configuration + # 14. Kafka configuration '.*Create system Kafka instance.*': lambda msg: 'y', '.*Kafka version.*': lambda msg: '3.8.0', - # 14. Final confirmation + # 15. Final confirmation '.*Use additional configurations.*': lambda msg: 'n', ".*Proceed with these settings.*": lambda msg: 'y', } @@ -271,25 +271,29 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): '.*Workspace.*name.*': lambda msg: 'Test Workspace', # 10. Operational mode '.*Operational Mode.*': lambda msg: '1', - # 11. Certificate Authority Trust + # 11. Permission mode + '.*Permission Mode.*': lambda msg: '1', + # 12. Internal certificate issuer kind (appears when Permission Mode is cluster) + '.*Certificate issuer kind.*': lambda msg: '2', # Select ClusterIssuer + # 13. Certificate Authority Trust '.*Trust default CAs.*': lambda msg: 'y', - # 12. Cluster ingress certificate secret name + # 14. Cluster ingress certificate secret name '.*Cluster ingress certificate secret name.*': lambda msg: '', # Leave empty for auto-detection - # 13. Domain & certificate management + # 15. Domain & certificate management '.*Configure domain.*certificate management.*': lambda msg: 'n', # Skip domain/cert config for simplicity - # 14. SSO properties + # 16. SSO properties '.*Configure SSO properties.*': lambda msg: 'n', # Skip SSO config - # 15. Special characters for user IDs + # 17. Special characters for user IDs '.*Allow special characters for user IDs and usernames.*': lambda msg: 'n', - # 16. Guided Tour + # 18. Guided Tour '.*Enable Guided Tour.*': lambda msg: 'y', - # 17. Feature adoption metrics + # 19. Feature adoption metrics '.*Enable feature adoption metrics.*': lambda msg: 'y', - # 18. Deployment progression metrics + # 20. Deployment progression metrics '.*Enable deployment progression metrics.*': lambda msg: 'y', - # 19. Usability metrics + # 21. Usability metrics '.*Enable usability metrics.*': lambda msg: 'y', - # 20. Application selection + # 22. Application selection '.*Install IoT.*': lambda msg: 'y', '.*Custom channel for iot.*': lambda msg: '9.2.x-dev', '.*Install Monitor.*': lambda msg: 'y', @@ -310,12 +314,12 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): '.*Install Visual Inspection.*': lambda msg: 'n', '.*Install.*Real Estate and Facilities.*': lambda msg: 'n', '.*Install AI Service.*': lambda msg: 'n', - # 20a. Grafana configuration (appears when advanced options are enabled) + # 23. Grafana configuration (appears when advanced options are enabled) '.*Install Grafana.*': lambda msg: 'y', - # 21. MongoDB configuration + # 24. MongoDB configuration '.*MongoDb namespace.*': lambda msg: 'mongoce', # Use default MongoDB namespace '.*Create MongoDb cluster.*': lambda msg: 'y', - # 22. Db2 configuration + # 25. Db2 configuration '.*Create system Db2 instance.*': lambda msg: 'y', '.*Re-use System Db2 instance for Manage application.*': lambda msg: 'n', '.*Create Manage dedicated Db2 instance.*': lambda msg: 'y', @@ -329,10 +333,10 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): '.*Select Kafka provider.*': lambda msg: '1', # Select default Kafka provider '.*Strimzi namespace.*': lambda msg: 'strimzi', # Strimzi namespace '.*Use pod templates.*': lambda msg: 'n', # Skip pod templates - # 23. Kafka configuration + # 26. Kafka configuration '.*Create system Kafka instance.*': lambda msg: 'y', '.*Kafka version.*': lambda msg: '3.8.0', - # 24. Final confirmation + # 27. Final confirmation '.*Use additional configurations.*': lambda msg: 'n', ".*Proceed with these settings.*": lambda msg: 'y', } @@ -385,7 +389,6 @@ def test_install_master_dev_mode_non_interactive(tmpdir): "--superuser-username", "MAS_SUPERUSER_USERNAME", "--superuser-password", "MAS_SUPERUSER_PASSWORD", "--mas-channel", "9.2.x-dev", - "--assist-channel", "9.2.x-dev", "--iot-channel", "9.2.x-dev", "--db2-system", "--kafka-provider", "strimzi", "--monitor-channel", "9.2.x-dev", @@ -422,7 +425,6 @@ def test_install_master_dev_mode_non_interactive(tmpdir): "--sls-namespace", "sls-fvtcore", "--sls-channel", "3.x-dev", "--approval-core", "100:300:true", - "--approval-assist", "100:300:true", "--approval-iot", "100:300:true", "--approval-manage", "100:600:true", "--approval-monitor", "100:300:true", @@ -512,7 +514,6 @@ def test_install_master_dev_mode_non_interactive_with_path_routing(tmpdir): "--sls-namespace", "sls-fvtcore", "--sls-channel", "3.x-dev", "--approval-core", "100:300:true", - "--approval-assist", "100:300:true", "--approval-iot", "100:300:true", "--approval-manage", "100:600:true", "--approval-monitor", "100:300:true", diff --git a/python/test/install/test_existing_catalog.py b/python/test/install/test_existing_catalog.py index 0b0a2c03c17..cfb66554493 100644 --- a/python/test/install/test_existing_catalog.py +++ b/python/test/install/test_existing_catalog.py @@ -60,11 +60,11 @@ def test_install_interactive_existing_catalog(tmpdir): '.*Install Visual Inspection.*': lambda msg: 'n', '.*Install.*Real Estate and Facilities.*': lambda msg: 'n', '.*Install AI Service.*': lambda msg: 'n', - # 11a. Grafana configuration + # 12. Grafana configuration '.*Install Grafana.*': lambda msg: 'y', - # 12. MongoDB configuration + # 13. MongoDB configuration '.*Create MongoDb cluster.*': lambda msg: 'y', - # 13. Db2 configuration + # 14. Db2 configuration '.*Create Manage dedicated Db2 instance.*': lambda msg: 'y', # 15. Final confirmation '.*Use additional configurations.*': lambda msg: 'n', diff --git a/python/test/utils/install_test_helper.py b/python/test/utils/install_test_helper.py index 98cda3bf437..a398b3cb803 100644 --- a/python/test/utils/install_test_helper.py +++ b/python/test/utils/install_test_helper.py @@ -122,6 +122,7 @@ def setup_mocks(self): dynamic_client = MagicMock(DynamicClient) resources = MagicMock() dynamic_client.resources = resources + dynamic_client.client = MagicMock() # Create individual API mocks routes_api = MagicMock() diff --git a/tekton/src/params/install.yml.j2 b/tekton/src/params/install.yml.j2 index a119a6131e2..46593d67655 100644 --- a/tekton/src/params/install.yml.j2 +++ b/tekton/src/params/install.yml.j2 @@ -421,6 +421,10 @@ # MAS Configuration # ----------------------------------------------------------------------------- +- name: mas_issuer_kind + type: string + description: Certificate issuer kind for MAS + default: "" - name: mas_domain type: string default: "" diff --git a/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 b/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 index 68b8c124be4..0c52a5d5d6b 100644 --- a/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 +++ b/tekton/src/pipelines/taskdefs/core/suite-install.yml.j2 @@ -30,6 +30,8 @@ value: $(params.mas_usability_metrics) - name: mas_deployment_progression value: $(params.mas_deployment_progression) + - name: mas_issuer_kind + value: $(params.mas_issuer_kind) - name: mas_icr_cp value: $(params.mas_icr_cp) - name: mas_icr_cpopen diff --git a/tekton/src/tasks/suite-install.yml.j2 b/tekton/src/tasks/suite-install.yml.j2 index 5cba6115ada..2a8e61e3ce4 100644 --- a/tekton/src/tasks/suite-install.yml.j2 +++ b/tekton/src/tasks/suite-install.yml.j2 @@ -94,6 +94,10 @@ spec: type: string description: Flag to control metrics used to understand progression of tasks and workflows within the product default: "True" + - name: mas_issuer_kind + type: string + description: Certificate issuer kind for MAS Suite CR (Issuer or ClusterIssuer) + default: "" - name: ibm_entitlement_key type: string @@ -212,6 +216,8 @@ spec: value: $(params.mas_usability_metrics) - name: MAS_DEPLOYMENT_PROGRESSION value: $(params.mas_deployment_progression) + - name: MAS_ISSUER_KIND + value: $(params.mas_issuer_kind) - name: IBM_ENTITLEMENT_KEY value: $(params.ibm_entitlement_key)