Skip to content

Commit 10696b8

Browse files
author
Kumar Gaurav Sharma
committed
declarative test framework - resource from file, wait before assert expectations, addressed comments
1 parent 52a35ee commit 10696b8

File tree

8 files changed

+466
-205
lines changed

8 files changed

+466
-205
lines changed

test/declarative_test_fwk/helper.py

Lines changed: 157 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,64 +14,93 @@
1414
"""Helper for Declarative tests framework for custom resources
1515
"""
1616

17+
from declarative_test_fwk import model
18+
1719
import logging
20+
from typing import Tuple
1821
from time import sleep
1922
from acktest.k8s import resource as k8s
2023

24+
# holds custom resource helper references
2125
TEST_HELPERS = dict()
2226

2327

24-
def resource_helper(resource_kind: str):
25-
"""
26-
Decorator to discover Custom Resource Helper
27-
:param resource_kind: custom resource kind
28+
def register_resource_helper(resource_kind: str, resource_plural: str):
29+
"""Decorator to discover Custom Resource Helper
30+
31+
Args:
32+
resource_kind: custom resource kind
33+
resource_plural: custom resource kind plural
34+
35+
Returns:
36+
wrapper
2837
"""
38+
2939
def registrar(cls):
30-
TEST_HELPERS[resource_kind.lower()] = cls
31-
logging.info(f"Registered ResourceHepler: {cls.__name__} for custom resource kind: {resource_kind}")
40+
global TEST_HELPERS
41+
if issubclass(cls, ResourceHelper):
42+
TEST_HELPERS[resource_kind.lower()] = cls
43+
cls.resource_plural = resource_plural.lower()
44+
logging.info(f"Registered ResourceHelper: {cls.__name__} for custom resource kind: {resource_kind}")
45+
else:
46+
msg = f"Unable to register helper for {resource_kind} resource: {cls} is not a subclass of ResourceHelper"
47+
logging.error(msg)
48+
raise Exception(msg)
3249
return registrar
3350

3451

3552
class ResourceHelper:
36-
"""
37-
Provides generic verb (create, patch, delete) methods for custom resources.
53+
"""Provides generic verb (create, patch, delete) methods for custom resources.
3854
Keep its methods stateless. Methods are on instance to allow specialization.
3955
"""
56+
4057
DEFAULT_WAIT_SECS = 30
4158

42-
def create(self, input_data: dict, input_replacements: dict = {}):
43-
"""
44-
Creates custom resource
45-
:param input_data: resource details
46-
:param input_replacements: input replacements
47-
:return: k8s.CustomResourceReference, created custom resource
59+
def create(self, input_data: dict, input_replacements: dict = {}) -> Tuple[k8s.CustomResourceReference, dict]:
60+
"""Creates custom resource inside Kubernetes cluster per the specifications in input data.
61+
62+
Args:
63+
input_data: custom resource details
64+
input_replacements: input replacements
65+
66+
Returns:
67+
k8s.CustomResourceReference, created custom resource
4868
"""
69+
4970
reference = self.custom_resource_reference(input_data, input_replacements)
5071
_ = k8s.create_custom_resource(reference, input_data)
5172
resource = k8s.wait_resource_consumed_by_controller(reference, wait_periods=10)
5273
assert resource is not None
53-
return (reference, resource)
74+
return reference, resource
5475

55-
def patch(self, input_data: dict, input_replacements: dict = {}):
56-
"""
57-
Patches custom resource
58-
:param input_data: resource patch details
59-
:param input_replacements: input replacements
60-
:return: k8s.CustomResourceReference, created custom resource
76+
def patch(self, input_data: dict, input_replacements: dict = {}) -> Tuple[k8s.CustomResourceReference, dict]:
77+
"""Patches custom resource inside Kubernetes cluster per the specifications in input data.
78+
79+
Args:
80+
input_data: custom resource patch details
81+
input_replacements: input replacements
82+
83+
Returns:
84+
k8s.CustomResourceReference, created custom resource
6185
"""
86+
6287
reference = self.custom_resource_reference(input_data, input_replacements)
6388
_ = k8s.patch_custom_resource(reference, input_data)
6489
sleep(self.DEFAULT_WAIT_SECS) # required as controller has likely not placed the resource in modifying
6590
resource = k8s.wait_resource_consumed_by_controller(reference, wait_periods=10)
6691
assert resource is not None
67-
return (reference, resource)
92+
return reference, resource
6893

