Skip to content

Commit 7073f4c

Browse files
mdujavaclaude
andcommitted
Add option for after_create and fix Account creation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 28a4674 commit 7073f4c

2 files changed

Lines changed: 119 additions & 6 deletions

File tree

threescale_api_crd/defaults.py

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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():

threescale_api_crd/resources.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,14 +1053,22 @@ def __init__(
10531053
)
10541054

10551055
def before_create(self, params, spec):
1056-
"""Called before create."""
1056+
"""Called before create.
1057+
1058+
Returns:
1059+
List of (resource, id_name) tuples for dependent resources that
1060+
need to be waited on after the main object is created.
1061+
"""
10571062
if "username" in params:
10581063
pars = params.copy()
10591064
pars["account_name"] = pars["name"]
10601065
pars["name"] = secrets.token_urlsafe(8)
10611066
# first user should be admin
10621067
pars["role"] = "admin"
1063-
self.parent.threescale_client.account_users.create(params=pars)
1068+
user = self.parent.threescale_client.account_users.create(params=pars)
1069+
# Return the user so that create() waits for it to have its ID
1070+
return [(user, AccountUsers.ID_NAME)]
1071+
return []
10641072

10651073
def before_update(self, new_params, resource):
10661074
"""Called before update."""

0 commit comments

Comments
 (0)