From c1fe73221547f656cd8ab4dd912f4d0df37e4b0c Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:44:27 -0300 Subject: [PATCH 01/22] [patch] support db2 upgrade to v12 --- python/src/mas/cli/update/app.py | 119 ++++++++++++++++++++--- tekton/src/pipelines/mas-update.yml.j2 | 6 ++ tekton/src/tasks/dependencies/db2.yml.j2 | 10 ++ 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 040a521f94b..7615c402e50 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -45,6 +45,7 @@ def update(self, argv): requiredParams = ["mas_catalog_version"] optionalParams = [ "db2_namespace", + "db2_v12_upgrade", "mongodb_namespace", "mongodb_v5_upgrade", "mongodb_v6_upgrade", @@ -134,8 +135,8 @@ def update(self, argv): self.detectGrafana4() self.detectMongoDb() - self.detectDb2uOrKafka("db2") - self.detectDb2uOrKafka("kafka") + self.detectDb2u() + self.detectKafka() self.detectCP4D() print() @@ -556,19 +557,13 @@ def detectCpdService(self, kind: str, api: str, name: str, param: str) -> None: logger.debug(f"{name} is not included in CP4D update: {e}") self.setParam(param, "false") - def detectDb2uOrKafka(self, mode: str) -> bool: - if mode == "db2": - haloStartingMessage = "Checking for Db2uCluster instances to update" - apiVersion = "db2u.databases.ibm.com/v1" - kinds = ["Db2uCluster", "Db2uInstance"] - paramName = "db2_namespace" - elif mode == "kafka": - haloStartingMessage = "Checking for Kafka instances to update" - apiVersion = "kafka.strimzi.io/v1beta2" - kinds = ["Kafka"] - paramName = "kafka_namespace" - else: - self.fatalError("Unexpected error") + def detectDb2u(self) -> None: + """Detect Db2uCluster and Db2uInstance instances to update.""" + haloStartingMessage = "Checking for Db2uCluster instances to update" + apiVersion = "db2u.databases.ibm.com/v1" + kinds = ["Db2uCluster", "Db2uInstance"] + paramName = "db2_namespace" + mode = "db2" with Halo(text=haloStartingMessage, spinner=self.spinner) as h: try: @@ -607,6 +602,45 @@ def detectDb2uOrKafka(self, mode: str) -> bool: for index, ns in enumerate(sorted(namespaces), start=1): self.printDescription([f"{index}. {ns}"]) self.promptForListSelect("Select namespace", sorted(namespaces), paramName) + + # Version comparison logic - check if Db2u needs major version upgrade + if len(instances) > 0: + # Get the current Db2u version from the first instance + currentDb2uVersion = None + if "spec" in instances[0] and "version" in instances[0]["spec"]: + currentDb2uVersion = instances[0]["spec"]["version"] + + if currentDb2uVersion != "": + # Get target Db2u version from catalog + targetDb2uVersion = self.chosenCatalog["db2_channel_default"] + + logger.debug(f"Current Db2u version: {currentDb2uVersion}, Target version from catalog: {targetDb2uVersion}") + + # Extract major version numbers + # Handle version formats like "11.5.8.0", "12.0.0.0", "v11.5", "v12.0" + currentVersionStr = currentDb2uVersion.lstrip('v') + targetVersionStr = targetDb2uVersion.lstrip('v') + try: + currentMajorVersion = int(currentVersionStr.split('.')[0]) + targetMajorVersion = int(targetVersionStr.split('.')[0]) + except (ValueError): + h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 channel doesn't map to a Db2. Version name {targetMajorVersion}") + # Check if upgrade to version or higher is needed + if targetMajorVersion > currentMajorVersion: + if self.noConfirm and self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": + h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 {currentMajorVersion} needs to be updated to {targetMajorVersion}") + self.fatalError(f"By choosing {self.getParam('mas_catalog_version')} you must confirm Db2 update to version {targetMajorVersion}") + elif self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": + h.stop_and_persist(symbol=self.successIcon, text=f"Db2 {currentMajorVersion} needs to be updated to {targetMajorVersion}") + if not self.yesOrNo(f"Confirm update from Db2 {currentMajorVersion} to {targetMajorVersion}", f"db2_v{targetMajorVersion}_upgrade"): + exit(1) + print() + logger.debug(f"Db2u major version upgrade required: {currentMajorVersion} -> {targetMajorVersion}") + else: + self.setParam(f"db2_v{targetMajorVersion}_upgrade", "false") + logger.debug("No Db2u major version upgrade required") + else: + logger.debug("Unable to determine current Db2u version from instance") else: logger.debug(f"Found no instances of {kindString} to update") h.stop_and_persist(symbol=self.successIcon, text=f"Found no {kindString} ({apiVersion}) instances to update") @@ -615,8 +649,57 @@ def detectDb2uOrKafka(self, mode: str) -> bool: logger.debug(f"{'[' + kindString + ']'}.{apiVersion} is not available in the cluster") h.stop_and_persist(symbol=self.successIcon, text=f"{kindString}.{apiVersion} is not available in the cluster") + def detectKafka(self) -> None: + """Detect Kafka instances to update and determine the provider.""" + haloStartingMessage = "Checking for Kafka instances to update" + apiVersion = "kafka.strimzi.io/v1beta2" + kind = "Kafka" + paramName = "kafka_namespace" + mode = "kafka" + + with Halo(text=haloStartingMessage, spinner=self.spinner) as h: + try: + k8sAPI = self.dynamicClient.resources.get(api_version=apiVersion, kind=kind) + instances = k8sAPI.get().to_dict()["items"] + logger.debug(f"Found {len(instances)} {kind} instances on the cluster") + + if len(instances) > 0: + # If the user provided the namespace using --kafka-namespace then we don't have any work to do here + if self.getParam(paramName) == "": + namespaces = set() + for instance in instances: + namespaces.add(instance["metadata"]["namespace"]) + + if len(namespaces) == 1: + # If kafka is only in one namespace, we will update that + h.stop_and_persist(symbol=self.successIcon, text=f"{len(instances)} {kind}s ({apiVersion}) in namespace '{list(namespaces)[0]}' will be updated") + logger.debug(f"There is only one namespace containing {kind}s so we will target that one: {namespaces}") + self.setParam(paramName, list(namespaces)[0]) + elif self.noConfirm: + # If kafka is in multiple namespaces and user has disabled prompts then we must error + namespaceList = ", ".join(list(namespaces)) + h.stop_and_persist(symbol=self.failureIcon, text=f"{len(instances)} {kind}s ({apiVersion}) were found in multiple namespaces") + logger.warning(f"There are multiple namespaces containing {kind}s and user has enable --no-confirm without setting --{mode}-namespace: {namespaceList}") + self.fatalError(f"{kind}s are installed in multiple namespaces. You must instruct which one to update using the '--{mode}-namespace' argument") + else: + # Otherwise, provide user the list of namespaces we found and ask them to pick on + h.stop_and_persist(symbol=self.successIcon, text=f"{len(instances)} {kind}s ({apiVersion}) found in multiple namespaces") + logger.debug(f"There are multiple namespaces containing {kind}s, user must choose: {namespaces}") + self.printDescription([ + f"{kind}s were found in multiple namespaces, select the namespace to target from the list below:" + ]) + for index, ns in enumerate(sorted(namespaces), start=1): + self.printDescription([f"{index}. {ns}"]) + self.promptForListSelect("Select namespace", sorted(namespaces), paramName) + else: + logger.debug(f"Found no instances of {kind}s to update") + h.stop_and_persist(symbol=self.successIcon, text=f"Found no {kind}s ({apiVersion}) instances to update") + except (ResourceNotFoundError, NotFoundError): + logger.debug(f"{kind}.{apiVersion} is not available in the cluster") + h.stop_and_persist(symbol=self.successIcon, text=f"{kind}.{apiVersion} is not available in the cluster") + # With Kafka we also have to determine the provider (strimzi or redhat) - if mode == "kafka" and self.getParam("kafka_namespace") != "" and self.getParam("kafka_provider") == "": + if self.getParam("kafka_namespace") != "" and self.getParam("kafka_provider") == "": try: subAPI = self.dynamicClient.resources.get(api_version="operators.coreos.com/v1alpha1", kind="Subscription") subs = subAPI.get().to_dict()["items"] @@ -632,3 +715,7 @@ def detectDb2uOrKafka(self, mode: str) -> bool: # If the param is still undefined then there is a big problem if self.getParam("kafka_provider") == "": self.fatalError("Unable to determine whether the installed Kafka instance is managed by Strimzi or Red Hat AMQ Streams") + + # If the param is still undefined then there is a big problem + if self.getParam("kafka_provider") == "": + self.fatalError("Unable to determine whether the installed Kafka instance is managed by Strimzi or Red Hat AMQ Streams") diff --git a/tekton/src/pipelines/mas-update.yml.j2 b/tekton/src/pipelines/mas-update.yml.j2 index 2fdccbd1632..639c8b728ad 100644 --- a/tekton/src/pipelines/mas-update.yml.j2 +++ b/tekton/src/pipelines/mas-update.yml.j2 @@ -47,6 +47,10 @@ spec: type: string default: "db2u" description: Namespace where db2 instances will be updated + - name: db2_v12_upgrade + type: string + description: Approves the Db2 upgrade to version 12 if needed + default: "" # mongodb update # ------------------------------------------------------------------------- @@ -243,6 +247,8 @@ spec: value: $(params.db2_action) - name: db2_namespace value: $(params.db2_namespace) + - name: db2_v12_upgrade + value: $(params.db2_v12_upgrade) - name: update-mongodb timeout: "0" diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index e708019f5ac..596185a7d44 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -168,6 +168,12 @@ spec: description: Optional MAS custom labels, comma separated list of key=value pairs default: "" + # Other Db2 parameters + - name: db2_v12_upgrade + type: string + description: Approves the Db2 upgrade to version 12 if needed + default: "" + stepTemplate: env: {{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(6) }} @@ -285,6 +291,10 @@ spec: # Custom labels support - name: CUSTOM_LABELS value: $(params.custom_labels) + + # Other parameters + - name: DB2_V12_UPGRADE + value: $(params.db2_v12_upgrade) steps: - name: db2 command: From 49f43f2531bd6e545abf2cbd325cdf7ebb65c6a0 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:06:25 -0300 Subject: [PATCH 02/22] [patch] update unit tests --- python/src/mas/cli/update/app.py | 1 - python/src/mas/cli/update/argParser.py | 8 + python/test/update/test_db2u_interactive.py | 215 +++++++++++++++++- .../test/update/test_db2u_non_interactive.py | 149 ++++++++++++ python/test/utils/update_test_helper.py | 11 +- 5 files changed, 378 insertions(+), 6 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 7615c402e50..e7a3e2d3ad4 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -61,7 +61,6 @@ def update(self, argv): # Dev Mode "artifactory_username", "artifactory_token" - ] for key, value in vars(self.args).items(): # These fields we just pass straight through to the parameters and fail if they are not set diff --git a/python/src/mas/cli/update/argParser.py b/python/src/mas/cli/update/argParser.py index 8487cd892e9..8bdcc760b17 100644 --- a/python/src/mas/cli/update/argParser.py +++ b/python/src/mas/cli/update/argParser.py @@ -114,6 +114,14 @@ def parse_args(self, args=None, namespace=None): # type: ignore[override] help="Namespace where Db2u operator and instances will be updated", ) +depsArgGroup.add_argument( + '--db2-v12-upgrade', + required=False, + action="store_const", + const="true", + help="Required to confirm a major version update for Db2 to version 12", +) + depsArgGroup.add_argument( '--mongodb-namespace', required=False, diff --git a/python/test/update/test_db2u_interactive.py b/python/test/update/test_db2u_interactive.py index 88f5a1b1de6..daa1cba182c 100644 --- a/python/test/update/test_db2u_interactive.py +++ b/python/test/update/test_db2u_interactive.py @@ -20,7 +20,14 @@ @pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) def test_db2u_one_namespace(tmpdir, resource_kind): - """Test interactive update when exactly one namespace contains Db2U resources.""" + """Test interactive update when exactly one namespace contains Db2U resources. + + Expected behavior: + - Automatically detects single namespace + - Sets db2_namespace parameter + - No namespace selection prompt needed + - Update proceeds successfully + """ prompt_handlers = { # Proceed with current cluster @@ -49,7 +56,15 @@ def test_db2u_one_namespace(tmpdir, resource_kind): @pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) def test_db2u_multiple_namespaces(tmpdir, resource_kind): - """Test interactive update when multiple namespaces contain Db2U resources.""" + """Test interactive update when multiple namespaces contain Db2U resources. + + Expected behavior: + - Detects resources in multiple namespaces + - Prompts user to select namespace + - User selects second namespace (db2u-ns2) + - Sets db2_namespace parameter to selected namespace + - Update proceeds successfully + """ prompt_handlers = { # Proceed with current cluster @@ -80,7 +95,14 @@ def test_db2u_multiple_namespaces(tmpdir, resource_kind): @pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) def test_db2u_none_found(tmpdir, resource_kind): - """Test interactive update when no Db2U resources exist.""" + """Test interactive update when no Db2U resources exist. + + Expected behavior: + - Detects no Db2U resources + - db2_namespace parameter remains empty + - No prompts for namespace selection + - Update continues without error (not a failure condition) + """ prompt_handlers = { # Proceed with current cluster @@ -107,4 +129,191 @@ def test_db2u_none_found(tmpdir, resource_kind): run_update_test(tmpdir, config) +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_accepted(tmpdir, resource_kind): + """Test interactive update with Db2 major version upgrade - user accepts. + + Expected behavior: + - Detects Db2 v11 needs upgrade to v12 + - Prompts user to confirm major version upgrade + - User accepts the upgrade + - Sets db2_v12_upgrade parameter to true + - Update proceeds successfully + """ + + prompt_handlers = { + # Proceed with current cluster + '.*Proceed with this cluster?.*': lambda msg: 'y', + # Catalog selection + '.*Select catalog version.*': lambda msg: '1', + # Db2 version upgrade confirmation + '.*Confirm update from Db2.*': lambda msg: 'y', + # Final confirmation + '.*Proceed with these settings.*': lambda msg: 'y', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", # Current version + db2u_target_version="v12.0", # Target requires upgrade + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_rejected(tmpdir, resource_kind): + """Test interactive update with Db2 major version upgrade - user rejects. + + Expected behavior: + - Detects Db2 v11 needs upgrade to v12 + - Prompts user to confirm major version upgrade + - User rejects the upgrade + - Raises SystemExit with exit code 1 + - Update does not proceed + """ + + prompt_handlers = { + # Proceed with current cluster + '.*Proceed with this cluster?.*': lambda msg: 'y', + # Catalog selection + '.*Select catalog version.*': lambda msg: '1', + # Db2 version upgrade confirmation - user rejects + '.*Confirm update from Db2.*': lambda msg: 'n', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", # Current version + db2u_target_version="v12.0", # Target requires upgrade + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + expect_system_exit=True, + expected_exit_code=1, + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_minor_version_upgrade_no_prompt(tmpdir, resource_kind): + """Test interactive update with Db2 minor version upgrade - no prompt needed. + + Expected behavior: + - Detects Db2 v11.5.8.0 needs upgrade to v11.5.9.0 + - No prompt for minor version upgrade + - Update proceeds automatically + """ + + prompt_handlers = { + # Proceed with current cluster + '.*Proceed with this cluster?.*': lambda msg: 'y', + # Catalog selection + '.*Select catalog version.*': lambda msg: '1', + # Final confirmation + '.*Proceed with these settings.*': lambda msg: 'y', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.8.0", # Current version + db2u_target_version="v11.5", # Same major version + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_multiple_namespaces_first_selection(tmpdir, resource_kind): + """Test interactive update - user selects first namespace from multiple. + + Expected behavior: + - Detects resources in multiple namespaces + - User selects first namespace (db2u-ns1) + - Sets db2_namespace parameter to first namespace + - Update proceeds successfully + """ + + prompt_handlers = { + '.*Proceed with this cluster?.*': lambda msg: 'y', + '.*Select catalog version.*': lambda msg: '1', + '.*Select namespace.*': lambda msg: '1', # Select first + '.*Proceed with these settings.*': lambda msg: 'y', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-ns1", "db2u-ns2"], + db2u_resource_kind=resource_kind, + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_multiple_namespaces_last_selection(tmpdir, resource_kind): + """Test interactive update - user selects last namespace from multiple. + + Expected behavior: + - Detects resources in multiple namespaces + - User selects last namespace (db2u-ns3) + - Sets db2_namespace parameter to last namespace + - Update proceeds successfully + """ + + prompt_handlers = { + '.*Proceed with this cluster?.*': lambda msg: 'y', + '.*Select catalog version.*': lambda msg: '1', + '.*Select namespace.*': lambda msg: '3', # Select last + '.*Proceed with these settings.*': lambda msg: 'y', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-ns1", "db2u-ns2", "db2u-ns3"], + db2u_resource_kind=resource_kind, + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + # Made with Bob diff --git a/python/test/update/test_db2u_non_interactive.py b/python/test/update/test_db2u_non_interactive.py index 29c292332b1..82b63217b65 100644 --- a/python/test/update/test_db2u_non_interactive.py +++ b/python/test/update/test_db2u_non_interactive.py @@ -170,4 +170,153 @@ def test_db2u_no_namespaces(tmpdir, resource_kind, with_arg): run_update_test(tmpdir, config) +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_without_flag(tmpdir, resource_kind): + """Test non-interactive update with Db2 major version upgrade but no flag - should fail. + + Expected behavior: + - Detects Db2 v11 needs upgrade to v12 + - No --db2-v12-upgrade flag provided + - Raises SystemExit with non-zero exit code + - Error message indicates --db2-v12-upgrade flag is required + """ + + config = UpdateTestConfig( + prompt_handlers={}, # No prompts in non-interactive mode + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", # Current version + db2u_target_version="v12.0", # Target requires upgrade + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=['--catalog', 'v9-260129-amd64', '--no-confirm'], + expect_system_exit=True, # Expect failure + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_with_flag(tmpdir, resource_kind): + """Test non-interactive update with Db2 major version upgrade and flag. + + Expected behavior: + - Detects Db2 v11 needs upgrade to v12 + - --db2-v12-upgrade flag provided + - Sets db2_v12_upgrade parameter to true + - Update proceeds successfully + """ + + config = UpdateTestConfig( + prompt_handlers={}, # No prompts in non-interactive mode + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", # Current version + db2u_target_version="v12.0", # Target requires upgrade + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=['--catalog', 'v9-260129-amd64', '--db2-v12-upgrade', '--no-confirm'], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_minor_version_upgrade_no_flag_needed(tmpdir, resource_kind): + """Test non-interactive update with Db2 minor version upgrade - no flag needed. + + Expected behavior: + - Detects Db2 v11.5.8.0 needs upgrade to v11.5.9.0 + - No flag required for minor version upgrade + - Update proceeds successfully + """ + + config = UpdateTestConfig( + prompt_handlers={}, # No prompts in non-interactive mode + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.8.0", # Current version + db2u_target_version="v11.5", # Same major version + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=['--catalog', 'v9-260129-amd64', '--no-confirm'], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_same_version_no_upgrade(tmpdir, resource_kind): + """Test non-interactive update when Db2 is already at target version. + + Expected behavior: + - Detects Db2 is already at target version + - No upgrade needed + - Update proceeds successfully + """ + + config = UpdateTestConfig( + prompt_handlers={}, # No prompts in non-interactive mode + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", # Current version + db2u_target_version="v11.5", # Same version + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=['--catalog', 'v9-260129-amd64', '--no-confirm'], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_combined_namespace_and_version_upgrade(tmpdir, resource_kind): + """Test non-interactive update with both namespace arg and version upgrade flag. + + Expected behavior: + - Uses explicit namespace argument + - Confirms major version upgrade with flag + - Update proceeds successfully + """ + + config = UpdateTestConfig( + prompt_handlers={}, # No prompts in non-interactive mode + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-ns1", "db2u-ns2"], # Multiple namespaces + db2u_resource_kind=resource_kind, + db2u_namespace_arg="db2u-ns1", # Explicit namespace + db2u_version="11.5.9.0", # Current version + db2u_target_version="v12.0", # Target requires upgrade + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=['--catalog', 'v9-260129-amd64', '--db2-namespace', 'db2u-ns1', '--db2-v12-upgrade', '--no-confirm'], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + # Made with Bob diff --git a/python/test/utils/update_test_helper.py b/python/test/utils/update_test_helper.py index ecc5c47dc60..f8f98ef8051 100644 --- a/python/test/utils/update_test_helper.py +++ b/python/test/utils/update_test_helper.py @@ -32,6 +32,8 @@ def __init__( db2u_namespaces: Optional[List[str]] = None, db2u_resource_kind: str = "Db2uCluster", db2u_namespace_arg: Optional[str] = None, + db2u_version: Optional[str] = None, + db2u_target_version: Optional[str] = None, kafka_namespaces: Optional[List[str]] = None, kafka_namespace_arg: Optional[str] = None, kafka_provider: Optional[str] = None, @@ -56,6 +58,8 @@ def __init__( db2u_namespaces: List of namespaces containing Db2U resources (None = no resources) db2u_resource_kind: Type of Db2U resource ("Db2uCluster" or "Db2uInstance") db2u_namespace_arg: Value for --db2-namespace CLI argument + db2u_version: Current Db2u version (e.g., "11.5.9.0") + db2u_target_version: Target Db2u version from catalog (e.g., "v12.0") kafka_namespaces: List of namespaces containing Kafka resources kafka_namespace_arg: Value for --kafka-namespace CLI argument kafka_provider: Kafka provider type ("strimzi" or "redhat") @@ -76,6 +80,8 @@ def __init__( self.db2u_namespaces = db2u_namespaces if db2u_namespaces is not None else [] self.db2u_resource_kind = db2u_resource_kind self.db2u_namespace_arg = db2u_namespace_arg + self.db2u_version = db2u_version if db2u_version is not None else "11.5.9.0" + self.db2u_target_version = db2u_target_version if db2u_target_version is not None else "v11.5" self.kafka_namespaces = kafka_namespaces if kafka_namespaces is not None else [] self.kafka_namespace_arg = kafka_namespace_arg self.kafka_provider = kafka_provider @@ -140,7 +146,7 @@ def create_db2u_resource(self, kind: str, name: str, namespace: str) -> Dict: "namespace": namespace }, "spec": { - "version": "11.5.9.0", + "version": self.config.db2u_version, "license": {"accept": True} }, "status": { @@ -495,7 +501,8 @@ def run_update_test(self): mocks['get_catalog'].return_value = { 'ocp_compatibility': ['4.16', '4.17', '4.18'], 'mongo_extras_version_default': '6.0.5', - 'cpd_product_version_default': '5.2.0' + 'cpd_product_version_default': '5.2.0', + 'db2_channel_default': self.config.db2u_target_version } # Pipeline setup From 0feade36f6eafe73aba1733d3fea6e7574cc6a9a Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:55:33 -0300 Subject: [PATCH 03/22] [patch] update logic for db2 --- python/src/mas/cli/update/app.py | 98 +++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 794ace191bd..31714e0700a 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -604,42 +604,76 @@ def detectDb2u(self) -> None: # Version comparison logic - check if Db2u needs major version upgrade if len(instances) > 0: - # Get the current Db2u version from the first instance - currentDb2uVersion = None - if "spec" in instances[0] and "version" in instances[0]["spec"]: - currentDb2uVersion = instances[0]["spec"]["version"] + # Get target Db2u version from catalog + targetDb2uVersion = self.chosenCatalog["db2_channel_default"] - if currentDb2uVersion != "": - # Get target Db2u version from catalog - targetDb2uVersion = self.chosenCatalog["db2_channel_default"] - - logger.debug(f"Current Db2u version: {currentDb2uVersion}, Target version from catalog: {targetDb2uVersion}") - - # Extract major version numbers - # Handle version formats like "11.5.8.0", "12.0.0.0", "v11.5", "v12.0" - currentVersionStr = currentDb2uVersion.lstrip('v') + if not targetDb2uVersion: + logger.warning("Unable to determine target Db2u version from catalog") + else: + # Extract target major version targetVersionStr = targetDb2uVersion.lstrip('v') try: - currentMajorVersion = int(currentVersionStr.split('.')[0]) targetMajorVersion = int(targetVersionStr.split('.')[0]) - except (ValueError): - h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 channel doesn't map to a Db2. Version name {targetMajorVersion}") - # Check if upgrade to version or higher is needed - if targetMajorVersion > currentMajorVersion: - if self.noConfirm and self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": - h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 {currentMajorVersion} needs to be updated to {targetMajorVersion}") - self.fatalError(f"By choosing {self.getParam('mas_catalog_version')} you must confirm Db2 update to version {targetMajorVersion}") - elif self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": - h.stop_and_persist(symbol=self.successIcon, text=f"Db2 {currentMajorVersion} needs to be updated to {targetMajorVersion}") - if not self.yesOrNo(f"Confirm update from Db2 {currentMajorVersion} to {targetMajorVersion}", f"db2_v{targetMajorVersion}_upgrade"): - exit(1) - print() - logger.debug(f"Db2u major version upgrade required: {currentMajorVersion} -> {targetMajorVersion}") - else: - self.setParam(f"db2_v{targetMajorVersion}_upgrade", "false") - logger.debug("No Db2u major version upgrade required") - else: - logger.debug("Unable to determine current Db2u version from instance") + except (ValueError, IndexError): + h.stop_and_persist(symbol=self.failureIcon, text=f"Invalid Db2 channel version format: {targetDb2uVersion}") + logger.error(f"Unable to parse target Db2u version: {targetDb2uVersion}") + targetMajorVersion = None + + if targetMajorVersion is not None: + # Check all instances to see if any need upgrade + needsUpgrade = False + instanceVersions = [] + + for instance in instances: + if not isinstance(instance, dict): + continue + + instanceName = instance["metadata"]["name"] + currentVersion = instance["spec"]["version"] + + if not currentVersion: + logger.warning(f"No version found for Db2u instance: {instanceName}") + continue + + logger.debug(f"Current Db2u version {instanceName}: {currentVersion}") + + # Extract major version from current instance + currentVersionStr = currentVersion.lstrip('v') + try: + currentMajorVersion = int(currentVersionStr.split('.')[0]) + instanceVersions.append((instanceName, currentMajorVersion, currentVersion)) + + # Check if this instance needs upgrade + if currentMajorVersion < targetMajorVersion: + needsUpgrade = True + logger.debug(f"Instance {instanceName} needs upgrade: {currentMajorVersion} -> {targetMajorVersion}") + except (ValueError, IndexError): + logger.warning(f"Unable to parse version for instance {instanceName}: {currentVersion}") + continue + + if not instanceVersions: + logger.warning("Unable to determine current Db2u version from any instance") + else: + logger.debug(f"Target Db2u version from catalog: {targetDb2uVersion} (major: {targetMajorVersion})") + + # Check if upgrade to version or higher is needed + if needsUpgrade: + # Get the minimum version across all instances for user messaging + minVersion = min(instanceVersions, key=lambda x: x[1]) + minMajorVersion = minVersion[1] + + if self.noConfirm and self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": + h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 {minMajorVersion} needs to be updated to {targetMajorVersion}") + self.fatalError(f"By choosing {self.getParam('mas_catalog_version')} you must confirm Db2 update to version {targetMajorVersion}") + elif self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": + h.stop_and_persist(symbol=self.successIcon, text=f"Db2 {minMajorVersion} needs to be updated to {targetMajorVersion}") + if not self.yesOrNo(f"Confirm update from Db2 {minMajorVersion} to {targetMajorVersion}", f"db2_v{targetMajorVersion}_upgrade"): + exit(1) + print() + logger.debug(f"Db2u major version upgrade required: {minMajorVersion} -> {targetMajorVersion}") + else: + self.setParam(f"db2_v{targetMajorVersion}_upgrade", "false") + logger.debug("No Db2u major version upgrade required") else: logger.debug(f"Found no instances of {kindString} to update") h.stop_and_persist(symbol=self.successIcon, text=f"Found no {kindString} ({apiVersion}) instances to update") From 38235acf3b026e232d529b41225fcbd5ab5e5f80 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:20:35 -0300 Subject: [PATCH 04/22] [patch] update regex pattern for channel --- python/src/mas/cli/update/app.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 31714e0700a..3210b7e85d6 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -11,6 +11,7 @@ import logging import logging.handlers +import re from typing import Callable from halo import Halo from prompt_toolkit import print_formatted_text, HTML @@ -610,13 +611,17 @@ def detectDb2u(self) -> None: if not targetDb2uVersion: logger.warning("Unable to determine target Db2u version from catalog") else: - # Extract target major version - targetVersionStr = targetDb2uVersion.lstrip('v') + # Extract target major version (first two digits) + # Handle version formats like "11.5.8.0", "12.0.0.0", "v11.5", "v12.0", "s11.5.9.0-cn6" try: - targetMajorVersion = int(targetVersionStr.split('.')[0]) - except (ValueError, IndexError): + match = re.match(r'^[vs]?(\d{2})(\d+).*', targetDb2uVersion) + if match: + targetMajorVersion = int(match.group(1)) + else: + raise ValueError(f"Version format does not match expected pattern: {targetDb2uVersion}") + except (ValueError, AttributeError) as e: h.stop_and_persist(symbol=self.failureIcon, text=f"Invalid Db2 channel version format: {targetDb2uVersion}") - logger.error(f"Unable to parse target Db2u version: {targetDb2uVersion}") + logger.error(f"Unable to parse target Db2u version: {targetDb2uVersion} - {str(e)}") targetMajorVersion = None if targetMajorVersion is not None: @@ -638,7 +643,7 @@ def detectDb2u(self) -> None: logger.debug(f"Current Db2u version {instanceName}: {currentVersion}") # Extract major version from current instance - currentVersionStr = currentVersion.lstrip('v') + currentVersionStr = currentVersion.lstrip('s') try: currentMajorVersion = int(currentVersionStr.split('.')[0]) instanceVersions.append((instanceName, currentMajorVersion, currentVersion)) From 96bd97520fa51f49b81b1802e266729b15cb6650 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:40:23 -0300 Subject: [PATCH 05/22] [patch] add db2 channel to db2 update --- python/src/mas/cli/update/app.py | 6 ++++-- tekton/src/pipelines/mas-update.yml.j2 | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 3210b7e85d6..8e1ba3fb769 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -564,6 +564,8 @@ def detectDb2u(self) -> None: kinds = ["Db2uCluster", "Db2uInstance"] paramName = "db2_namespace" mode = "db2" + # Get target Db2u version from catalog + targetDb2uVersion = self.chosenCatalog["db2_channel_default"] with Halo(text=haloStartingMessage, spinner=self.spinner) as h: try: @@ -602,11 +604,10 @@ def detectDb2u(self) -> None: for index, ns in enumerate(sorted(namespaces), start=1): self.printDescription([f"{index}. {ns}"]) self.promptForListSelect("Select namespace", sorted(namespaces), paramName) + self.setParam("db2_channel", self.chosenCatalog["db2_channel_default"]) # Version comparison logic - check if Db2u needs major version upgrade if len(instances) > 0: - # Get target Db2u version from catalog - targetDb2uVersion = self.chosenCatalog["db2_channel_default"] if not targetDb2uVersion: logger.warning("Unable to determine target Db2u version from catalog") @@ -675,6 +676,7 @@ def detectDb2u(self) -> None: if not self.yesOrNo(f"Confirm update from Db2 {minMajorVersion} to {targetMajorVersion}", f"db2_v{targetMajorVersion}_upgrade"): exit(1) print() + self.setParam("db2_channel", targetDb2uVersion) logger.debug(f"Db2u major version upgrade required: {minMajorVersion} -> {targetMajorVersion}") else: self.setParam(f"db2_v{targetMajorVersion}_upgrade", "false") diff --git a/tekton/src/pipelines/mas-update.yml.j2 b/tekton/src/pipelines/mas-update.yml.j2 index 639c8b728ad..a373e02ff4a 100644 --- a/tekton/src/pipelines/mas-update.yml.j2 +++ b/tekton/src/pipelines/mas-update.yml.j2 @@ -249,6 +249,8 @@ spec: value: $(params.db2_namespace) - name: db2_v12_upgrade value: $(params.db2_v12_upgrade) + - name: db2_channel + value: $(params.db2_channel) - name: update-mongodb timeout: "0" From ac23d16f0499547faa8e4ca5d39c098ca829d509 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:33:55 -0300 Subject: [PATCH 06/22] [patch] update db2 update tests --- python/src/mas/cli/update/app.py | 10 +++++++--- python/test/update/test_db2u_interactive.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 8e1ba3fb769..c25e06806c4 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -615,7 +615,7 @@ def detectDb2u(self) -> None: # Extract target major version (first two digits) # Handle version formats like "11.5.8.0", "12.0.0.0", "v11.5", "v12.0", "s11.5.9.0-cn6" try: - match = re.match(r'^[vs]?(\d{2})(\d+).*', targetDb2uVersion) + match = re.match(r'^[vs]?(\d{2})[\d.]*', targetDb2uVersion) if match: targetMajorVersion = int(match.group(1)) else: @@ -670,13 +670,17 @@ def detectDb2u(self) -> None: if self.noConfirm and self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": h.stop_and_persist(symbol=self.failureIcon, text=f"Db2 {minMajorVersion} needs to be updated to {targetMajorVersion}") - self.fatalError(f"By choosing {self.getParam('mas_catalog_version')} you must confirm Db2 update to version {targetMajorVersion}") + self.fatalError(f"By choosing {self.getParam('mas_catalog_version')} you must confirm Db2 update to version {targetMajorVersion} using '--db2-v{targetMajorVersion}-upgrade' when using '--no-confirm'") elif self.getParam(f"db2_v{targetMajorVersion}_upgrade") != "true": h.stop_and_persist(symbol=self.successIcon, text=f"Db2 {minMajorVersion} needs to be updated to {targetMajorVersion}") if not self.yesOrNo(f"Confirm update from Db2 {minMajorVersion} to {targetMajorVersion}", f"db2_v{targetMajorVersion}_upgrade"): exit(1) print() - self.setParam("db2_channel", targetDb2uVersion) + else: + h.stop_and_persist(symbol=self.successIcon, text=f"Db2 will be updated from {minMajorVersion} to {targetMajorVersion}") + + # Set db2_channel when upgrade is confirmed (either via flag or user prompt) + self.setParam("db2_channel", targetDb2uVersion) logger.debug(f"Db2u major version upgrade required: {minMajorVersion} -> {targetMajorVersion}") else: self.setParam(f"db2_v{targetMajorVersion}_upgrade", "false") diff --git a/python/test/update/test_db2u_interactive.py b/python/test/update/test_db2u_interactive.py index daa1cba182c..f80d891167b 100644 --- a/python/test/update/test_db2u_interactive.py +++ b/python/test/update/test_db2u_interactive.py @@ -143,11 +143,11 @@ def test_db2u_major_version_upgrade_accepted(tmpdir, resource_kind): prompt_handlers = { # Proceed with current cluster - '.*Proceed with this cluster?.*': lambda msg: 'y', + '.*Proceed with this cluster.*': lambda msg: 'y', # Catalog selection '.*Select catalog version.*': lambda msg: '1', - # Db2 version upgrade confirmation - '.*Confirm update from Db2.*': lambda msg: 'y', + # Db2 version upgrade confirmation - match the exact format + '.*Confirm update from Db2 11 to 12.*': lambda msg: 'y', # Final confirmation '.*Proceed with these settings.*': lambda msg: 'y', } @@ -164,7 +164,7 @@ def test_db2u_major_version_upgrade_accepted(tmpdir, resource_kind): "metadata": {"name": "inst1"}, "status": {"versions": {"reconciled": "9.1.7"}} }], - timeout_seconds=30 + timeout_seconds=60 ) run_update_test(tmpdir, config) @@ -184,11 +184,11 @@ def test_db2u_major_version_upgrade_rejected(tmpdir, resource_kind): prompt_handlers = { # Proceed with current cluster - '.*Proceed with this cluster?.*': lambda msg: 'y', + '.*Proceed with this cluster.*': lambda msg: 'y', # Catalog selection '.*Select catalog version.*': lambda msg: '1', - # Db2 version upgrade confirmation - user rejects - '.*Confirm update from Db2.*': lambda msg: 'n', + # Db2 version upgrade confirmation - user rejects - match exact format + '.*Confirm update from Db2 11 to 12.*': lambda msg: 'n', } config = UpdateTestConfig( @@ -205,7 +205,7 @@ def test_db2u_major_version_upgrade_rejected(tmpdir, resource_kind): }], expect_system_exit=True, expected_exit_code=1, - timeout_seconds=30 + timeout_seconds=60 ) run_update_test(tmpdir, config) From 51ef4936d991218174f26b8db6aa66e649461ae5 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:00:04 -0300 Subject: [PATCH 07/22] [patch] add parameter for db2_channel --- tekton/src/pipelines/mas-update.yml.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tekton/src/pipelines/mas-update.yml.j2 b/tekton/src/pipelines/mas-update.yml.j2 index a373e02ff4a..ba7aa42aeb8 100644 --- a/tekton/src/pipelines/mas-update.yml.j2 +++ b/tekton/src/pipelines/mas-update.yml.j2 @@ -51,6 +51,10 @@ spec: type: string description: Approves the Db2 upgrade to version 12 if needed default: "" + - name: db2_channel + type: string + description: Db2 channel to be upgraded + default: "" # mongodb update # ------------------------------------------------------------------------- From b8f78110a48cc7fe1f0fe8d65f81827c22bfda20 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:56:10 -0300 Subject: [PATCH 08/22] [patch] kick build --- tekton/src/pipelines/mas-update.yml.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/tekton/src/pipelines/mas-update.yml.j2 b/tekton/src/pipelines/mas-update.yml.j2 index ba7aa42aeb8..b83bd1cb06d 100644 --- a/tekton/src/pipelines/mas-update.yml.j2 +++ b/tekton/src/pipelines/mas-update.yml.j2 @@ -362,3 +362,4 @@ spec: # An aggregate status of all the pipelineTasks under the tasks section (excluding the finally section). # This variable is only available in the finally tasks and can have any one of the values (Succeeded, Failed, Completed, or None) value: $(tasks.status) + From e8b46e85b8c8584990919f0144701e7d4aa31505 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:51:30 -0300 Subject: [PATCH 09/22] [patch] fix db2_v12_upgrade default --- tekton/src/tasks/dependencies/db2.yml.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index a61dbeaa803..f7654ff7aef 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -176,6 +176,7 @@ spec: - name: db2_v12_upgrade type: string description: Approves the Db2 upgrade to version 12 if needed + default: "" # Backup/Restore specific parameters - name: db2_backup_version type: string From 9e88459bc008d3c5465195470a3b7bb6e89a00c9 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:00:36 -0300 Subject: [PATCH 10/22] [patch] add db2 license file support to installation workflow --- python/src/mas/cli/aiservice/install/app.py | 1 + python/src/mas/cli/install/app.py | 6 ++++++ python/src/mas/cli/install/argBuilder.py | 2 ++ python/src/mas/cli/install/argParser.py | 5 +++++ python/src/mas/cli/install/params.py | 1 + .../cli/install/settings/additionalConfigs.py | 17 +++++++++++++++++ .../src/mas/cli/install/settings/db2Settings.py | 14 ++++++++++++++ python/test/install/test_dev_mode.py | 9 ++++++--- python/test/install/test_existing_catalog.py | 3 ++- python/test/install/test_no_catalog.py | 3 ++- tekton/src/tasks/dependencies/db2.yml.j2 | 8 ++++++++ 11 files changed, 64 insertions(+), 5 deletions(-) diff --git a/python/src/mas/cli/aiservice/install/app.py b/python/src/mas/cli/aiservice/install/app.py index 4d0bb5f3aac..2d0c6c76d4f 100644 --- a/python/src/mas/cli/aiservice/install/app.py +++ b/python/src/mas/cli/aiservice/install/app.py @@ -491,6 +491,7 @@ def install(self, argv): dynClient=self.dynamicClient, namespace=pipelinesNamespace, slsLicenseFile=self.slsLicenseFileSecret, + db2LicenseFile=self.db2LicenseFileSecret, additionalConfigs=self.additionalConfigsSecret, podTemplates=self.podTemplatesSecret, certs=self.certsSecret diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index e1395166975..88be05840a2 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -1567,6 +1567,7 @@ def nonInteractiveMode(self) -> None: self.db2SetTolerations = False self.installAIService = False self.slsLicenseFileLocal = None + self.db2LicenseFileLocal = None self.approvals: Dict[str, Dict[str, Any]] = { "approval_core": {"id": "suite-verify"}, # After Core Platform verification has completed @@ -1724,6 +1725,9 @@ def nonInteractiveMode(self) -> None: if value is not None and value != "": self.slsLicenseFileLocal = value self.setParam("sls_action", "install") + elif key == "db2_license_file": + if value is not None and value != "": + self.db2LicenseFileLocal = value elif key == "dedicated_sls": if value: self.setParam("sls_namespace", f"mas-{self.args.mas_instance_id}-sls") @@ -1960,6 +1964,7 @@ def install(self, argv): self.additionalConfigs() self.podTemplates() self.slsLicenseFile() + self.db2LicenseFile() self.manualCertificates() # Show a summary of the installation configuration @@ -2118,6 +2123,7 @@ def install(self, argv): dynClient=self.dynamicClient, namespace=pipelinesNamespace, slsLicenseFile=self.slsLicenseFileSecret, + db2LicenseFile=self.db2LicenseFileSecret, additionalConfigs=self.additionalConfigsSecret, podTemplates=self.podTemplatesSecret, certs=self.certsSecret diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index eeb9891f69d..0211c7a49dc 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -451,6 +451,8 @@ def buildCommand(self) -> str: command += f" --db2-type \"{self.getParam('db2_type')}\"{newline}" if self.getParam('db2_timezone') != "": command += f" --db2-timezone \"{self.getParam('db2_timezone')}\"{newline}" + if self.db2LicenseFileLocal != "": + command += f" --db2-license-file \"{self.db2LicenseFileLocal}\"{newline}" if self.getParam('db2_affinity_key') != "": command += f" --db2-affinity-key \"{self.getParam('db2_affinity_key')}\"{newline}" diff --git a/python/src/mas/cli/install/argParser.py b/python/src/mas/cli/install/argParser.py index ff0134a245c..69b93976bd2 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -1187,6 +1187,11 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Timezone for Db2 instance" ) +db2ArgGroup.add_argument( + "--db2-license-file", + required=False, + help="Db2 License File for Db2" +) db2ArgGroup.add_argument( "--db2-affinity-key", required=False, diff --git a/python/src/mas/cli/install/params.py b/python/src/mas/cli/install/params.py index 187dfd90546..13a538953f7 100644 --- a/python/src/mas/cli/install/params.py +++ b/python/src/mas/cli/install/params.py @@ -90,6 +90,7 @@ "db2_timezone", "db2_namespace", "db2_channel", + "db2_license_file", "db2_affinity_key", "db2_affinity_value", "db2_tolerate_key", diff --git a/python/src/mas/cli/install/settings/additionalConfigs.py b/python/src/mas/cli/install/settings/additionalConfigs.py index 52f978f0a32..09f6293db14 100644 --- a/python/src/mas/cli/install/settings/additionalConfigs.py +++ b/python/src/mas/cli/install/settings/additionalConfigs.py @@ -32,11 +32,13 @@ class AdditionalConfigsMixin(): noConfirm: bool templatesDir: str slsLicenseFileLocal: str | None + db2LicenseFileLocal: str | None manualCertsDir: str | None showAdvancedOptions: bool additionalConfigsSecret: Dict[str, Any] | None podTemplatesSecret: Dict[str, Any] | None slsLicenseFileSecret: Dict[str, Any] | None + db2LicenseFileSecret: Dict[str, Any] | None certsSecret: Dict[str, Any] | None # Methods from BaseApp @@ -272,6 +274,21 @@ def slsLicenseFile(self) -> None: self.setParam("sls_entitlement_file", f"/workspace/entitlement/{path.basename(self.slsLicenseFileLocal)}") self.slsLicenseFileSecret = self.addFilesToSecret(slsLicenseFileSecret, self.slsLicenseFileLocal, '') + def db2LicenseFile(self) -> None: + if self.db2LicenseFileLocal: + db2LicenseFileSecret = { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": { + "name": "pipeline-db2-license" + } + } + self.setParam("db2_license_file", f"/workspace/entitlement/{path.basename(self.db2LicenseFileLocal)}") + self.db2LicenseFileSecret = self.addFilesToSecret(db2LicenseFileSecret, self.db2LicenseFileLocal, '') + else: + self.db2LicenseFileSecret = None + def addFilesToSecret(self, secretDict: dict, configPath: str, extension: str, keyPrefix: str = '') -> dict: """ Add file (or files) to pipeline-additional-configs diff --git a/python/src/mas/cli/install/settings/db2Settings.py b/python/src/mas/cli/install/settings/db2Settings.py index 3a251125b5c..10630543688 100644 --- a/python/src/mas/cli/install/settings/db2Settings.py +++ b/python/src/mas/cli/install/settings/db2Settings.py @@ -78,6 +78,15 @@ def promptForListSelect( ) -> str: ... + def promptForFile( + self, + message: str, + mustExist: bool = True, + default: str = "", + envVar: str = "" + ) -> str: + ... + # Methods from ConfigGeneratorMixin or InstallSettingsMixin def selectLocalConfigDir(self) -> None: ... @@ -241,6 +250,11 @@ def configDb2(self, silentMode=False) -> None: # Do we need to configure Db2u? if self.getParam("db2_action_system") == "install" or self.getParam("db2_action_manage") == "install" or self.getParam("db2_action_facilities") == "install": + self.printDescription([ + "Db2 Universal Operator for v12 onwards requires to add a License activation key", + "If you don't have a license press enter to continue." + ]) + self.db2LicenseFileLocal = self.promptForFile("Db2 License file", envVar="DB2_LICENSE_FILE", default="", mustExist=False) if self.showAdvancedOptions: self.printH2("Installation Namespace") self.promptForString("Install namespace", "db2_namespace", default="db2u") diff --git a/python/test/install/test_dev_mode.py b/python/test/install/test_dev_mode.py index 90b666559b4..7e5734d5674 100644 --- a/python/test/install/test_dev_mode.py +++ b/python/test/install/test_dev_mode.py @@ -67,7 +67,8 @@ def test_install_master_dev_mode(tmpdir): ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 5. SLS configuration '.*SLS channel.*': lambda msg: '1.x-stable', - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', # SLS License (exact match with HTML tags) + '.*>Db2 License file<.*': lambda msg: '', # Db2 License (exact match with HTML tags) # 6. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', @@ -148,7 +149,8 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 5. SLS configuration '.*SLS channel.*': lambda msg: '1.x-stable', - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', # SLS License (exact match with HTML tags) + '.*>Db2 License file<.*': lambda msg: '', # Db2 License (exact match with HTML tags) # 6. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', @@ -253,7 +255,8 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): # 6. SLS configuration '.*SLS Mode.*': lambda msg: '1', # SLS Mode prompt (appears with advanced options) '.*SLS channel.*': lambda msg: '1.x-stable', - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', # SLS License (exact match with HTML tags) + '.*>Db2 License file<.*': lambda msg: '', # Db2 License (exact match with HTML tags) # 7. DRO configuration '.*DRO.*Namespace.*': lambda msg: '', # DRO Namespace prompt (appears with advanced options) ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', diff --git a/python/test/install/test_existing_catalog.py b/python/test/install/test_existing_catalog.py index 875d367f1ae..559c05c7ef2 100644 --- a/python/test/install/test_existing_catalog.py +++ b/python/test/install/test_existing_catalog.py @@ -35,7 +35,8 @@ def test_install_interactive_existing_catalog(tmpdir): # 5. Storage classes ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 6. SLS configuration - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', # SLS License (exact match with HTML tags) + '.*>Db2 License file<.*': lambda msg: '', # Db2 License (exact match with HTML tags) # 7. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', diff --git a/python/test/install/test_no_catalog.py b/python/test/install/test_no_catalog.py index b283f901593..5f14f3595dd 100644 --- a/python/test/install/test_no_catalog.py +++ b/python/test/install/test_no_catalog.py @@ -35,7 +35,8 @@ def test_install_interactive_no_catalog(tmpdir): # 5. Storage classes ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 6. SLS configuration - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', # SLS License (exact match with HTML tags) + '.*>Db2 License file<.*': lambda msg: '', # Db2 License (exact match with HTML tags) # 7. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index f7654ff7aef..3974995cd9a 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -11,6 +11,10 @@ spec: - name: ibm_entitlement_key type: string default: "" + - name: db2_license_file + type: string + description: Path to Db2 license file + default: "" # Db2u Operator - name: db2_channel @@ -222,6 +226,8 @@ spec: # Entitlement - name: IBM_ENTITLEMENT_KEY value: $(params.ibm_entitlement_key) + - name: DB2_LICENSE_FILE + value: $(params.db2_license_file) # Db2u Operator - name: DB2_CHANNEL @@ -370,3 +376,5 @@ spec: optional: true - name: backups optional: true + - name: shared-entitlement + optional: true From 631798ef7c94cbd41ce10927853deb2154ec4aa9 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:59:55 -0300 Subject: [PATCH 11/22] [patch] add db2 license file support to db2 in aiservice installation --- python/src/mas/cli/aiservice/install/app.py | 13 ++++++++++++- python/src/mas/cli/aiservice/install/argBuilder.py | 2 ++ python/src/mas/cli/aiservice/install/argParser.py | 6 +++++- python/src/mas/cli/aiservice/install/params.py | 1 + python/test/aiservice/install/test_app.py | 8 ++++++-- python/test/aiservice/install/test_dev_mode.py | 6 ++++-- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/python/src/mas/cli/aiservice/install/app.py b/python/src/mas/cli/aiservice/install/app.py index 2d0c6c76d4f..f94c47b0b1c 100644 --- a/python/src/mas/cli/aiservice/install/app.py +++ b/python/src/mas/cli/aiservice/install/app.py @@ -148,6 +148,7 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None: self.storageClassProvider = "custom" self.slsLicenseFileLocal = None + self.db2LicenseFileLocal = None # Catalog self.configCatalog() @@ -176,6 +177,11 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None: self.configMongoDb() self.setDB2DefaultChannel() self.setDB2DefaultSettings() + self.printDescription([ + "Db2 Universal Operator for v12 onwards requires to add a License activation key", + "If you don't have a license press enter to continue." + ]) + self.db2LicenseFileLocal = self.promptForFile("Db2 License file", envVar="DB2_LICENSE_FILE", default="", mustExist=False) @logMethodCall def nonInteractiveMode(self) -> None: @@ -188,6 +194,7 @@ def nonInteractiveMode(self) -> None: self.storageClassProvider = "custom" self.slsLicenseFileLocal = None + self.db2LicenseFileLocal = None self.approvals = { "approval_aiservice": {"id": "aiservice"}, @@ -297,6 +304,9 @@ def nonInteractiveMode(self) -> None: if value is not None and value != "": self.slsLicenseFileLocal = value self.setParam("sls_action", "install") + elif key == "db2_license_file": + if value is not None and value != "": + self.db2LicenseFileLocal = value elif key == "dedicated_sls": if value: self.setParam("sls_namespace", f"mas-{self.args.aiservice_instance_id}-sls") @@ -444,8 +454,9 @@ def install(self, argv): else: self.nonInteractiveMode() - # Set up the sls license file + # Set up the sls and db2 license file self.slsLicenseFile() + self.db2LicenseFile() # Show a summary of the installation configuration self.printH1("Non-Interactive Install Command") diff --git a/python/src/mas/cli/aiservice/install/argBuilder.py b/python/src/mas/cli/aiservice/install/argBuilder.py index 61a84d5f4d4..6d2405305fc 100644 --- a/python/src/mas/cli/aiservice/install/argBuilder.py +++ b/python/src/mas/cli/aiservice/install/argBuilder.py @@ -184,6 +184,8 @@ def buildCommand(self) -> str: if self.getParam('db2_channel') != "": command += f" --db2-channel \"{self.getParam('db2_channel')}\"{newline}" + if self.db2LicenseFileLocal: + command += f" --db2-license-file \"{self.db2LicenseFileLocal}\"" command += " --accept-license --no-confirm" return command diff --git a/python/src/mas/cli/aiservice/install/argParser.py b/python/src/mas/cli/aiservice/install/argParser.py index 6deb68edffc..b6c3d38c10b 100644 --- a/python/src/mas/cli/aiservice/install/argParser.py +++ b/python/src/mas/cli/aiservice/install/argParser.py @@ -460,7 +460,11 @@ def isValidFile(parser, arg) -> str: required=False, help="Subscription channel for Db2u" ) - +db2ArgGroup.add_argument( + "--db2-license-file", + required=False, + help="Db2 License File for Db2" +) # Development Mode # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/aiservice/install/params.py b/python/src/mas/cli/aiservice/install/params.py index 83efc7697ee..cde9a749db7 100644 --- a/python/src/mas/cli/aiservice/install/params.py +++ b/python/src/mas/cli/aiservice/install/params.py @@ -40,6 +40,7 @@ "db2_timezone", "db2_namespace", "db2_channel", + "db2_license_file", "db2_affinity_key", "db2_affinity_value", "db2_tolerate_key", diff --git a/python/test/aiservice/install/test_app.py b/python/test/aiservice/install/test_app.py index d499ab2afa7..38c162d5c08 100644 --- a/python/test/aiservice/install/test_app.py +++ b/python/test/aiservice/install/test_app.py @@ -173,8 +173,10 @@ def set_mixin_prompt_input(**kwargs): return 'nfs-client' if re.match('.*SLS Mode.*', message): return '1' - if re.match('.*License file.*', message): + if re.match('.*>License file<.*', message): return f'{tmpdir}/authorized_entitlement.lic' + if re.match('.*>Db2 License file<.*', message): + return '' if re.match('.*Instance ID.*', message): return 'apmdevops' if re.match('.*Operational Mode.*', message): @@ -299,8 +301,10 @@ def set_mixin_prompt_input(**kwargs): return 'nfs-client' if re.match('.*SLS Mode.*', message): return '1' - if re.match('.*License file.*', message): + if re.match('.*>License file<.*', message): return f'{tmpdir}/authorized_entitlement.lic' + if re.match('.*>Db2 License file<.*', message): + return '' if re.match('.*Instance ID.*', message): return 'apmdevops' if re.match('.*Operational Mode.*', message): diff --git a/python/test/aiservice/install/test_dev_mode.py b/python/test/aiservice/install/test_dev_mode.py index 8d3d99224b4..99f2f942ca8 100644 --- a/python/test/aiservice/install/test_dev_mode.py +++ b/python/test/aiservice/install/test_dev_mode.py @@ -66,7 +66,8 @@ def test_aiservice_install_master_dev_mode(tmpdir): # 4. Storage classes ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 5. SLS configuration - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>Db2 License file<.*': lambda msg: '', # 6. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', @@ -137,7 +138,8 @@ def test_aiservice_install_master_dev_mode_existing_catalog(tmpdir): # 4. Storage classes ".*Use the auto-detected storage classes.*": lambda msg: 'y', # 5. SLS configuration - '.*License file.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>License file<.*': lambda msg: f'{tmpdir}/authorized_entitlement.lic', + '.*>Db2 License file<.*': lambda msg: '', # 6. DRO configuration ".*Contact e-mail address.*": lambda msg: 'maximo@ibm.com', ".*Contact first name.*": lambda msg: 'Test', From 0d9900e6e439fe38efb3134f788c5470cce4ed05 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:40:26 -0300 Subject: [PATCH 12/22] [patch] add db2_license_file reference into taskdefs --- tekton/src/params/install-db2.yml.j2 | 4 ++++ tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 4 ++++ tekton/src/tasks/dependencies/db2.yml.j2 | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tekton/src/params/install-db2.yml.j2 b/tekton/src/params/install-db2.yml.j2 index e28f08fafd8..a0e80431f0f 100644 --- a/tekton/src/params/install-db2.yml.j2 +++ b/tekton/src/params/install-db2.yml.j2 @@ -40,6 +40,10 @@ type: string description: Db2 Openshift Custom Resource (db2ucluster or db2uinstance). Defaults to db2ucluster if not specified. default: "" +- name: db2_license_file + type: string + description: Db2 activation license file + default: "" # Dependences - Db2 - Node scheduling # ------------------------------------------------------------------------- diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index 2ffb7f7a82b..f55d983db26 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -54,6 +54,8 @@ value: $(params.db2_table_org) - name: db2u_kind value: $(params.db2u_kind) + - name: db2_license_file + value: $(params.db2_license_file) # Node Scheduling - name: db2_affinity_key @@ -144,3 +146,5 @@ workspaces: - name: configs workspace: shared-configs + - name: entitlement + workspace: shared-entitlement diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index 3974995cd9a..750e1bd22ad 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -13,7 +13,7 @@ spec: default: "" - name: db2_license_file type: string - description: Path to Db2 license file + description: Db2 activation license file default: "" # Db2u Operator From f872f5708fb240263399b990c3600563114c80bf Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:55:04 -0300 Subject: [PATCH 13/22] [patch] add entitlement workspace as optional --- tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index f55d983db26..60670b04587 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -148,3 +148,4 @@ workspace: shared-configs - name: entitlement workspace: shared-entitlement + optional: true \ No newline at end of file From 5f15b01a0b7423fb3c406fc59437f155bc1afa8e Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:24:01 -0300 Subject: [PATCH 14/22] [patch] add db2 license file support and entitlement workspace --- tekton/src/pipelines/mas-install.yml.j2 | 8 ++++---- .../pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 | 8 +++++++- tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 3 ++- tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 | 8 ++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 5cbf23660ea..7f5154afc89 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -114,18 +114,18 @@ spec: # 2.3 Db2 # 2.3.1 System Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - cert-manager # 2.3.2 Dedicated Manage Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - db2-system # 2.3.3 Install Dedicated Db2 for AI Service # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - cert-manager @@ -463,7 +463,7 @@ spec: # 13. Install and configure Facilities # ------------------------------------------------------------------------- # 13.1 Dedicated Facilities Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/apps/db2-setup-facilities.yml.j2') | indent(4)}} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/apps/db2-setup-facilities.yml.j2', template_vars={'db2_license_workspace': 'true'}) | indent(4)}} runAfter: - db2-manage diff --git a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 index 59a45be78b1..fb261188e6f 100644 --- a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 +++ b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 @@ -42,6 +42,8 @@ value: $(params.db2_table_org) - name: db2u_kind value: $(params.db2u_kind) + - name: db2_license_file + value: $(params.db2_license_file) # Node Scheduling - name: db2_affinity_key @@ -127,4 +129,8 @@ kind: Task workspaces: - name: configs - workspace: shared-configs \ No newline at end of file + workspace: shared-configs +{%- if db2_license_workspace is defined and db2_license_workspace == 'true' %} + - name: entitlement + workspace: shared-entitlement +{% endif %} diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index 60670b04587..0e95e3c4dff 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -146,6 +146,7 @@ workspaces: - name: configs workspace: shared-configs +{%- if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: entitlement workspace: shared-entitlement - optional: true \ No newline at end of file +{% endif %} \ No newline at end of file diff --git a/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 b/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 index d242a3e04c5..2aa10bb2a97 100644 --- a/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 +++ b/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 @@ -59,6 +59,10 @@ spec: - name: db2u_kind type: string default: "" + - name: db2_license_file + type: string + default: "" + description: Db2 activation license file # Db2 - Node scheduling - name: db2_affinity_key @@ -208,6 +212,8 @@ spec: value: $(params.db2_table_org) - name: DB2U_KIND value: $(params.db2u_kind) + - name: DB2_LICENSE_FILE + value: $(params.db2_license_file) # Db2 - Node Scheduling - name: DB2_AFFINITY_KEY @@ -303,3 +309,5 @@ spec: workspaces: - name: configs optional: true + - name: shared-entitlement + optional: true From a45c0acb67c870a13d89a2bf0dbc0beea2e1d856 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:26:41 -0300 Subject: [PATCH 15/22] [patch] fix formatting in pipeline generation --- tekton/src/pipelines/mas-install.yml.j2 | 2 +- tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 | 2 +- tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 2 +- tekton/src/tasks/dependencies/db2.yml.j2 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 7f5154afc89..f526edb780d 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -9,7 +9,7 @@ spec: - name: shared-configs # Any pre-generated configs that will be copied into the shared-configs workspace during suite-install - name: shared-additional-configs - # The SLS entitlement key file that will be installed during install-sls. + # The SLS entitlement key and Db2 license file that will be installed during install-sls and db2. - name: shared-entitlement # Pre-generated certificates that will be copied into certs folder of shared-configs workspace to be used by suite-certs task - name: shared-certificates diff --git a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 index fb261188e6f..0697cb0cc80 100644 --- a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 +++ b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 @@ -130,7 +130,7 @@ workspaces: - name: configs workspace: shared-configs -{%- if db2_license_workspace is defined and db2_license_workspace == 'true' %} +{% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: entitlement workspace: shared-entitlement {% endif %} diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index 0e95e3c4dff..32fcfcaead4 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -146,7 +146,7 @@ workspaces: - name: configs workspace: shared-configs -{%- if db2_license_workspace is defined and db2_license_workspace == 'true' %} +{% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: entitlement workspace: shared-entitlement {% endif %} \ No newline at end of file diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index 750e1bd22ad..fc8d42ecd3a 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -376,5 +376,5 @@ spec: optional: true - name: backups optional: true - - name: shared-entitlement + - name: entitlement optional: true From 433b4d50f7da1dd4b68d473dd1bd5810f355fe2a Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:10:33 -0300 Subject: [PATCH 16/22] [patch] move db2 license file to dedicated workspace --- python/src/mas/cli/install/app.py | 2 +- python/src/mas/cli/install/params.py | 1 - python/src/mas/cli/install/settings/additionalConfigs.py | 2 +- tekton/src/pipelines/mas-install.yml.j2 | 2 ++ .../src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 | 4 ++-- tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 4 ++-- tekton/src/tasks/dependencies/db2.yml.j2 | 2 +- tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 88be05840a2..0b2d38b3565 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -1960,7 +1960,7 @@ def install(self, argv): if self.deployCP4D: self.configCP4D() - # Set up the secrets for additional configs, podtemplates, sls license file and manual certificates + # Set up the secrets for additional configs, podtemplates, sls license file, db2 license file and manual certificates self.additionalConfigs() self.podTemplates() self.slsLicenseFile() diff --git a/python/src/mas/cli/install/params.py b/python/src/mas/cli/install/params.py index 13a538953f7..187dfd90546 100644 --- a/python/src/mas/cli/install/params.py +++ b/python/src/mas/cli/install/params.py @@ -90,7 +90,6 @@ "db2_timezone", "db2_namespace", "db2_channel", - "db2_license_file", "db2_affinity_key", "db2_affinity_value", "db2_tolerate_key", diff --git a/python/src/mas/cli/install/settings/additionalConfigs.py b/python/src/mas/cli/install/settings/additionalConfigs.py index 09f6293db14..678cb7327dc 100644 --- a/python/src/mas/cli/install/settings/additionalConfigs.py +++ b/python/src/mas/cli/install/settings/additionalConfigs.py @@ -284,7 +284,7 @@ def db2LicenseFile(self) -> None: "name": "pipeline-db2-license" } } - self.setParam("db2_license_file", f"/workspace/entitlement/{path.basename(self.db2LicenseFileLocal)}") + self.setParam("db2_license_file", f"/workspace/db2/{path.basename(self.db2LicenseFileLocal)}") self.db2LicenseFileSecret = self.addFilesToSecret(db2LicenseFileSecret, self.db2LicenseFileLocal, '') else: self.db2LicenseFileSecret = None diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index f526edb780d..263d5d11fef 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -15,6 +15,8 @@ spec: - name: shared-certificates # PodTemplates configurations - name: shared-pod-templates + # Db2 License File + - name: shared-db2 params: # 1. Common Parameters diff --git a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 index 0697cb0cc80..5941968e8cf 100644 --- a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 +++ b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 @@ -131,6 +131,6 @@ - name: configs workspace: shared-configs {% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - - name: entitlement - workspace: shared-entitlement + - name: db2 + workspace: shared-db2 {% endif %} diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index 32fcfcaead4..7234e6ae74f 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -147,6 +147,6 @@ - name: configs workspace: shared-configs {% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - - name: entitlement - workspace: shared-entitlement + - name: db2 + workspace: shared-db2 {% endif %} \ No newline at end of file diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2 index fc8d42ecd3a..1c245910a19 100644 --- a/tekton/src/tasks/dependencies/db2.yml.j2 +++ b/tekton/src/tasks/dependencies/db2.yml.j2 @@ -376,5 +376,5 @@ spec: optional: true - name: backups optional: true - - name: entitlement + - name: db2 optional: true diff --git a/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 b/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 index 2aa10bb2a97..300c3996c92 100644 --- a/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 +++ b/tekton/src/tasks/suite-db2-setup-for-facilities.yml.j2 @@ -309,5 +309,5 @@ spec: workspaces: - name: configs optional: true - - name: shared-entitlement + - name: db2 optional: true From 795ab634a6b879943855cdfdf1c590a685e62e76 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:22:11 -0300 Subject: [PATCH 17/22] [patch] add db2 license file support for v11 to v12 upgrade --- python/src/mas/cli/update/app.py | 33 ++++++- python/src/mas/cli/update/argParser.py | 6 ++ python/test/update/test_db2u_interactive.py | 47 ++++++++++ .../test/update/test_db2u_non_interactive.py | 91 +++++++++++++++++-- python/test/utils/update_test_helper.py | 14 +++ tekton/src/pipelines/mas-install.yml.j2 | 8 +- tekton/src/pipelines/mas-update.yml.j2 | 13 +++ .../taskdefs/dependencies/db2.yml.j2 | 4 +- 8 files changed, 199 insertions(+), 17 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index e2d8950fb15..94fe6f30a0c 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -24,13 +24,14 @@ from mas.devops.ocp import createNamespace, getConsoleURL, getClusterVersion, isClusterVersionInRange from mas.devops.mas import listMasInstances, getCurrentCatalog from mas.devops.aiservice import listAiServiceInstances -from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchUpdatePipeline, prepareUpdateSlackSecrets +from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchUpdatePipeline, prepareUpdateSlackSecrets, prepareInstallSecrets +from ..install.settings import AdditionalConfigsMixin logger = logging.getLogger(__name__) -class UpdateApp(BaseApp): +class UpdateApp(BaseApp, AdditionalConfigsMixin): def update(self, argv): """ @@ -39,6 +40,7 @@ def update(self, argv): self.args = updateArgParser.parse_args(args=argv) self.noConfirm = self.args.no_confirm self.devMode = self.args.dev_mode + self.db2LicenseFileLocal = None if self.args.mas_catalog_version: # Non-interactive mode @@ -84,6 +86,11 @@ def update(self, argv): elif key in ["no_confirm", "help"]: pass + # Db2 License file has special handling + elif key == "db2_license_file": + if value is not None and value != "": + self.db2LicenseFileLocal = value + # Fail if there's any arguments we don't know how to handle else: print(f"Unknown option: {key} {value}") @@ -190,6 +197,9 @@ def update(self, argv): continueWithUpdate = self.yesOrNo("Proceed with these settings") # Prepare the namespace and launch the installation pipeline if self.noConfirm or continueWithUpdate: + # Db2 workspace in update pipeline + self.db2LicenseFile() + self.createTektonFileWithDigest() self.printH1("Launch Update") @@ -205,6 +215,11 @@ def update(self, argv): with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h: createNamespace(self.dynamicClient, pipelinesNamespace) preparePipelinesNamespace(dynClient=self.dynamicClient) + prepareInstallSecrets( + dynClient=self.dynamicClient, + namespace=pipelinesNamespace, + db2LicenseFile=self.db2LicenseFileSecret, + ) h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") # Create slack secret if slack token and channel are provided @@ -751,6 +766,20 @@ def detectDb2u(self) -> None: else: h.stop_and_persist(symbol=self.successIcon, text=f"Db2 will be updated from {minMajorVersion} to {targetMajorVersion}") + # Db2 v11 to v12 upgrades require a customer-provided Db2 v12 license file. + # Enforce this here so the update is blocked before the Tekton pipeline is launched. + if minMajorVersion == 11 and targetMajorVersion == 12: + # In non-interactive mode we must fail fast because there is no safe way to recover. + if self.noConfirm and self.db2LicenseFileLocal is None: + self.fatalError("The Db2 v11 to v12 upgrade cannot proceed without a valid '--db2-license-file' argument when using '--no-confirm'") + elif self.db2LicenseFileLocal is None: + # In interactive mode, prompt for a valid license file before allowing the upgrade to continue. + self.printDescription([ + "Db2 v11 to v12 upgrades require a valid Db2 v12 activation license file.", + "If you cannot provide a valid file, the update must be aborted." + ]) + self.db2LicenseFileLocal = self.promptForFile("Path to a valid Db2 v12 license file", envVar="DB2_LICENSE_FILE", default="", mustExist=False) + # Set db2_channel when upgrade is confirmed (either via flag or user prompt) self.setParam("db2_channel", targetDb2uVersion) logger.debug(f"Db2u major version upgrade required: {minMajorVersion} -> {targetMajorVersion}") diff --git a/python/src/mas/cli/update/argParser.py b/python/src/mas/cli/update/argParser.py index 7009eba514d..f1084038c12 100644 --- a/python/src/mas/cli/update/argParser.py +++ b/python/src/mas/cli/update/argParser.py @@ -124,6 +124,12 @@ def parse_args(self, args=None, namespace=None): # type: ignore[override] help="Required to confirm a major version update for Db2 to version 12", ) +depsArgGroup.add_argument( + '--db2-license-file', + required=False, + help="Path to a valid Db2 v12 activation license file required for Db2 v11 to v12 upgrades", +) + depsArgGroup.add_argument( '--mongodb-namespace', required=False, diff --git a/python/test/update/test_db2u_interactive.py b/python/test/update/test_db2u_interactive.py index f80d891167b..d9648d7f7ac 100644 --- a/python/test/update/test_db2u_interactive.py +++ b/python/test/update/test_db2u_interactive.py @@ -141,6 +141,10 @@ def test_db2u_major_version_upgrade_accepted(tmpdir, resource_kind): - Update proceeds successfully """ + valid_license_file = os.path.join(str(tmpdir), "db2-license.lic") + with open(valid_license_file, "w") as handle: + handle.write("db2-license") + prompt_handlers = { # Proceed with current cluster '.*Proceed with this cluster.*': lambda msg: 'y', @@ -148,6 +152,8 @@ def test_db2u_major_version_upgrade_accepted(tmpdir, resource_kind): '.*Select catalog version.*': lambda msg: '1', # Db2 version upgrade confirmation - match the exact format '.*Confirm update from Db2 11 to 12.*': lambda msg: 'y', + # License prompt + '.*Path to a valid Db2 v12 license file.*': lambda msg: valid_license_file, # Final confirmation '.*Proceed with these settings.*': lambda msg: 'y', } @@ -211,6 +217,47 @@ def test_db2u_major_version_upgrade_rejected(tmpdir, resource_kind): run_update_test(tmpdir, config) +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_aborted_when_license_file_rejected(tmpdir, resource_kind): + """Test interactive Db2 v11 to v12 upgrade aborts when an invalid license file path is provided. + + Expected behavior: + - User confirms the major version upgrade + - User provides a path to a non-existent license file (mustExist=False, so prompt accepts it) + - User confirms the final settings + - db2LicenseFile() attempts to open the file and raises FileNotFoundError + - Update is aborted with an unhandled exception + """ + + invalid_license_file = os.path.join(str(tmpdir), "missing-db2-license.lic") + + prompt_handlers = { + '.*Proceed with this cluster.*': lambda msg: 'y', + '.*Select catalog version.*': lambda msg: '1', + '.*Confirm update from Db2 11 to 12.*': lambda msg: 'y', + '.*Path to a valid Db2 v12 license file.*': lambda msg: invalid_license_file, + '.*Proceed with these settings.*': lambda msg: 'y', + } + + config = UpdateTestConfig( + prompt_handlers=prompt_handlers, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", + db2u_target_version="v12.0", + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + expect_exception=FileNotFoundError, + timeout_seconds=60 + ) + + run_update_test(tmpdir, config) + + @pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) def test_db2u_minor_version_upgrade_no_prompt(tmpdir, resource_kind): """Test interactive update with Db2 minor version upgrade - no prompt needed. diff --git a/python/test/update/test_db2u_non_interactive.py b/python/test/update/test_db2u_non_interactive.py index 82b63217b65..005e5ea6b8c 100644 --- a/python/test/update/test_db2u_non_interactive.py +++ b/python/test/update/test_db2u_non_interactive.py @@ -208,10 +208,14 @@ def test_db2u_major_version_upgrade_with_flag(tmpdir, resource_kind): Expected behavior: - Detects Db2 v11 needs upgrade to v12 - --db2-v12-upgrade flag provided - - Sets db2_v12_upgrade parameter to true + - Valid --db2-license-file path provided - Update proceeds successfully """ + valid_license_file = os.path.join(str(tmpdir), "db2-license.lic") + with open(valid_license_file, "w") as handle: + handle.write("db2-license") + config = UpdateTestConfig( prompt_handlers={}, # No prompts in non-interactive mode installed_catalog_id="v9-251231-amd64", @@ -224,7 +228,74 @@ def test_db2u_major_version_upgrade_with_flag(tmpdir, resource_kind): "metadata": {"name": "inst1"}, "status": {"versions": {"reconciled": "9.1.7"}} }], + argv=[ + '--catalog', 'v9-260129-amd64', + '--db2-v12-upgrade', + '--db2-license-file', valid_license_file, + '--no-confirm' + ], + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_with_flag_without_license_file(tmpdir, resource_kind): + """Test non-interactive Db2 v11 to v12 upgrade without license file - should fail.""" + + config = UpdateTestConfig( + prompt_handlers={}, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", + db2u_target_version="v12.0", + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], argv=['--catalog', 'v9-260129-amd64', '--db2-v12-upgrade', '--no-confirm'], + expect_system_exit=True, + timeout_seconds=30 + ) + + run_update_test(tmpdir, config) + + +@pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) +def test_db2u_major_version_upgrade_with_flag_invalid_license_file(tmpdir, resource_kind): + """Test non-interactive Db2 v11 to v12 upgrade with invalid license file path - should fail. + + Expected behavior: + - --db2-v12-upgrade flag is provided + - --db2-license-file points to a non-existent file + - db2LicenseFile() attempts to open the file and raises FileNotFoundError + - Update is aborted with FileNotFoundError + """ + + invalid_license_file = os.path.join(str(tmpdir), "missing-db2-license.lic") + + config = UpdateTestConfig( + prompt_handlers={}, + installed_catalog_id="v9-251231-amd64", + target_catalog_version="v9-260129-amd64", + db2u_namespaces=["db2u-system"], + db2u_resource_kind=resource_kind, + db2u_version="11.5.9.0", + db2u_target_version="v12.0", + mas_instances=[{ + "metadata": {"name": "inst1"}, + "status": {"versions": {"reconciled": "9.1.7"}} + }], + argv=[ + '--catalog', 'v9-260129-amd64', + '--db2-v12-upgrade', + '--db2-license-file', invalid_license_file, + '--no-confirm' + ], + expect_exception=FileNotFoundError, timeout_seconds=30 ) @@ -291,13 +362,11 @@ def test_db2u_same_version_no_upgrade(tmpdir, resource_kind): @pytest.mark.parametrize("resource_kind", ["Db2uCluster", "Db2uInstance"]) def test_db2u_combined_namespace_and_version_upgrade(tmpdir, resource_kind): - """Test non-interactive update with both namespace arg and version upgrade flag. + """Test non-interactive update with namespace arg, version flag, and license file.""" - Expected behavior: - - Uses explicit namespace argument - - Confirms major version upgrade with flag - - Update proceeds successfully - """ + valid_license_file = os.path.join(str(tmpdir), "db2-license-combined.lic") + with open(valid_license_file, "w") as handle: + handle.write("db2-license") config = UpdateTestConfig( prompt_handlers={}, # No prompts in non-interactive mode @@ -312,7 +381,13 @@ def test_db2u_combined_namespace_and_version_upgrade(tmpdir, resource_kind): "metadata": {"name": "inst1"}, "status": {"versions": {"reconciled": "9.1.7"}} }], - argv=['--catalog', 'v9-260129-amd64', '--db2-namespace', 'db2u-ns1', '--db2-v12-upgrade', '--no-confirm'], + argv=[ + '--catalog', 'v9-260129-amd64', + '--db2-namespace', 'db2u-ns1', + '--db2-v12-upgrade', + '--db2-license-file', valid_license_file, + '--no-confirm' + ], timeout_seconds=30 ) diff --git a/python/test/utils/update_test_helper.py b/python/test/utils/update_test_helper.py index f8f98ef8051..1ee92e95745 100644 --- a/python/test/utils/update_test_helper.py +++ b/python/test/utils/update_test_helper.py @@ -45,6 +45,7 @@ def __init__( timeout_seconds: int = 30, expect_system_exit: bool = False, expected_exit_code: Optional[int] = None, + expect_exception: Optional[type] = None, argv: Optional[list] = None ): """ @@ -71,6 +72,7 @@ def __init__( timeout_seconds: Timeout for watchdog (default 30s) expect_system_exit: Whether to expect SystemExit to be raised expected_exit_code: Expected exit code if SystemExit is raised + expect_exception: Expect a specific exception type to be raised (e.g. FileNotFoundError) argv: Command line arguments to pass to app.update() (default: []) """ self.prompt_handlers = prompt_handlers @@ -93,6 +95,7 @@ def __init__( self.timeout_seconds = timeout_seconds self.expect_system_exit = expect_system_exit self.expected_exit_code = expected_exit_code + self.expect_exception = expect_exception self.argv = argv if argv is not None else [] @@ -461,6 +464,7 @@ def run_update_test(self): ('install_pipelines', mock.patch('mas.cli.update.app.installOpenShiftPipelines')), ('create_namespace', mock.patch('mas.cli.update.app.createNamespace')), ('prepare_pipelines_namespace', mock.patch('mas.cli.update.app.preparePipelinesNamespace')), + ('prepare_install_secrets', mock.patch('mas.cli.update.app.prepareInstallSecrets')), ('update_tekton_definitions', mock.patch('mas.cli.update.app.updateTektonDefinitions')), ('launch_update_pipeline', mock.patch('mas.cli.update.app.launchUpdatePipeline')), ('mixins_prompt', mock.patch('mas.cli.displayMixins.prompt')), @@ -520,6 +524,8 @@ def run_update_test(self): # Setup prompt handler self.setup_prompt_handler(mocks['mixins_prompt'], prompt_session_instance) + exception_raised = None + try: self.app = UpdateApp() self.app.update(argv=self.config.argv) @@ -528,6 +534,10 @@ def run_update_test(self): exit_code = e.code if not self.config.expect_system_exit: raise + except Exception as e: + exception_raised = e + if self.config.expect_exception is None or not isinstance(e, self.config.expect_exception): + raise finally: self.stop_watchdog() @@ -535,6 +545,10 @@ def run_update_test(self): if self.test_failed['message']: raise TimeoutError(self.test_failed['message']) + # Verify specific exception was raised if expected + if self.config.expect_exception is not None and exception_raised is None: + raise AssertionError(f"Expected {self.config.expect_exception.__name__} to be raised but it was not") + # Verify SystemExit was raised if expected if self.config.expect_system_exit and not system_exit_raised: raise AssertionError("Expected SystemExit to be raised but it was not") diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 8f932bcbad1..7377f7f503e 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -116,18 +116,18 @@ spec: # 2.3 Db2 # 2.3.1 System Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system', 'db2_license_workspace': 'true'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system'}) | indent(4) }} runAfter: - cert-manager # 2.3.2 Dedicated Manage Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage', 'db2_license_workspace': 'true'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage'}) | indent(4) }} runAfter: - db2-system # 2.3.3 Install Dedicated Db2 for AI Service # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice', 'db2_license_workspace': 'true'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice'}) | indent(4) }} runAfter: - cert-manager @@ -466,7 +466,7 @@ spec: # 13. Install and configure Facilities # ------------------------------------------------------------------------- # 13.1 Dedicated Facilities Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/apps/db2-setup-facilities.yml.j2', template_vars={'db2_license_workspace': 'true'}) | indent(4)}} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/apps/db2-setup-facilities.yml.j2') | indent(4)}} runAfter: - db2-manage diff --git a/tekton/src/pipelines/mas-update.yml.j2 b/tekton/src/pipelines/mas-update.yml.j2 index dd680073ef2..30a3ad94084 100644 --- a/tekton/src/pipelines/mas-update.yml.j2 +++ b/tekton/src/pipelines/mas-update.yml.j2 @@ -4,6 +4,10 @@ kind: Pipeline metadata: name: mas-update spec: + workspaces: + # Db2 License File + - name: shared-db2 + optional: true params: # Tekton Pipeline Configuration # ------------------------------------------------------------------------- @@ -51,6 +55,10 @@ spec: type: string description: Approves the Db2 upgrade to version 12 if needed default: "" + - name: db2_license_file + type: string + description: Optional path to a Db2 v12 activation license file; required at runtime for Db2 v11 to v12 upgrades + default: "" - name: db2_channel type: string description: Db2 channel to be upgraded @@ -295,8 +303,13 @@ spec: value: $(params.db2_namespace) - name: db2_v12_upgrade value: $(params.db2_v12_upgrade) + - name: db2_license_file + value: $(params.db2_license_file) - name: db2_channel value: $(params.db2_channel) + workspaces: + - name: db2 + workspace: shared-db2 - name: update-mongodb timeout: "0" diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index 7234e6ae74f..bed330b9543 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -146,7 +146,5 @@ workspaces: - name: configs workspace: shared-configs -{% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: db2 - workspace: shared-db2 -{% endif %} \ No newline at end of file + workspace: shared-db2 \ No newline at end of file From 2f759a84a1753e7ceb795eba2d1935ac02a75753 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:44:27 -0300 Subject: [PATCH 18/22] [patch] make db2 license workspace conditional per task --- tekton/src/pipelines/mas-install.yml.j2 | 6 +++--- .../src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 | 2 -- tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 | 4 +++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 7377f7f503e..578d07009b9 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -116,18 +116,18 @@ spec: # 2.3 Db2 # 2.3.1 System Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'system', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - cert-manager # 2.3.2 Dedicated Manage Db2 - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'manage', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - db2-system # 2.3.3 Install Dedicated Db2 for AI Service # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice'}) | indent(4) }} + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/db2.yml.j2', template_vars={'suffix': 'aiservice', 'db2_license_workspace': 'true'}) | indent(4) }} runAfter: - cert-manager diff --git a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 index 5941968e8cf..00c86f12d29 100644 --- a/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 +++ b/tekton/src/pipelines/taskdefs/apps/db2-setup-facilities.yml.j2 @@ -130,7 +130,5 @@ workspaces: - name: configs workspace: shared-configs -{% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: db2 workspace: shared-db2 -{% endif %} diff --git a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 index bed330b9543..7234e6ae74f 100644 --- a/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 +++ b/tekton/src/pipelines/taskdefs/dependencies/db2.yml.j2 @@ -146,5 +146,7 @@ workspaces: - name: configs workspace: shared-configs +{% if db2_license_workspace is defined and db2_license_workspace == 'true' %} - name: db2 - workspace: shared-db2 \ No newline at end of file + workspace: shared-db2 +{% endif %} \ No newline at end of file From a5e4219a7e29b398a11d20ae6e763f0f93003668 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:54:13 -0300 Subject: [PATCH 19/22] [patch] consolidate slack and install secrets into prepareUpdateSecrets --- python/src/mas/cli/update/app.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/python/src/mas/cli/update/app.py b/python/src/mas/cli/update/app.py index 94fe6f30a0c..404ab297ccf 100644 --- a/python/src/mas/cli/update/app.py +++ b/python/src/mas/cli/update/app.py @@ -24,7 +24,7 @@ from mas.devops.ocp import createNamespace, getConsoleURL, getClusterVersion, isClusterVersionInRange from mas.devops.mas import listMasInstances, getCurrentCatalog from mas.devops.aiservice import listAiServiceInstances -from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchUpdatePipeline, prepareUpdateSlackSecrets, prepareInstallSecrets +from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchUpdatePipeline, prepareUpdateSecrets from ..install.settings import AdditionalConfigsMixin @@ -215,22 +215,12 @@ def update(self, argv): with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h: createNamespace(self.dynamicClient, pipelinesNamespace) preparePipelinesNamespace(dynClient=self.dynamicClient) - prepareInstallSecrets( + prepareUpdateSecrets( dynClient=self.dynamicClient, - namespace=pipelinesNamespace, + slack_token=self.getParam("slack_token"), + slack_channel=self.getParam("slack_channel"), db2LicenseFile=self.db2LicenseFileSecret, ) - h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") - - # Create slack secret if slack token and channel are provided - if self.getParam("slack_token") and self.getParam("slack_channel"): - with Halo(text='Creating Slack notification secret', spinner=self.spinner) as h: - prepareUpdateSlackSecrets( - dynClient=self.dynamicClient, - slack_token=self.getParam("slack_token"), - slack_channel=self.getParam("slack_channel") - ) - h.stop_and_persist(symbol=self.successIcon, text="Slack notification secret created") with Halo(text=f'Installing latest Tekton definitions (v{self.version})', spinner=self.spinner) as h: updateTektonDefinitions(pipelinesNamespace, self.tektonDefsPath) From 572be47b064ca53c572bb9b2cad6f7c1021614df Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:22:58 -0300 Subject: [PATCH 20/22] [patch] update mock patch reference to prepareUpdateSecrets --- python/test/utils/update_test_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/test/utils/update_test_helper.py b/python/test/utils/update_test_helper.py index 1ee92e95745..941efd0216d 100644 --- a/python/test/utils/update_test_helper.py +++ b/python/test/utils/update_test_helper.py @@ -464,7 +464,7 @@ def run_update_test(self): ('install_pipelines', mock.patch('mas.cli.update.app.installOpenShiftPipelines')), ('create_namespace', mock.patch('mas.cli.update.app.createNamespace')), ('prepare_pipelines_namespace', mock.patch('mas.cli.update.app.preparePipelinesNamespace')), - ('prepare_install_secrets', mock.patch('mas.cli.update.app.prepareInstallSecrets')), + ('prepare_update_secrets', mock.patch('mas.cli.update.app.prepareUpdateSecrets')), ('update_tekton_definitions', mock.patch('mas.cli.update.app.updateTektonDefinitions')), ('launch_update_pipeline', mock.patch('mas.cli.update.app.launchUpdatePipeline')), ('mixins_prompt', mock.patch('mas.cli.displayMixins.prompt')), From 1616aa5768fbfef3115947b308a1c4202222ea78 Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 6 May 2026 06:44:22 -0300 Subject: [PATCH 21/22] [patch] change default branch from main to master for tag builds --- image/cli/install/pre-install-rbac.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image/cli/install/pre-install-rbac.sh b/image/cli/install/pre-install-rbac.sh index babeba1933c..5cbcf778736 100644 --- a/image/cli/install/pre-install-rbac.sh +++ b/image/cli/install/pre-install-rbac.sh @@ -45,8 +45,8 @@ else 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" + PREINSTALL_BRANCH="master" + echo "Using master branch for tag build" fi # Clone the repository From 5f352015288cd1a75c2e66eca228185b08208ede Mon Sep 17 00:00:00 2001 From: terc1997 <64480693+terc1997@users.noreply.github.com> Date: Wed, 6 May 2026 06:55:52 -0300 Subject: [PATCH 22/22] [patch] change default branch from main to master for tag builds --- image/cli/install/pre-install-rbac.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/image/cli/install/pre-install-rbac.sh b/image/cli/install/pre-install-rbac.sh index 5cbcf778736..415260a180e 100644 --- a/image/cli/install/pre-install-rbac.sh +++ b/image/cli/install/pre-install-rbac.sh @@ -44,7 +44,7 @@ else PREINSTALL_BRANCH="${GITHUB_REF_NAME}" echo "Attempting to clone matching branch: ${PREINSTALL_BRANCH}" else - # For tag builds, use main branch + # For tag builds, use master branch PREINSTALL_BRANCH="master" echo "Using master branch for tag build" fi @@ -54,8 +54,8 @@ else 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 + echo "Branch ${PREINSTALL_BRANCH} not found, falling back to master branch" + git clone --depth 1 --branch master https://github.com/ibm-mas/pre-install.git fi PREINSTALL_SOURCE="/tmp/install/pre-install"