69-
def delete(self, reference: k8s.CustomResourceReference):
70-
"""
71-
Deletes custom resource and waits for delete completion
72-
:param reference: resource reference
73-
:return: None
94+
def delete(self, reference: k8s.CustomResourceReference) -> None:
95+
"""Deletes custom resource inside Kubernetes cluster and waits for delete completion
96+
97+
Args:
98+
reference: custom resource reference
99+
100+
Returns:
101+
None
74102
"""
103+
75104
resource = k8s.get_resource(reference)
76105
if not resource:
77106
logging.warning(f"ResourceReference {reference} not found. Not invoking k8s delete api.")
@@ -81,92 +110,150 @@ def delete(self, reference: k8s.CustomResourceReference):
81110
sleep(self.DEFAULT_WAIT_SECS)
82111
self.wait_for_delete(reference) # throws exception if wait fails
83112

84-
def assert_expectations(self, verb: str, input_data: dict, expectations: dict, reference: k8s.CustomResourceReference):
85-
"""
86-
Asserts custom resource reference against supplied expectations
113+
def assert_expectations(self, verb: str, input_data: dict, expectations: model.ExpectDict, reference: k8s.CustomResourceReference) -> None:
114+
"""Asserts custom resource reference inside Kubernetes cluster against the supplied expectations
115+
87116
:param verb: expectations after performing the verb (apply, patch, delete)
88117
:param input_data: input data to verb
89118
:param expectations: expectations to assert
90119
:param reference: custom resource reference
91120
:return: None
92121
"""
93-
# condition assertion contains wait logic
94-
self._assert_conditions(expectations, reference)
95-
122+
self._assert_conditions(expectations, reference, wait=False)
96123
# conditions expectations met, now check current resource against expectations
97124
resource = k8s.get_resource(reference)
98125
self.assert_items(expectations.get("status"), resource.get("status"))
99126

100127
# self._assert_state(expectations.get("spec"), resource) # uncomment to support spec assertions
101128

102-
def _assert_conditions(self, expectations: dict, reference: k8s.CustomResourceReference):
129+
def wait_for(self, wait_expectations: dict, reference: k8s.CustomResourceReference) -> None:
130+
"""Waits for custom resource reference details inside Kubernetes cluster to match supplied config,
131+
currently supports wait on "status.conditions",
132+
it can be enhanced later for wait on any/other properties.
133+
134+
Args:
135+
wait_expectations: properties to wait for
136+
reference: custom resource reference
137+
138+
Returns:
139+
None
140+
"""
141+
142+
# wait for conditions
143+
self._assert_conditions(wait_expectations, reference, wait=True)
144+
145+
def _assert_conditions(self, expectations: dict, reference: k8s.CustomResourceReference, wait: bool = True) -> None:
103146
expect_conditions: dict = {}
104147
if "status" in expectations and "conditions" in expectations["status"]:
105148
expect_conditions = expectations["status"]["conditions"]
106149

107-
for (condition_name, condition_value) in expect_conditions.items():
108-
assert k8s.wait_on_condition(reference, condition_name, condition_value, wait_periods=30)
150+
default_wait_periods = 60
151+
# period_length = 1 will result in condition check every second
152+
default_period_length = 1
153+
for (condition_name, expected_value) in expect_conditions.items():
154+
if type(expected_value) is str:
155+
# Example: ACK.Terminal: "True"
156+
if wait:
157+
assert k8s.wait_on_condition(reference, condition_name, expected_value,
158+
wait_periods=default_wait_periods, period_length=default_period_length)
159+
else:
160+
actual_condition = k8s.get_resource_condition(reference, condition_name)
161+
assert actual_condition is not None
162+
assert expected_value == actual_condition.get("status")
109163

