Skip to content

Commit 189142d

Browse files
committed
[major] More tidy
1 parent 31364f9 commit 189142d

File tree

4 files changed

+162
-55
lines changed

4 files changed

+162
-55
lines changed

Makefile

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
.PHONY: install build lint pyinstaller clean
22

3-
venv:
4-
python3 -m venv venv
3+
.venv:
4+
python3 -m venv .venv
55

66
clean:
7-
rm -rf venv
7+
rm -rf .venv
88

9-
install: venv
10-
. venv/bin/activate && python -m pip install --editable .[dev]
9+
install: .venv
10+
. .venv/bin/activate && python -m pip install --editable .[dev]
1111

12-
build: venv
12+
build: .venv
1313
rm -f README.rst
14-
. venv/bin/activate && python -m build
14+
. .venv/bin/activate && python -m build
1515

1616
# Note: "make install" needs to be ran once before this target will work, but we
1717
# don't want to set it as a dependency otherwise it unnecessarily slows down
1818
# fast implement/test cycles. "make install" created an editable install of the
1919
# package which is linked to the files you are editing so there is no need to
2020
# re-install after each change.
2121
unit-test:
22-
. venv/bin/activate && pytest test/src/mock
22+
. .venv/bin/activate && pytest test/src/mock
2323

24-
lint: venv
24+
lint: .venv
2525
rm -f README.rst
26-
. venv/bin/activate && flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics && flake8 src --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
26+
. .venv/bin/activate && flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics && flake8 src --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
2727

2828
pyinstaller: venv
2929
rm -f README.rst
30-
. venv/bin/activate && pyinstaller src/mas-upgrade --onefile --noconfirm --add-data="src/mas/devops/templates/ibm-mas-tekton.yaml:mas/devops/templates" --add-data="src/mas/devops/templates/subscription.yml.j2:mas/devops/templates/" --add-data="src/mas/devops/templates/pipelinerun-upgrade.yml.j2:mas/devops/templates/"
30+
. .venv/bin/activate && pyinstaller src/mas-upgrade --onefile --noconfirm --add-data="src/mas/devops/templates/ibm-mas-tekton.yaml:mas/devops/templates" --add-data="src/mas/devops/templates/subscription.yml.j2:mas/devops/templates/" --add-data="src/mas/devops/templates/pipelinerun-upgrade.yml.j2:mas/devops/templates/"

src/mas/devops/mas/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .apps import ( # noqa: F401
22
verifyAppInstance,
3-
getAppsSubscriptionChannel
3+
getAppsSubscriptionChannel,
4+
waitForAppReady
45
)
56

