Skip to content

Commit 8bd1708

Browse files
dixitgsathwaraDixit Sathwara
andauthored
[patch] add lookupPipelineStorageClass to read storage class from existing config-pvc (#484)
Co-authored-by: Dixit Sathwara <Dixit.Sathwara1@ibm.com>
1 parent 3e5cd22 commit 8bd1708

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

‎src/mas/devops/tekton.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,39 @@ def enablePipelinesConsolePlugin(dynClient: DynamicClient) -> bool:
297297
return False
298298

299299

300+
def lookupPipelineStorageClass(dynClient: DynamicClient, instanceId: str) -> tuple[str | None, str | None]:
301+
"""
302+
Look up the storage class and access mode already in use by the config-pvc
303+
PersistentVolumeClaim in the instance's pipelines namespace.
304+
305+
During an upgrade the pipelines namespace already exists (it was created by
306+
the original install).
307+
308+
Parameters:
309+
dynClient (DynamicClient): OpenShift Dynamic Client
310+
instanceId (str): MAS instance ID
311+
312+
Returns:
313+
tuple[str | None, str | None]: (storageClassName, accessMode) read from
314+
the existing config-pvc, or (None, None) if the PVC does not exist yet.
315+
"""
316+
namespace = f"mas-{instanceId}-pipelines"
317+
try:
318+
pvcAPI = dynClient.resources.get(api_version="v1", kind="PersistentVolumeClaim")
319+
existingPVC = pvcAPI.get(name="config-pvc", namespace=namespace)
320+
storageClass = existingPVC.spec.storageClassName or None
321+
accessModes = existingPVC.spec.accessModes or []
322+
accessMode = accessModes[0] if accessModes else None
323+
logger.info(f"Detected existing config-pvc in {namespace}: storageClass='{storageClass}', accessMode='{accessMode}'")
324+
return storageClass, accessMode
325+
except NotFoundError:
326+
logger.debug(f"config-pvc not found in {namespace}, will fall back to storage class detection")
327+
return None, None
328+
except AttributeError:
329+
logger.debug(f"config-pvc response in {namespace} was not a PVC object, will fall back to storage class detection")
330+
return None, None
331+
332+
300333
def addMissingStorageClassToTektonPVC(dynClient: DynamicClient, namespace: str, pvcName: str, storageClassName: str = None) -> bool:
301334
"""
302335
OpenShift Pipelines has a problem when there is no default storage class defined in a cluster, this function
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# *****************************************************************************
2+
# Copyright (c) 2026 IBM Corporation and other Contributors.
3+
#
4+
# All rights reserved. This program and the accompanying materials
5+
# are made available under the terms of the Eclipse Public License v1.0
6+
# which accompanies this distribution, and is available at
7+
# http://www.eclipse.org/legal/epl-v10.html
8+
#
9+
# *****************************************************************************
10+
11+
from unittest.mock import MagicMock
12+
from kubernetes.dynamic.exceptions import NotFoundError
13+
14+
from mas.devops.tekton import lookupPipelineStorageClass
15+
16+
17+
def _make_pvc(storage_class, access_modes):
18+
"""Build a minimal ResourceInstance-like mock for a PVC."""
19+
pvc = MagicMock()
20+
pvc.spec.storageClassName = storage_class
21+
pvc.spec.accessModes = access_modes
22+
return pvc
23+
24+
25+
def _make_dyn_client(pvc=None, raise_exc=None):
26+
"""Build a DynamicClient mock that returns the given PVC or raises raise_exc."""
27+
pvc_api = MagicMock()
28+
if raise_exc is not None:
29+
pvc_api.get.side_effect = raise_exc
30+
else:
31+
pvc_api.get.return_value = pvc
32+
dyn_client = MagicMock()
33+
dyn_client.resources.get.return_value = pvc_api
34+
return dyn_client
35+
36+
37+
class TestLookupPipelineStorageClass:
38+
"""Tests for tekton.lookupPipelineStorageClass()"""
39+
40+
def test_returns_storage_class_and_access_mode_from_existing_pvc(self):
41+
"""Normal upgrade path: config-pvc already exists with a known storage class."""
42+
pvc = _make_pvc("ibmc-file-gold-gid", ["ReadWriteMany"])
43+
dyn_client = _make_dyn_client(pvc=pvc)
44+
45+
sc, mode = lookupPipelineStorageClass(dyn_client, "inst1")
46+
47+
assert sc == "ibmc-file-gold-gid"
48+
assert mode == "ReadWriteMany"
49+
dyn_client.resources.get.assert_called_once_with(api_version="v1", kind="PersistentVolumeClaim")
50+
dyn_client.resources.get.return_value.get.assert_called_once_with(name="config-pvc", namespace="mas-inst1-pipelines")
51+
52+
def test_returns_rwo_access_mode_for_sno_pvc(self):
53+
"""SNO cluster: config-pvc uses ReadWriteOnce."""
54+
pvc = _make_pvc("ocs-storagecluster-cephfs", ["ReadWriteOnce"])
55+
dyn_client = _make_dyn_client(pvc=pvc)
56+
57+
sc, mode = lookupPipelineStorageClass(dyn_client, "mas1")
58+
59+
assert sc == "ocs-storagecluster-cephfs"
60+
assert mode == "ReadWriteOnce"
61+
62+
def test_returns_none_none_when_pvc_not_found(self):
63+
"""No config-pvc in namespace (MAS not installed via CLI): returns (None, None)."""
64+
http_response = MagicMock()
65+
http_response.status = 404
66+
http_response.reason = "Not Found"
67+
http_response.data = b'{"reason":"NotFound"}'
68+
not_found = NotFoundError(http_response)
69+
dyn_client = _make_dyn_client(raise_exc=not_found)
70+
71+
sc, mode = lookupPipelineStorageClass(dyn_client, "inst1")
72+
73+
assert sc is None
74+
assert mode is None
75+
76+
def test_returns_none_none_when_response_is_not_a_resource_instance(self):
77+
"""API returns a plain dict instead of a ResourceInstance (e.g. in unit tests)."""
78+
pvc_api = MagicMock()
79+
pvc_api.get.return_value = {"kind": "PersistentVolumeClaim"} # plain dict, no .spec
80+
dyn_client = MagicMock()
81+
dyn_client.resources.get.return_value = pvc_api
82+
83+
sc, mode = lookupPipelineStorageClass(dyn_client, "inst1")
84+
85+
assert sc is None
86+
assert mode is None
87+
88+
def test_returns_none_access_mode_when_access_modes_list_is_empty(self):
89+
"""PVC exists but accessModes list is empty: access mode returns None."""
90+
pvc = _make_pvc("standard", [])
91+
dyn_client = _make_dyn_client(pvc=pvc)
92+
93+
sc, mode = lookupPipelineStorageClass(dyn_client, "inst1")
94+
95+
assert sc == "standard"
96+
assert mode is None
97+
98+
def test_namespace_is_derived_from_instance_id(self):
99+
"""The lookup targets the correct pipelines namespace for the given instance ID."""
100+
pvc = _make_pvc("thin", ["ReadWriteOnce"])
101+
dyn_client = _make_dyn_client(pvc=pvc)
102+
103+
lookupPipelineStorageClass(dyn_client, "myinst")
104+
105+
dyn_client.resources.get.return_value.get.assert_called_once_with(name="config-pvc", namespace="mas-myinst-pipelines")

0 commit comments

Comments
 (0)