1414"""Helper for Declarative tests framework for custom resources
1515"""
1616
17+ from declarative_test_fwk import model
18+
1719import logging
20+ from typing import Tuple
1821from time import sleep
1922from acktest .k8s import resource as k8s
2023
24+ # holds custom resource helper references
2125TEST_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
3552class 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
163246def 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