67
from .suite import ( # noqa: F401

src/mas/devops/mas/apps.py

Lines changed: 144 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,160 @@
99
# *****************************************************************************
1010

1111
import logging
12+
import json
13+
from time import sleep
1214
from openshift.dynamic import DynamicClient
1315
from openshift.dynamic.exceptions import NotFoundError, ResourceNotFoundError, UnauthorizedError
1416

1517
from ..olm import getSubscription
1618

1719
logger = logging.getLogger(__name__)
1820

21+
# IoT has a different api version
22+
APP_API_VERSIONS = dict(iot="iot.ibm.com/v1")
1923

20-
def verifyAppInstance(dynClient: DynamicClient, instanceId: str, applicationId: str) -> bool:
24+
APP_IDS = [
25+
"assist",
26+
"facilities",
27+
"iot",
28+
"manage",
29+
"monitor",
30+
"optimizer",
31+
"predict",
32+
"visualinspection"
33+
]
34+
APP_KINDS = dict(
35+
predict="PredictApp",
36+
monitor="MonitorApp",
37+
iot="IoT",
38+
visualinspection="VisualInspectionApp",
39+
assist="AssistApp",
40+
manage="ManageApp",
41+
optimizer="OptimizerApp",
42+
facilities="FacilitiesApp",
43+
)
44+
APPWS_KINDS = dict(
45+
predict="PredictWorkspace",
46+
monitor="MonitorWorkspace",
47+
iot="IoTWorkspace",
48+
visualinspection="VisualInspectionWorkspace",
49+
assist="AssistWorkspace",
50+
manage="ManageWorkspace",
51+
optimizer="OptimizerWorkspace",
52+
facilities="FacilitiesWorkspace",
53+
)
54+
55+
56+
def getAppResource(dynClient: DynamicClient, instanceId: str, applicationId: str, workspaceId: str = None) -> bool:
2157
"""
22-
Validate that the chosen app instance exists
58+
Get the application or workspace Custom Resource
59+
60+
:param dynClient: Description
61+
:type dynClient: DynamicClient
62+
:param instanceId: Description
63+
:type instanceId: str
64+
:param applicationId: Description
65+
:type applicationId: str
66+
:return: Description
67+
:rtype: bool
68+
:type workspaceId: str
69+
:return: Description
70+
:rtype: bool
2371
"""
72+
73+
apiVersion = APP_API_VERSIONS[applicationId] if applicationId in APP_API_VERSIONS else "apps.mas.ibm.com/v1"
74+
kind = APP_KINDS[applicationId] if workspaceId is None else APPWS_KINDS[applicationId]
75+
name = instanceId if workspaceId is None else f"{instanceId}-{workspaceId}"
76+
namespace = f"mas-{instanceId}-{applicationId}"
77+
78+
# logger.debug(f"Getting {kind}.{apiVersion} {name} from {namespace}")
79+
2480
try:
25-
# IoT has a different api version
26-
operatorApiVersions = dict(iot="iot.ibm.com/v1")
27-
apiVersion = operatorApiVersions[applicationId] if applicationId in operatorApiVersions else "apps.mas.ibm.com/v1"
28-
operatorKinds = dict(
29-
health="HealthApp",
30-
predict="PredictApp",
31-
monitor="MonitorApp",
32-
iot="IoT",
33-
visualinspection="VisualInspectionApp",
34-
assist="AssistApp",
35-
manage="ManageApp",
36-
optimizer="OptimizerApp",
37-
facilities="FacilitiesApp",
38-
)
39-
appAPI = dynClient.resources.get(api_version=apiVersion, kind=operatorKinds[applicationId])
40-
appAPI.get(name=instanceId, namespace=f"mas-{instanceId}-{applicationId}")
41-
return True
81+
appAPI = dynClient.resources.get(api_version=apiVersion, kind=kind)
82+
resource = appAPI.get(name=name, namespace=namespace)
83+
return resource
4284
except NotFoundError:
43-
return False
85+
return None
4486
except ResourceNotFoundError:
45-
# The MAS App CRD has not even been installed in the cluster
46-
return False
47-
except UnauthorizedError:
48-
logger.error("Error: Unable to verify MAS app instance due to failed authorization: {e}")
49-
return False
87+
# The CRD has not even been installed in the cluster
88+
return None
89+
except UnauthorizedError as e:
90+
logger.error(f"Error: Unable to lookup {kind}.{apiVersion} due to authorization failure: {e}")
91+
return None
92+
93+
94+
def verifyAppInstance(dynClient: DynamicClient, instanceId: str, applicationId: str) -> bool:
95+
"""
96+
Validate that the chosen app instance exists
97+
"""
98+
return getAppResource(dynClient, instanceId, applicationId) is not None
99+
100+
101+
def waitForAppReady(dynClient: DynamicClient, instanceId: str, applicationId: str, workspaceId: str = None, retries: int = 100, delay: int = 600) -> bool:
102+
"""
103+
Docstring for waitForAppReady
104+
105+
:param dynClient: Description
106+
:type dynClient: DynamicClient
107+
:param instanceId: Description
108+
:type instanceId: str
109+
:param applicationId: Description
110+
:type applicationId: str
111+
:param workspaceId: Description
112+
:type workspaceId: str
113+
:param retries: Description
114+
:type retries: int
115+
:param delay: Description
116+
:type delay: int
117+
:return: Description
118+
:rtype: bool
119+
"""
120+
resourceName = f"{APP_KINDS[applicationId]}/{instanceId}"
121+
if workspaceId is not None:
122+
resourceName = f"{APPWS_KINDS[applicationId]}/{instanceId}-{workspaceId}"
123+
124+
appCR = None
125+
appStatus = None
126+
127+
attempt = 0
128+
logger.info(f"Polling for {resourceName} to report ready state")
129+
130+
while attempt < retries:
131+
attempt += 1
132+
appCR = getAppResource(dynClient, instanceId, applicationId, workspaceId)
133+
134+
if appCR is None:
135+
logger.info(f"[{attempt}/{retries}] {resourceName} does not exist")
136+
else:
137+
appStatus = appCR.status
138+
if appStatus is None:
139+
logger.info(f"[{attempt}/{retries}] {resourceName} has no status")
140+
else:
141+
if appStatus.conditions is None:
142+
logger.info(f"[{attempt}/{retries}] {resourceName} has no status conditions")
143+
else:
144+
foundReadyCondition: bool = False
145+
for condition in appStatus.conditions:
146+
if condition.type == "Ready":
147+
foundReadyCondition = True
148+
if condition.status == "True":
149+
logger.info(f"[{attempt}/{retries}] {resourceName} is in ready state: {condition.message}")
150+
logger.debug(f"CR status={json.dumps(appStatus.to_dict())}")
151+
return True
152+
else:
153+
logger.info(f"[{attempt}/{retries}] {resourceName} is not in ready state: {condition.message}")
154+
continue
155+
if not foundReadyCondition:
156+
logger.info(f"[{attempt}/{retries}] {resourceName} has no ready status condition")
157+
sleep(delay)
158+
159+
# If we made it this far it means that the application was not ready in time
160+
logger.warning(f"Retry limit reached polling for {resourceName} to report ready state")
161+
if appStatus is None:
162+
logger.debug("No CR status available")
163+
else:
164+
logger.debug(f"CR status={json.dumps(appStatus.to_dict())}")
165+
return False
50166

51167

52168
def getAppsSubscriptionChannel(dynClient: DynamicClient, instanceId: str) -> list:
@@ -55,25 +171,10 @@ def getAppsSubscriptionChannel(dynClient: DynamicClient, instanceId: str) -> lis
55171
"""
56172
try:
57173
installedApps = []
58-
appKinds = [
59-
"assist",
60-
"facilities",
61-
"health",
62-
"hputilities",
63-
"iot",
64-
"manage",
65-
"monitor",
66-
"mso",
67-
"optimizer",
68-
"safety",
69-
"predict",
70-
"visualinspection",
71-
"aibroker"
72-
]
73-
for appKind in appKinds:
74-
appSubscription = getSubscription(dynClient, f"mas-{instanceId}-{appKind}", f"ibm-mas-{appKind}")
174+
for appId in APP_IDS:
175+
appSubscription = getSubscription(dynClient, f"mas-{instanceId}-{appId}", f"ibm-mas-{appId}")
75176
if appSubscription is not None:
76-
installedApps.append({"appId": appKind, "channel": appSubscription.spec.channel})
177+
installedApps.append({"appId": appId, "channel": appSubscription.spec.channel})
77178
return installedApps
78179
except NotFoundError:
79180
return []

test/src/test_mas.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ def test_is_airgap_install():
6262
# The cluster we are using to test with does not have the MAS ICSP or IDMS installed
6363
assert mas.isAirgapInstall(dynClient) is False
6464
assert mas.isAirgapInstall(dynClient, checkICSP=False) is False
65+
66+
67+
# def test_is_app_ready():
68+
# mas.waitForAppReady(dynClient, "fvtcpd", "iot")
69+
# mas.waitForAppReady(dynClient, "fvtcpd", "iot", "masdev")

0 commit comments

Comments
 (0)