110-
def assert_items(self, expectations: dict, state: dict):
111-
"""
112-
Asserts state against supplied expectations
164+
elif type(expected_value) is dict:
165+
# Example:
166+
# ACK.ResourceSynced:
167+
# status: "False"
168+
# message: "Expected message ..."
169+
# timeout: 60 # seconds
170+
condition_value = expected_value.get("status")
171+
condition_message = expected_value.get("message")
172+
# default wait 60 seconds
173+
wait_timeout = expected_value.get("timeout", default_wait_periods)
174+
175+
if wait:
176+
assert k8s.wait_on_condition(reference, condition_name, condition_value,
177+
wait_periods=wait_timeout, period_length=default_period_length)
178+
179+
actual_condition = k8s.get_resource_condition(reference, condition_name)
180+
assert actual_condition is not None
181+
assert condition_value == actual_condition.get("status")
182+
if condition_message is not None:
183+
assert condition_message == actual_condition.get("message")
184+
185+
else:
186+
raise Exception(f"Condition {condition_name} is provided with invalid value: {expected_value} ")
187+
188+
def assert_items(self, expectations: dict, state: dict) -> None:
189+
"""Asserts state against supplied expectations
113190
Override it as needed for custom verifications
114-
:param expectations: dictionary with items to assert in state
115-
:param state: dictionary with items
116-
:return: None
191+
192+
Args:
193+
expectations: dictionary with items (expected) to assert in state
194+
state: dictionary with items (actual)
195+
196+
Returns:
197+
None
117198
"""
199+
118200
if not expectations:
201+
# nothing to assert as there are no expectations
119202
return
120203
if not state:
204+
# there are expectations but no given state to validate
205+
# following assert will fail and assert introspection will provide useful information for debugging
121206
assert expectations == state
122207

123-
for (property, value) in expectations.items():
208+
for (key, value) in expectations.items():
124209
# conditions are processed separately
125-
if property == "conditions":
210+
if key == "conditions":
126211
continue
127-
assert (property, value) == (property, state.get(property))
212+
assert (key, value) == (key, state.get(key))
128213

129214
def custom_resource_reference(self, input_data: dict, input_replacements: dict = {}) -> k8s.CustomResourceReference:
215+
"""Helper method to provide k8s.CustomResourceReference for supplied input
216+
217+
Args:
218+
input_data: custom resource input data
219+
input_replacements: input replacements
220+
221+
Returns:
222+
k8s.CustomResourceReference
130223
"""
131-
Helper method to provide k8s.CustomResourceReference for supplied input
132-
:param input_data: custom resource input data
133-
:param input_replacements: input replacements
134-
:return: k8s.CustomResourceReference
135-
"""
136-
resource_plural = self.resource_plural(input_data.get("kind"))
224+
137225
resource_name = input_data.get("metadata").get("name")
138226
crd_group = input_replacements.get("CRD_GROUP")
139227
crd_version = input_replacements.get("CRD_VERSION")
140228

141229
reference = k8s.CustomResourceReference(
142-
crd_group, crd_version, resource_plural, resource_name, namespace="default")
230+
crd_group, crd_version, self.resource_plural, resource_name, namespace="default")
143231
return reference
144232

145-
def wait_for_delete(self, reference: k8s.CustomResourceReference):
146-
"""
147-
Override this method to implement custom resource delete logic.
148-
:param reference: custom resource reference
149-
:return: None
150-
"""
151-
logging.debug(f"No-op wait_for_delete()")
233+
def wait_for_delete(self, reference: k8s.CustomResourceReference) -> None:
234+
"""Override this method to implement custom wail logic on resource delete.
152235
153-
def resource_plural(self, resource_kind: str) -> str:
154-
"""
155-
Provide plural string for supplied custom resource kind
156-
Override as needed
157-
:param resource_kind: custom resource kind
158-
:return: plural string
236+
Args:
237+
reference: custom resource reference
238+
239+
Returns:
240+
None
159241
"""
160-
return resource_kind.lower() + "s"
242+
243+
logging.debug(f"No-op wait_for_delete()")
161244

162245

163246
def get_resource_helper(resource_kind: str) -> ResourceHelper:
164-
"""
165-
Provides ResourceHelper for supplied custom resource kind
247+
"""Provides ResourceHelper for supplied custom resource kind
166248
If no helper is registered for the supplied resource kind then returns default ResourceHelper
167-
:param resource_kind: custom resource kind
168-
:return: custom resource helper
249+
250+
Args:
251+
resource_kind: custom resource kind string
252+
253+
Returns:
254+
custom resource helper instance
169255
"""
256+
170257
helper_cls = TEST_HELPERS.get(resource_kind.lower())
171258
if helper_cls:
172259
return helper_cls()

0 commit comments

Comments
 (0)