From 06ca37dcae4ea70a5b1d5ad64cb8eaeada8ee550 Mon Sep 17 00:00:00 2001
From: terc1997 <64480693+terc1997@users.noreply.github.com>
Date: Fri, 25 Sep 2026 17:07:26 -0300
Subject: [PATCH 1/4] [patch] add support for storage class in upgrade
---
.secrets.baseline | 2 +-
python/src/mas/cli/upgrade/app.py | 67 ++++-
python/src/mas/cli/upgrade/argParser.py | 16 +-
.../upgrade/test_config_storage_classes.py | 254 ++++++++++++++++++
4 files changed, 336 insertions(+), 3 deletions(-)
create mode 100644 python/tests/integration/upgrade/test_config_storage_classes.py
diff --git a/.secrets.baseline b/.secrets.baseline
index a637da4a06..4322b5438d 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,7 +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/python/src/mas/cli/upgrade/app.py b/python/src/mas/cli/upgrade/app.py
index d04e7c1cc8..cf306e0e11 100644
--- a/python/src/mas/cli/upgrade/app.py
+++ b/python/src/mas/cli/upgrade/app.py
@@ -129,6 +129,65 @@ 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. Both already provided via CLI args — validate and return
+ if self.getParam("storage_class_rwo") and self.getParam("storage_class_rwx"):
+ 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.")
+ 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.")
+ 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."
+ )
+
+ 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()
+
+ self.params["storage_class_rwo"] = prompt(
+ HTML("ReadWriteOnce (RWO) storage class "),
+ validator=StorageClassValidator(),
+ validate_while_typing=False,
+ )
+ self.params["storage_class_rwx"] = prompt(
+ HTML("ReadWriteMany (RWX) storage class "),
+ validator=StorageClassValidator(),
+ validate_while_typing=False,
+ )
+
def computeMonitorInstallOrderForUpgrade(self, instanceId):
"""
Determine the installation order for Monitor relative to IoT based on TARGET Monitor version.
@@ -245,6 +304,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 +495,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"
From e77734a76d8c72196d9412e3af65e0019b05f16d Mon Sep 17 00:00:00 2001
From: terc1997 <64480693+terc1997@users.noreply.github.com>
Date: Tue, 29 Sep 2026 13:55:34 -0300
Subject: [PATCH 2/4] [patch] allow partial storage class args in upgrade
---
python/src/mas/cli/upgrade/app.py | 34 ++++++++++++++++++++-----------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/python/src/mas/cli/upgrade/app.py b/python/src/mas/cli/upgrade/app.py
index cf306e0e11..181760b962 100644
--- a/python/src/mas/cli/upgrade/app.py
+++ b/python/src/mas/cli/upgrade/app.py
@@ -139,12 +139,18 @@ def configStorageClasses(self) -> None:
2. Auto-detected from getDefaultStorageClasses()
3. Interactive prompt (falls back to fatalError in --no-confirm mode)
"""
- # 1. Both already provided via CLI args — validate and return
- if self.getParam("storage_class_rwo") and self.getParam("storage_class_rwx"):
+ # 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
@@ -171,22 +177,26 @@ def configStorageClasses(self) -> None:
"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()
- self.params["storage_class_rwo"] = prompt(
- HTML("ReadWriteOnce (RWO) storage class "),
- validator=StorageClassValidator(),
- validate_while_typing=False,
- )
- self.params["storage_class_rwx"] = prompt(
- HTML("ReadWriteMany (RWX) storage class "),
- validator=StorageClassValidator(),
- validate_while_typing=False,
- )
+ validator = StorageClassValidator(dynamic_client=self.dynamicClient)
+ 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):
"""
From f312bef83f83fee2b41dafab8917ddcc939b9188 Mon Sep 17 00:00:00 2001
From: terc1997 <64480693+terc1997@users.noreply.github.com>
Date: Tue, 29 Sep 2026 14:02:44 -0300
Subject: [PATCH 3/4] [patch] remove unused dynamic_client param from
StorageClassValidator
---
python/src/mas/cli/upgrade/app.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/src/mas/cli/upgrade/app.py b/python/src/mas/cli/upgrade/app.py
index 181760b962..5417bfb17f 100644
--- a/python/src/mas/cli/upgrade/app.py
+++ b/python/src/mas/cli/upgrade/app.py
@@ -184,7 +184,7 @@ def configStorageClasses(self) -> None:
print_formatted_text(HTML(f" - {storageClass.metadata.name}"))
print()
- validator = StorageClassValidator(dynamic_client=self.dynamicClient)
+ validator = StorageClassValidator()
if not self.getParam("storage_class_rwo"):
self.params["storage_class_rwo"] = prompt(
HTML("ReadWriteOnce (RWO) storage class "),
From 1fe5ebff5714efda3a32c048256cae713951a1cc Mon Sep 17 00:00:00 2001
From: terc1997 <64480693+terc1997@users.noreply.github.com>
Date: Wed, 30 Sep 2026 09:01:00 -0300
Subject: [PATCH 4/4] [patch] trigger build
---
Makefile | 1 +
1 file changed, 1 insertion(+)
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
+