Skip to content

Commit 3cf0b9c

Browse files
Dixit SathwaraDixit Sathwara
authored andcommitted
[patch] add the pre_install rbac apply function
1 parent 79a18a1 commit 3cf0b9c

2 files changed

Lines changed: 361 additions & 4 deletions

File tree

src/mas/devops/preinstall_rbac.py

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
# *****************************************************************************
2+
# Copyright (c) 2024 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+
import logging
12+
import yaml
13+
14+
from os import path, listdir
15+
16+
from kubernetes import client as k8s_client
17+
from openshift.dynamic import DynamicClient
18+
from jinja2 import Environment
19+
20+
logger = logging.getLogger(__name__)
21+
22+
DEFAULT_PREINSTALL_MAS_RBAC_ROOT = "/opt/app-root/rbac"
23+
24+
25+
def _validate_preinstall_mas_rbac_permission_mode(permissionMode: str) -> None:
26+
if permissionMode not in {"cluster", "namespaced", "minimal"}:
27+
raise ValueError(f"Unsupported permission mode: {permissionMode}")
28+
29+
30+
def _validate_selected_apps(selectedApps: list[str] | None) -> set[str]:
31+
if not selectedApps:
32+
return set()
33+
34+
validApps = {
35+
"core",
36+
"aiservice",
37+
"arcgis",
38+
"facilities",
39+
"iot",
40+
"manage",
41+
"monitor",
42+
"optimizer",
43+
"predict",
44+
"visualinspection"
45+
}
46+
47+
validatedApps = set()
48+
for app in selectedApps:
49+
if app not in validApps:
50+
raise ValueError(f"Unsupported selected app: {app}")
51+
validatedApps.add(app)
52+
53+
return validatedApps
54+
55+
56+
def _get_selected_operator_dirs(selectedApps: set[str]) -> set[str]:
57+
58+
appToOperatorDir = {
59+
"core": "ibm-mas",
60+
"aiservice": "ibm-aiservice",
61+
"arcgis": "ibm-mas-arcgis",
62+
"facilities": "ibm-mas-facilities",
63+
"iot": "ibm-mas-iot",
64+
"manage": "ibm-mas-manage",
65+
"monitor": "ibm-mas-monitor",
66+
"optimizer": "ibm-mas-optimizer",
67+
"predict": "ibm-mas-predict",
68+
"visualinspection": "ibm-mas-visualinspection"
69+
}
70+
71+
return {appToOperatorDir[app] for app in selectedApps}
72+
73+
74+
def _should_apply_preinstall_mas_rbac_file(fileName: str, permissionMode: str) -> bool:
75+
lowerName = path.basename(fileName).lower()
76+
77+
if lowerName == "kustomization.yaml":
78+
return False
79+
80+
if not (lowerName.endswith(".yml") or lowerName.endswith(".yaml")):
81+
return False
82+
83+
if permissionMode == "cluster":
84+
return lowerName.startswith("cluster-role-")
85+
86+
if permissionMode == "namespaced":
87+
return lowerName.startswith("role-non-essential-")
88+
89+
return False
90+
91+
92+
def _collect_preinstall_mas_rbac_files_from_source(
93+
sourceOperatorsRoot: str,
94+
masVersion: str,
95+
permissionMode: str,
96+
operatorNames: set[str] | None = None
97+
) -> list[str]:
98+
if not path.isdir(sourceOperatorsRoot):
99+
logger.debug(f"Skipping missing RBAC source root {sourceOperatorsRoot}")
100+
return []
101+
102+
if operatorNames is None:
103+
operatorNames = {
104+
operatorName for operatorName in listdir(sourceOperatorsRoot)
105+
if path.isdir(path.join(sourceOperatorsRoot, operatorName))
106+
}
107+
108+
manifestFiles = []
109+
for operatorName in sorted(operatorNames):
110+
operatorRoot = path.join(sourceOperatorsRoot, operatorName)
111+
if not path.isdir(operatorRoot):
112+
logger.debug(f"Skipping missing operator root {operatorRoot}")
113+
continue
114+
115+
versionDir = path.join(operatorRoot, "rbac", masVersion)
116+
if not path.isdir(versionDir):
117+
logger.debug(f"Skipping missing RBAC version directory {versionDir}")
118+
continue
119+
120+
for manifestName in sorted(listdir(versionDir)):
121+
manifestFile = path.join(versionDir, manifestName)
122+
if not path.isfile(manifestFile):
123+
continue
124+
125+
if _should_apply_preinstall_mas_rbac_file(manifestName, permissionMode):
126+
manifestFiles.append(manifestFile)
127+
128+
return manifestFiles
129+
130+
131+
def _discover_preinstall_mas_rbac_files(
132+
rbacRootDir: str | None,
133+
masVersion: str,
134+
permissionMode: str,
135+
selectedApps: set[str]
136+
) -> list[str]:
137+
if not rbacRootDir:
138+
rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT
139+
140+
if permissionMode == "minimal":
141+
return []
142+
143+
selectedOperatorDirs = _get_selected_operator_dirs(selectedApps)
144+
145+
sourceRoots = [
146+
(
147+
path.join(rbacRootDir, "maximo-operator-catalog", "operators"),
148+
selectedOperatorDirs
149+
),
150+
(
151+
path.join(rbacRootDir, "openshift-platform", "operators"),
152+
None
153+
)
154+
]
155+
156+
manifestFiles = []
157+
for sourceRoot, operatorNames in sourceRoots:
158+
manifestFiles.extend(
159+
_collect_preinstall_mas_rbac_files_from_source(
160+
sourceOperatorsRoot=sourceRoot,
161+
masVersion=masVersion,
162+
permissionMode=permissionMode,
163+
operatorNames=operatorNames
164+
)
165+
)
166+
167+
return sorted(set(manifestFiles))
168+
169+
170+
def _get_preinstall_mas_rbac_namespaces(masInstanceId: str, permissionMode: str, selectedApps: set[str]) -> set[str]:
171+
if permissionMode == "minimal":
172+
return set()
173+
174+
namespaces = {f"mas-{masInstanceId}-core"}
175+
176+
if permissionMode == "cluster":
177+
return namespaces
178+
179+
appNamespaces = {
180+
"aiservice": f"mas-{masInstanceId}-aiservice",
181+
"arcgis": f"mas-{masInstanceId}-arcgis",
182+
"facilities": f"mas-{masInstanceId}-facilities",
183+
"iot": f"mas-{masInstanceId}-iot",
184+
"manage": f"mas-{masInstanceId}-manage",
185+
"monitor": f"mas-{masInstanceId}-monitor",
186+
"optimizer": f"mas-{masInstanceId}-optimizer",
187+
"predict": f"mas-{masInstanceId}-predict",
188+
"visualinspection": f"mas-{masInstanceId}-visualinspection"
189+
}
190+
191+
for app in selectedApps:
192+
if app in appNamespaces:
193+
namespaces.add(appNamespaces[app])
194+
195+
return namespaces
196+
197+
198+
def _check_self_subject_access(
199+
dynClient: DynamicClient,
200+
verb: str,
201+
resource: str,
202+
group: str = "rbac.authorization.k8s.io",
203+
namespace: str | None = None
204+
) -> bool:
205+
authAPI = k8s_client.AuthorizationV1Api(dynClient.client)
206+
review = k8s_client.V1SelfSubjectAccessReview(
207+
spec=k8s_client.V1SelfSubjectAccessReviewSpec(
208+
resource_attributes=k8s_client.V1ResourceAttributes(
209+
namespace=namespace,
210+
verb=verb,
211+
resource=resource,
212+
group=group
213+
)
214+
)
215+
)
216+
result = authAPI.create_self_subject_access_review(body=review)
217+
status = getattr(result, "status", None)
218+
return bool(getattr(status, "allowed", False))
219+
220+
221+
def buildClusterAdminPermissionMatrix() -> list[dict[str, str]]:
222+
return [
223+
{"verb": "create", "resource": "namespaces", "group": ""},
224+
{"verb": "create", "resource": "clusterroles"},
225+
{"verb": "update", "resource": "clusterroles"},
226+
{"verb": "create", "resource": "clusterrolebindings"},
227+
{"verb": "update", "resource": "clusterrolebindings"},
228+
]
229+
230+
231+
def permissionCheckForRBAC(
232+
dynClient: DynamicClient,
233+
checks: list[dict[str, str]] | None = None
234+
) -> list[dict[str, str | bool]]:
235+
if checks is None:
236+
checks = buildClusterAdminPermissionMatrix()
237+
238+
results = []
239+
240+
for check in checks:
241+
verb = check["verb"]
242+
resource = check["resource"]
243+
group = check.get("group", "rbac.authorization.k8s.io")
244+
namespace = check.get("namespace")
245+
246+
allowed = _check_self_subject_access(
247+
dynClient=dynClient,
248+
verb=verb,
249+
resource=resource,
250+
group=group,
251+
namespace=namespace
252+
)
253+
254+
result: dict[str, str | bool] = {
255+
"verb": verb,
256+
"resource": resource,
257+
"group": group,
258+
"allowed": allowed
259+
}
260+
261+
if namespace is not None:
262+
result["namespace"] = namespace
263+
264+
results.append(result)
265+
266+
return results
267+
268+
269+
def applyPreInstallMASRBAC(
270+
dynClient: DynamicClient,
271+
masVersion: str,
272+
masInstanceId: str,
273+
permissionMode: str,
274+
selectedApps: list[str] | None = None,
275+
rbacRootDir: str | None = None
276+
) -> None:
277+
if not rbacRootDir:
278+
rbacRootDir = DEFAULT_PREINSTALL_MAS_RBAC_ROOT
279+
280+
validatedApps = _validate_selected_apps(selectedApps)
281+
282+
if not validatedApps:
283+
logger.info("No selected apps provided for pre-install MAS RBAC apply")
284+
return
285+
286+
manifestFiles = _discover_preinstall_mas_rbac_files(
287+
rbacRootDir=rbacRootDir,
288+
masVersion=masVersion,
289+
permissionMode=permissionMode,
290+
selectedApps=validatedApps
291+
)
292+
293+
logger.info(
294+
f"Applying pre-install MAS RBAC from {rbacRootDir} for MAS {masVersion}, "
295+
f"masInstanceId={masInstanceId}, permissionMode={permissionMode}, "
296+
f"selectedApps={sorted(validatedApps)}, "
297+
f"manifestCount={len(manifestFiles)}"
298+
)
299+
300+
if not manifestFiles:
301+
logger.info("No pre-install MAS RBAC manifests selected for apply")
302+
return
303+
304+
namespaceAPI = dynClient.resources.get(api_version="v1", kind="Namespace")
305+
requiredNamespaces = _get_preinstall_mas_rbac_namespaces(
306+
masInstanceId=masInstanceId,
307+
permissionMode=permissionMode,
308+
selectedApps=validatedApps
309+
)
310+
311+
for namespace in sorted(requiredNamespaces):
312+
logger.info(f"Ensuring namespace exists for pre-install MAS RBAC: {namespace}")
313+
namespaceAPI.apply(body={
314+
"apiVersion": "v1",
315+
"kind": "Namespace",
316+
"metadata": {
317+
"name": namespace
318+
}
319+
})
320+
321+
env = Environment()
322+
appliedResourceCount = 0
323+
324+
for manifestFile in manifestFiles:
325+
logger.info(f"Applying pre-install MAS RBAC manifest {manifestFile}")
326+
with open(manifestFile, "r") as file:
327+
template = env.from_string(file.read())
328+
renderedManifest = template.render(mas_instance_id=masInstanceId)
329+
330+
for resourceBody in yaml.safe_load_all(renderedManifest):
331+
if resourceBody is None:
332+
continue
333+
334+
apiVersion = resourceBody["apiVersion"]
335+
kind = resourceBody["kind"]
336+
metadata = resourceBody["metadata"]
337+
resourceName = metadata["name"]
338+
resourceNamespace = metadata.get("namespace")
339+
340+
if kind in {"Role", "RoleBinding"} and not resourceNamespace:
341+
raise ValueError(
342+
f"Namespaced RBAC resource {kind}/{resourceName} from {manifestFile} is missing metadata.namespace"
343+
)
344+
345+
logger.debug(
346+
f"Applying {kind} {resourceName} "
347+
f"(apiVersion={apiVersion}, namespace={resourceNamespace})"
348+
)
349+
350+
resourceAPI = dynClient.resources.get(api_version=apiVersion, kind=kind)
351+
if resourceNamespace:
352+
resourceAPI.apply(body=resourceBody, namespace=resourceNamespace)
353+
else:
354+
resourceAPI.apply(body=resourceBody)
355+
356+
appliedResourceCount += 1
357+
358+
logger.info(
359+
f"Pre-install MAS RBAC apply completed: processedFiles={len(manifestFiles)}, "
360+
f"appliedResources={appliedResourceCount}"
361+
)

src/mas/devops/templates/pipelinerun-install.yml.j2

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,6 @@ spec:
561561
- name: mas_permission_mode
562562
value: "{{ mas_permission_mode }}"
563563
{%- endif %}
564-
{%- if mas_selected_apps is defined and mas_selected_apps != "" %}
565-
- name: mas_selected_apps
566-
value: "{{ mas_selected_apps }}"
567-
{%- endif %}
568564

569565
# MAS Workspace
570566
# -------------------------------------------------------------------------

0 commit comments

Comments
 (0)