@@ -64,6 +64,14 @@ def disable_crd_implemented(self):
6464 """Set False to crd is implemented attribute"""
6565 self .__class__ .CRD_IMPLEMENTED = False
6666
67+ def after_create (self , params , instance ):
68+ """Called after create with the created instance.
69+
70+ Override this method in subclasses to perform post-creation actions
71+ like creating related CRDs (e.g., ApplicationAuth for OIDC services).
72+ """
73+ pass
74+
6775 def fetch_crd_entity (self , name : str ):
6876 """Fetches the entity based on crd name
6977 Args:
@@ -201,7 +209,10 @@ def create(self, params: dict = None, **kwargs) -> "DefaultResourceCRD":
201209 spec ["metadata" ]["namespace" ] = self .threescale_client .ocp_namespace
202210 spec ["metadata" ]["name" ] = name
203211 spec = self ._set_provider_ref_new_crd (spec )
204- self .before_create (params , spec )
212+ # before_create may return dependent objects that need to be waited on
213+ dependencies = self .before_create (params , spec ) or []
214+ if not isinstance (dependencies , list ):
215+ dependencies = [dependencies ]
205216
206217 spec ["spec" ].update (self .translate_to_crd (params ))
207218 DefaultClientCRD .cleanup_spec (spec , self .KEYS , params )
@@ -239,13 +250,47 @@ def create(self, params: dict = None, **kwargs) -> "DefaultResourceCRD":
239250 assert created_objects
240251 assert success
241252
253+ # Wait for dependent objects created in before_create to be fully ready with IDs
254+ for dep in dependencies :
255+ self ._wait_for_dependency_ready (dep )
256+
242257 instance = (self ._create_instance (response = created_objects )[:1 ] or [None ])[
243258 0
244259 ]
260+
261+ # Call after_create hook if defined
262+ self .after_create (params , instance )
263+
245264 return instance
246265
247266 return threescale_api .defaults .DefaultClient .create (self , params , ** kwargs )
248267
268+ def _wait_for_dependency_ready (self , dep ):
269+ """Wait for a dependent resource to be fully ready with its ID.
270+
271+ Args:
272+ dep: tuple of (resource_instance, id_name) where resource_instance
273+ is the created dependent resource and id_name is the status
274+ field name for its ID (e.g., 'developerUserID')
275+ """
276+ if dep is None :
277+ return
278+ resource , id_name = dep
279+ if resource is None or not hasattr (resource , 'crd' ):
280+ return
281+
282+ @backoff .on_predicate (backoff .fibo , lambda x : not x , max_tries = 12 , jitter = None )
283+ def _wait ():
284+ resource .crd = resource .crd .refresh ()
285+ return self ._is_ready_with_id (resource .crd , id_name )
286+
287+ _wait ()
288+ # Update the resource's entity with the ID so it doesn't need to fetch again
289+ status = resource .crd .as_dict ().get ("status" , {})
290+ new_id = status .get (id_name )
291+ if new_id :
292+ resource ._entity ["id" ] = new_id
293+
249294 def _set_provider_ref_new_crd (self , spec ):
250295 """set provider reference to new crd"""
251296 if self .threescale_client .ocp_provider_ref is None :
@@ -279,6 +324,26 @@ def _is_ready(self, obj):
279324 # Orphan is valid (waiting for parent), or Synced/Ready with valid ID
280325 return state ["Orphan" ] or ((state ["Synced" ] or state ["Ready" ]) and (new_id != 0 ))
281326
327+ def _is_ready_with_id (self , obj , id_name ):
328+ """Is object ready with ID populated?
329+
330+ Unlike _is_ready, this does NOT consider Orphan state as ready.
331+ Requires Synced=True or Ready=True with a valid ID.
332+ Used for waiting on dependent objects created in before_create.
333+ """
334+ if not ("status" in obj .model and "conditions" in obj .model .status ):
335+ return False
336+ status = obj .as_dict ()["status" ]
337+ new_id = status .get (id_name , 0 )
338+ state = {"Failed" : True , "Invalid" : True , "Synced" : False , "Ready" : False }
339+ for sta in status ["conditions" ]:
340+ state [sta ["type" ]] = sta ["status" ] == "True"
341+
342+ if state ["Failed" ] or state ["Invalid" ]:
343+ return False
344+ # Require Synced or Ready with valid ID (not Orphan)
345+ return (state ["Synced" ] or state ["Ready" ]) and new_id != 0
346+
282347 def _create_instance (self , response , klass = None , collection : bool = False ):
283348 klass = klass or self ._instance_klass
284349 if self .is_crd_implemented ():
@@ -287,7 +352,8 @@ def _create_instance(self, response, klass=None, collection: bool = False):
287352 else :
288353 extracted = self ._extract_resource (response , collection )
289354 instance = self ._instantiate (extracted = extracted , klass = klass )
290- LOG .info ("[INSTANCE] CRD Created instance: %s" , str (instance ))
355+ # Avoid str(instance) as it may trigger __repr__ which accesses entity_id
356+ LOG .info ("[INSTANCE] CRD Created instance: %s" , type (instance ).__name__ )
291357 return instance
292358
293359 def _extract_resource_crd (self , response , collection , klass ):
@@ -379,6 +445,16 @@ def update(
379445 if result .status ():
380446 LOG .error ("[INSTANCE] Update CRD failed: %s" , str (result ))
381447 raise Exception (str (result ))
448+
449+ # Wait for the CRD to be synced after update
450+ @backoff .on_predicate (backoff .fibo , lambda x : not x , max_tries = 12 , jitter = None )
451+ def _wait_for_sync ():
452+ resource .crd = resource .crd .refresh ()
453+ return self ._is_ready (resource .crd )
454+
455+ if not _wait_for_sync ():
456+ LOG .warning ("[UPDATE] CRD update did not reach ready state" )
457+
382458 # return self.read(resource.entity_id)
383459 return resource
384460
@@ -583,14 +659,43 @@ def crd(self, value):
583659
584660 @property
585661 def entity_id (self ) -> int :
586- return self ._entity_id or self ._entity .get ("id" ) or self .get_id_from_crd ()
662+ """Returns entity ID, fetching from CRD if needed.
663+
664+ If the CRD is in Orphan state (waiting for parent), returns None
665+ instead of blocking. Use get_id_from_crd() to explicitly wait.
666+ """
667+ if self ._entity_id :
668+ return self ._entity_id
669+ if self ._entity .get ("id" ):
670+ return self ._entity .get ("id" )
671+ # Don't block if CRD is in Orphan state - return None
672+ if self ._crd and self ._is_orphan ():
673+ return None
674+ return self .get_id_from_crd ()
587675
588676 @entity_id .setter
589677 def entity_id (self , value ):
590678 self ._entity_id = value
591679
680+ def _is_orphan (self ):
681+ """Check if the CRD is in Orphan state (waiting for parent resource)."""
682+ if not self ._crd :
683+ return False
684+ crd_dict = self ._crd .as_dict ()
685+ status = crd_dict .get ("status" )
686+ if not status :
687+ return False
688+ conditions = status .get ("conditions" , [])
689+ for cond in conditions :
690+ if cond .get ("type" ) == "Orphan" and cond .get ("status" ) == "True" :
691+ return True
692+ return False
693+
592694 def get_id_from_crd (self ):
593- """Returns object id extracted from CRD."""
695+ """Returns object id extracted from CRD.
696+
697+ This will wait with backoff until the ID is available.
698+ """
594699 # 12 tries with fibonacci backoff: 1+1+2+3+5+8+13+21+34+55+89+144 ≈ 376 seconds (~6 min)
595700 @backoff .on_predicate (backoff .fibo , lambda x : x is None , max_tries = 12 , jitter = None )
596701 def _get_id ():
0 commit comments