diff --git a/.secrets.baseline b/.secrets.baseline
index f184613553..d784e33eec 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,9 +3,7 @@
"files": "build/bin/config/oscap/ssg-rhel9-ds.xml|^.secrets.baseline$|^docs/catalogs/",
"lines": null
},
-
- "generated_at": "2026-09-18T10:00:02Z",
-
+ "generated_at": "2026-09-25T20:06:58Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
diff --git a/Makefile b/Makefile
index 02b3357442..f7b7f5d526 100644
--- a/Makefile
+++ b/Makefile
@@ -82,3 +82,4 @@ endif
.PHONY: run
run:
podman run --rm -it -e IBM_ENTITLEMENT_KEY --pull Always quay.io/ibmmas/cli:master bash
+
diff --git a/python/src/mas/cli/upgrade/app.py b/python/src/mas/cli/upgrade/app.py
index d04e7c1cc8..5417bfb17f 100644
--- a/python/src/mas/cli/upgrade/app.py
+++ b/python/src/mas/cli/upgrade/app.py
@@ -129,6 +129,75 @@ def configPipelineStorageClass(self, instanceId: str) -> None:
self.pipelineStorageClass = suggestedStorageClass
self.pipelineStorageAccessMode = suggestedAccessMode
+ def configStorageClasses(self) -> None:
+ """
+ Determine storage_class_rwo and storage_class_rwx for Manage Foundation Db2/PVC provisioning.
+ Only called when Manage Foundation is being installed during upgrade (9.0 → 9.1).
+
+ Priority:
+ 1. CLI args (--storage-class-rwo / --storage-class-rwx) — already set in self.params
+ 2. Auto-detected from getDefaultStorageClasses()
+ 3. Interactive prompt (falls back to fatalError in --no-confirm mode)
+ """
+ # 1. Validate any CLI args already provided (independently — partial args are allowed)
+ if self.getParam("storage_class_rwo"):
+ if getStorageClass(self.dynamicClient, self.getParam("storage_class_rwo")) is None:
+ self.fatalError(f"Storage class '{self.getParam('storage_class_rwo')}' specified via --storage-class-rwo is not available on this cluster.")
+ return
+ if self.getParam("storage_class_rwx"):
+ if getStorageClass(self.dynamicClient, self.getParam("storage_class_rwx")) is None:
+ self.fatalError(f"Storage class '{self.getParam('storage_class_rwx')}' specified via --storage-class-rwx is not available on this cluster.")
+ return
+
+ # Both provided and valid — nothing more to do
+ if self.getParam("storage_class_rwo") and self.getParam("storage_class_rwx"):
+ logger.debug(f"Using storage classes from CLI args: RWO={self.getParam('storage_class_rwo')} RWX={self.getParam('storage_class_rwx')}")
+ return
+
+ # 2. Try auto-detection
+ defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient)
+ if defaultStorageClasses.provider is not None:
+ print_formatted_text(HTML(f"Storage provider auto-detected: {defaultStorageClasses.providerName}"))
+ print_formatted_text(HTML(f" - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}"))
+ print_formatted_text(HTML(f" - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}"))
+
+ useDetected = True
+ if not self.noConfirm:
+ useDetected = self.yesOrNo("Use the auto-detected storage classes")
+
+ if useDetected:
+ self.params["storage_class_rwo"] = defaultStorageClasses.rwo
+ self.params["storage_class_rwx"] = defaultStorageClasses.rwx
+ logger.debug(f"Using auto-detected storage classes: RWO={defaultStorageClasses.rwo} RWX={defaultStorageClasses.rwx}")
+ return
+
+ # 3. No provider detected or user declined — fail in non-interactive mode
+ if self.noConfirm:
+ self.fatalError(
+ "No storage classes could be detected for Manage Foundation Db2 instances and --no-confirm is set. "
+ "Please re-run with --storage-class-rwo and --storage-class-rwx."
+ )
+ return
+
+ self.printDescription(["Select the ReadWriteOnce and ReadWriteMany storage classes to use from the list below:"])
+ for storageClass in getStorageClasses(self.dynamicClient):
+ print_formatted_text(HTML(f" - {storageClass.metadata.name}"))
+ print()
+
+ validator = StorageClassValidator()
+ if not self.getParam("storage_class_rwo"):
+ self.params["storage_class_rwo"] = prompt(
+ HTML("ReadWriteOnce (RWO) storage class "),
+ validator=validator,
+ validate_while_typing=False,
+ )
+ if not self.getParam("storage_class_rwx"):
+ self.params["storage_class_rwx"] = prompt(
+ HTML("ReadWriteMany (RWX) storage class "),
+ validator=validator,
+ validate_while_typing=False,
+ )
+
def computeMonitorInstallOrderForUpgrade(self, instanceId):
"""
Determine the installation order for Monitor relative to IoT based on TARGET Monitor version.
@@ -245,6 +314,12 @@ def upgrade(self, argv):
self.pipelineStorageClass = args.storage_pipeline or None
self.pipelineStorageAccessMode = args.storage_accessmode or None
+ # Pre-populate RWO/RWX storage classes from CLI args (used for Manage Foundation Db2 instances)
+ if args.storage_class_rwo:
+ self.params["storage_class_rwo"] = args.storage_class_rwo
+ if args.storage_class_rwx:
+ self.params["storage_class_rwx"] = args.storage_class_rwx
+
# Set image_pull_policy if provided
if args.image_pull_policy and args.image_pull_policy != "":
self.setParam("image_pull_policy", args.image_pull_policy)
@@ -430,7 +505,7 @@ def channelOrder(ch):
self.setParam("mas_app_settings_aio_flag", "false")
self.setParam("mas_app_channel_manage", self.nextChannel)
self.setParam("mas_workspace_id", getWorkspaceId(self.dynamicClient, instanceId))
-
+ self.configStorageClasses()
self.configDb2(silentMode=True)
# Compute Monitor install order for upgrade
diff --git a/python/src/mas/cli/upgrade/argParser.py b/python/src/mas/cli/upgrade/argParser.py
index 491534a404..48117ac8f7 100644
--- a/python/src/mas/cli/upgrade/argParser.py
+++ b/python/src/mas/cli/upgrade/argParser.py
@@ -59,7 +59,9 @@
help="Configure upgrade for development mode",
)
masArgGroup.add_argument("--next-channel", required=False, default="", help="The Target Mas channel to Upgrade on")
-storageArgGroup = upgradeArgParser.add_argument_group("Storage", "Storage class configuration for the upgrade pipeline PVC.")
+storageArgGroup = upgradeArgParser.add_argument_group(
+ "Storage", "Storage class configuration for the upgrade pipeline PVC and Manage Foundation Db2 instances."
+)
storageArgGroup.add_argument(
"--storage-pipeline",
required=False,
@@ -74,6 +76,18 @@
metavar="{ReadWriteMany,ReadWriteOnce}",
help="Pipeline storage class access mode (ReadWriteMany or ReadWriteOnce). " "Auto-detected from the existing config-pvc when omitted.",
)
+storageArgGroup.add_argument(
+ "--storage-class-rwo",
+ required=False,
+ dest="storage_class_rwo",
+ help="ReadWriteOnce storage class used for Db2 and Manage Foundation PVCs when installing Manage Foundation during upgrade (9.0 to 9.1). Auto-detected when omitted.",
+)
+storageArgGroup.add_argument(
+ "--storage-class-rwx",
+ required=False,
+ dest="storage_class_rwx",
+ help="ReadWriteMany storage class used for Db2 and Manage Foundation PVCs when installing Manage Foundation during upgrade (9.0 to 9.1). Auto-detected when omitted.",
+)
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/tests/integration/upgrade/test_config_storage_classes.py b/python/tests/integration/upgrade/test_config_storage_classes.py
new file mode 100644
index 0000000000..3c3517366c
--- /dev/null
+++ b/python/tests/integration/upgrade/test_config_storage_classes.py
@@ -0,0 +1,254 @@
+# *****************************************************************************
+# 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
+#
+# *****************************************************************************
+
+"""
+Tests for UpgradeApp.configStorageClasses()
+
+Covers all resolution paths:
+ 1. CLI args supplied (--storage-class-rwo / --storage-class-rwx) — both valid on cluster
+ 2. CLI args supplied but one is invalid → fatalError
+ 3. Auto-detected provider, --no-confirm accepts automatically
+ 4. Auto-detected provider, interactive user accepts
+ 5. Auto-detected provider, interactive user declines → falls through to manual prompt
+ 6. No provider detected, --no-confirm → fatalError
+ 7. No provider detected, interactive → manual prompt
+"""
+
+import pytest
+from unittest.mock import Mock, patch, MagicMock
+from mas.cli.upgrade.app import UpgradeApp
+
+
+def _make_app(no_confirm=True):
+ """Create a minimal UpgradeApp with a mock dynamic client."""
+ with patch("mas.cli.cli.which", return_value="/usr/bin/kubectl"):
+ app = UpgradeApp()
+ app._dynClient = MagicMock()
+ app.noConfirm = no_confirm
+ app.params = {}
+ app.fatalError = Mock(side_effect=SystemExit(1))
+ return app
+
+
+def _make_storage_classes(provider="ibmcloud", provider_name="IBM Cloud", rwx="ibmc-file-gold-gid", rwo="ibmc-block-gold"):
+ sc = MagicMock()
+ sc.provider = provider
+ sc.providerName = provider_name
+ sc.rwx = rwx
+ sc.rwo = rwo
+ return sc
+
+
+class TestConfigStorageClassesFromCLIArgs:
+ """Path 1 — both --storage-class-rwo and --storage-class-rwx already in self.params."""
+
+ def test_uses_cli_args_without_calling_auto_detect(self):
+ app = _make_app()
+ app.params["storage_class_rwo"] = "my-block-class"
+ app.params["storage_class_rwx"] = "my-file-class"
+
+ with patch("mas.cli.upgrade.app.getStorageClass", return_value=Mock()):
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses") as mock_detect:
+ app.configStorageClasses()
+
+ mock_detect.assert_not_called()
+ assert app.params["storage_class_rwo"] == "my-block-class"
+ assert app.params["storage_class_rwx"] == "my-file-class"
+
+ def test_validates_both_classes_exist_on_cluster(self):
+ app = _make_app()
+ app.params["storage_class_rwo"] = "my-block-class"
+ app.params["storage_class_rwx"] = "my-file-class"
+
+ with patch("mas.cli.upgrade.app.getStorageClass", return_value=Mock()) as mock_sc:
+ app.configStorageClasses()
+
+ assert mock_sc.call_count == 2
+ mock_sc.assert_any_call(app.dynamicClient, "my-block-class")
+ mock_sc.assert_any_call(app.dynamicClient, "my-file-class")
+
+
+class TestConfigStorageClassesFromCLIArgsInvalid:
+ """Path 2 — CLI args provided but a class does not exist on the cluster."""
+
+ def test_fatal_error_when_rwo_not_found(self):
+ app = _make_app()
+ app.params["storage_class_rwo"] = "bad-rwo"
+ app.params["storage_class_rwx"] = "my-file-class"
+
+ def _sc_lookup(client, name):
+ return None if name == "bad-rwo" else Mock()
+
+ with patch("mas.cli.upgrade.app.getStorageClass", side_effect=_sc_lookup):
+ with pytest.raises(SystemExit):
+ app.configStorageClasses()
+
+ app.fatalError.assert_called_once()
+ assert "--storage-class-rwo" in app.fatalError.call_args[0][0]
+
+ def test_fatal_error_when_rwx_not_found(self):
+ app = _make_app()
+ app.params["storage_class_rwo"] = "my-block-class"
+ app.params["storage_class_rwx"] = "bad-rwx"
+
+ def _sc_lookup(client, name):
+ return None if name == "bad-rwx" else Mock()
+
+ with patch("mas.cli.upgrade.app.getStorageClass", side_effect=_sc_lookup):
+ with pytest.raises(SystemExit):
+ app.configStorageClasses()
+
+ app.fatalError.assert_called_once()
+ assert "--storage-class-rwx" in app.fatalError.call_args[0][0]
+
+
+class TestConfigStorageClassesAutoDetectNoConfirm:
+ """Path 3 — auto-detected provider, --no-confirm accepts automatically."""
+
+ def test_sets_rwo_and_rwx_from_auto_detected_provider(self):
+ app = _make_app(no_confirm=True)
+ sc = _make_storage_classes()
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with patch("mas.cli.upgrade.app.print_formatted_text"):
+ app.configStorageClasses()
+
+ assert app.params["storage_class_rwo"] == "ibmc-block-gold"
+ assert app.params["storage_class_rwx"] == "ibmc-file-gold-gid"
+
+ def test_does_not_prompt_in_no_confirm_mode(self):
+ app = _make_app(no_confirm=True)
+ sc = _make_storage_classes()
+ app.yesOrNo = Mock()
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with patch("mas.cli.upgrade.app.print_formatted_text"):
+ app.configStorageClasses()
+
+ app.yesOrNo.assert_not_called()
+
+
+class TestConfigStorageClassesAutoDetectInteractiveAccept:
+ """Path 4 — auto-detected provider, interactive user accepts."""
+
+ def test_sets_params_when_user_accepts_detected_classes(self):
+ app = _make_app(no_confirm=False)
+ sc = _make_storage_classes()
+ app.yesOrNo = Mock(return_value=True)
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with patch("mas.cli.upgrade.app.print_formatted_text"):
+ app.configStorageClasses()
+
+ app.yesOrNo.assert_called_once_with("Use the auto-detected storage classes")
+ assert app.params["storage_class_rwo"] == "ibmc-block-gold"
+ assert app.params["storage_class_rwx"] == "ibmc-file-gold-gid"
+
+
+class TestConfigStorageClassesAutoDetectInteractiveDecline:
+ """Path 5 — auto-detected provider, interactive user declines → manual prompt."""
+
+ def test_falls_through_to_manual_prompt_when_user_declines(self):
+ app = _make_app(no_confirm=False)
+ sc = _make_storage_classes()
+ app.yesOrNo = Mock(return_value=False)
+ app.printDescription = Mock()
+
+ mock_sc_entry = MagicMock()
+ mock_sc_entry.metadata.name = "thin"
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with patch("mas.cli.upgrade.app.getStorageClasses", return_value=[mock_sc_entry]):
+ with patch("mas.cli.upgrade.app.print_formatted_text"):
+ with patch("mas.cli.upgrade.app.prompt", side_effect=["custom-rwo", "custom-rwx"]):
+ app.configStorageClasses()
+
+ assert app.params["storage_class_rwo"] == "custom-rwo"
+ assert app.params["storage_class_rwx"] == "custom-rwx"
+
+
+class TestConfigStorageClassesNoDetectionNoConfirm:
+ """Path 6 — no provider detected, --no-confirm → fatalError."""
+
+ def test_fatal_error_when_no_provider_and_no_confirm(self):
+ app = _make_app(no_confirm=True)
+ sc = MagicMock()
+ sc.provider = None
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with pytest.raises(SystemExit):
+ app.configStorageClasses()
+
+ app.fatalError.assert_called_once()
+ assert "--storage-class-rwo" in app.fatalError.call_args[0][0]
+ assert "--storage-class-rwx" in app.fatalError.call_args[0][0]
+
+
+class TestConfigStorageClassesNoDetectionInteractive:
+ """Path 7 — no provider detected, interactive → manual prompt for both classes."""
+
+ def test_prompts_for_rwo_and_rwx_when_no_provider_detected(self):
+ app = _make_app(no_confirm=False)
+ sc = MagicMock()
+ sc.provider = None
+ app.printDescription = Mock()
+
+ mock_sc_entry = MagicMock()
+ mock_sc_entry.metadata.name = "thin"
+
+ with patch("mas.cli.upgrade.app.getDefaultStorageClasses", return_value=sc):
+ with patch("mas.cli.upgrade.app.getStorageClasses", return_value=[mock_sc_entry]):
+ with patch("mas.cli.upgrade.app.print_formatted_text"):
+ with patch("mas.cli.upgrade.app.prompt", side_effect=["my-rwo", "my-rwx"]):
+ app.configStorageClasses()
+
+ assert app.params["storage_class_rwo"] == "my-rwo"
+ assert app.params["storage_class_rwx"] == "my-rwx"
+
+
+class TestArgParserStorageClassArgs:
+ """Verify --storage-class-rwo and --storage-class-rwx are accepted by the arg parser."""
+
+ def test_storage_class_args_parsed_correctly(self):
+ from mas.cli.upgrade.argParser import upgradeArgParser
+
+ args = upgradeArgParser.parse_args(["--mas-instance-id", "test", "--storage-class-rwo", "my-block", "--storage-class-rwx", "my-file"])
+ assert args.storage_class_rwo == "my-block"
+ assert args.storage_class_rwx == "my-file"
+
+ def test_storage_class_args_default_to_none(self):
+ from mas.cli.upgrade.argParser import upgradeArgParser
+
+ args = upgradeArgParser.parse_args(["--mas-instance-id", "test"])
+ assert args.storage_class_rwo is None
+ assert args.storage_class_rwx is None
+
+ def test_storage_class_args_independent_of_pipeline_args(self):
+ """--storage-class-rwo/rwx are independent from --storage-pipeline."""
+ from mas.cli.upgrade.argParser import upgradeArgParser
+
+ args = upgradeArgParser.parse_args(
+ [
+ "--mas-instance-id",
+ "test",
+ "--storage-pipeline",
+ "ibmc-file-gold-gid",
+ "--storage-accessmode",
+ "ReadWriteMany",
+ "--storage-class-rwo",
+ "ibmc-block-gold",
+ "--storage-class-rwx",
+ "ibmc-file-gold-gid",
+ ]
+ )
+ assert args.storage_pipeline == "ibmc-file-gold-gid"
+ assert args.storage_accessmode == "ReadWriteMany"
+ assert args.storage_class_rwo == "ibmc-block-gold"
+ assert args.storage_class_rwx == "ibmc-file-gold-gid"