Skip to content

Commit 6fd085c

Browse files
authored
Add auto-delete and auto-detach feature for ResourceClaims (#101)
- Auto-delete allows the ResourceClaim to automatically delete based on the resource status. - Auto-detach allows the ResoruceClaim to delete its ResourceHandle and transition to a "detached" state.
1 parent 8656b51 commit 6fd085c

File tree

8 files changed

+481
-15
lines changed

8 files changed

+481
-15
lines changed

helm/crds/resourceclaims.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ spec:
5050
description: ResourceClaim specification
5151
type: object
5252
properties:
53+
autoDelete:
54+
description: |
55+
Configuration for auto-delete of the ResourceClaim.
56+
type: object
57+
properties:
58+
when:
59+
description: |
60+
Condition to check which triggers deletion.
61+
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
62+
type: string
63+
autoDetach:
64+
description: |
65+
Configuration for auto-delete of the ResourceClaim.
66+
type: object
67+
properties:
68+
when:
69+
description: |
70+
Condition to check which triggers detach of ResourceHandle from the ResourceClaim.
71+
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
72+
type: string
5373
lifespan:
5474
description: >-
5575
Lifespan configuration for the ResourceClaim.
@@ -203,6 +223,10 @@ spec:
203223
properties:
204224
apiVersion:
205225
type: string
226+
detached:
227+
description: >-
228+
If true then ResourceHandle is no longer associated to this ResourceClaim.
229+
type: boolean
206230
kind:
207231
type: string
208232
name:

helm/templates/crds/resourceclaims.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ spec:
5151
description: ResourceClaim specification
5252
type: object
5353
properties:
54+
autoDelete:
55+
description: |
56+
Configuration for auto-delete of the ResourceClaim.
57+
type: object
58+
properties:
59+
when:
60+
description: |
61+
Condition to check which triggers deletion.
62+
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
63+
type: string
64+
autoDetach:
65+
description: |
66+
Configuration for auto-delete of the ResourceClaim.
67+
type: object
68+
properties:
69+
when:
70+
description: |
71+
Condition to check which triggers detach of ResourceHandle from the ResourceClaim.
72+
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
73+
type: string
5474
lifespan:
5575
description: >-
5676
Lifespan configuration for the ResourceClaim.
@@ -204,6 +224,10 @@ spec:
204224
properties:
205225
apiVersion:
206226
type: string
227+
detached:
228+
description: >-
229+
If true then ResourceHandle is no longer associated to this ResourceClaim.
230+
type: boolean
207231
kind:
208232
type: string
209233
name:

operator/resourceclaim.py

+96-3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ def approval_state(self) -> Optional[str]:
132132
"""Return approval state of this ResourceClaim."""
133133
return self.status.get('approval', {}).get('state')
134134

135+
@property
136+
def auto_delete_when(self) -> Optional[str]:
137+
"""Return condition which triggers automatic delete if defined."""
138+
return self.spec.get('autoDelete', {}).get('when')
139+
140+
@property
141+
def auto_detach_when(self) -> Optional[str]:
142+
"""Return condition which triggers automatic detach if defined."""
143+
return self.spec.get('autoDetach', {}).get('when')
144+
135145
@property
136146
def claim_is_initialized(self) -> bool:
137147
return f"{Poolboy.operator_domain}/resource-claim-init-timestamp" in self.annotations
@@ -181,6 +191,13 @@ def is_approved(self) -> bool:
181191
If claim is already has a resource hadle then it is considered approved."""
182192
return self.has_resource_handle or self.approval_state == 'approved'
183193

194+
@property
195+
def is_detached(self) -> bool:
196+
"""Return whether this ResourceClaim has been detached from its ResourceHandle."""
197+
if not self.status:
198+
return False
199+
return self.status.get('resourceHandle', {}).get('detached', False)
200+
184201
@property
185202
def lifespan_end_datetime(self) -> Optional[datetime]:
186203
"""Return datetime object representing when this ResourceClaim will be automatically deleted.
@@ -365,6 +382,53 @@ async def bind_resource_handle(self,
365382

366383
return resource_handle
367384

385+
def check_condition(self, when_condition, resource_handle, resource_provider):
386+
# resource_provider may be None if there is no top-level provider.
387+
resource_provider_vars = resource_provider.vars if resource_provider else {}
388+
vars_ = {
389+
**resource_provider_vars,
390+
**resource_handle.vars,
391+
"resource_claim": self,
392+
"resource_handle": resource_handle,
393+
"resource_provider": resource_provider,
394+
"metadata": self.meta,
395+
"spec": self.spec,
396+
"status": self.status,
397+
}
398+
check_value = recursive_process_template_strings(
399+
'{{(' + when_condition + ')|bool}}',
400+
variables = vars_
401+
)
402+
return check_value
403+
404+
def check_auto_delete(self, logger, resource_handle, resource_provider) -> bool:
405+
if not self.auto_delete_when:
406+
return False
407+
try:
408+
check_value = self.check_condition(
409+
resource_handle=resource_handle,
410+
resource_provider=resource_provider,
411+
when_condition=self.auto_delete_when
412+
)
413+
return check_value
414+
except Exception as exception:
415+
logger.warning(f"Auto delete check failed for {self}: {exception}")
416+
return False
417+
418+
def check_auto_detach(self, logger, resource_handle, resource_provider):
419+
if not self.auto_detach_when:
420+
return False
421+
try:
422+
check_value = self.check_condition(
423+
resource_handle=resource_handle,
424+
resource_provider=resource_provider,
425+
when_condition=self.auto_detach_when
426+
)
427+
return check_value
428+
except Exception as exception:
429+
logger.warning(f"Auto detach check failed for {self}: {exception}")
430+
return False
431+
368432
def get_resource_state_from_status(self, resource_number):
369433
if not self.status \
370434
or not 'resources' in self.status \
@@ -522,6 +586,14 @@ async def delete(self):
522586
version = Poolboy.operator_version,
523587
)
524588

589+
async def detach(self, resource_handle):
590+
await self.merge_patch_status({
591+
"resourceHandle": {
592+
"detached": True
593+
}
594+
})
595+
await resource_handle.delete()
596+
525597
async def get_resource_handle(self):
526598
return await resourcehandle.ResourceHandle.get(self.resource_handle_name)
527599

@@ -598,6 +670,17 @@ async def manage(self, logger) -> None:
598670
and self.lifespan_start_datetime > datetime.utcnow():
599671
return
600672

673+
if self.is_detached:
674+
# Normally lifespan end is tracked by the ResourceHandle.
675+
# Detached ResourceClaims have no handle.
676+
if self.lifespan_end_datetime \
677+
and self.lifespan_start_datetime < datetime.utcnow():
678+
logger.info(f"Deleting detacthed {self} at end of lifespan")
679+
await self.delete()
680+
# No further processing for detached ResourceClaim
681+
return
682+
683+
resource_provider = None
601684
if self.has_resource_provider:
602685
if self.has_spec_resources:
603686
raise kopf.TemporaryError(
@@ -619,7 +702,7 @@ async def manage(self, logger) -> None:
619702
)
620703

621704
try:
622-
provider = await self.get_resource_provider()
705+
resource_provider = await self.get_resource_provider()
623706
except kubernetes_asyncio.client.exceptions.ApiException as e:
624707
if e.status == 404:
625708
raise kopf.TemporaryError(
@@ -629,11 +712,11 @@ async def manage(self, logger) -> None:
629712
else:
630713
raise
631714

632-
if provider.approval_required:
715+
if resource_provider.approval_required:
633716
if not 'approval' in self.status:
634717
await self.merge_patch_status({
635718
"approval": {
636-
"message": provider.approval_pending_message,
719+
"message": resource_provider.approval_pending_message,
637720
"state": "pending"
638721
}
639722
})
@@ -683,6 +766,16 @@ async def manage(self, logger) -> None:
683766
resource_claim_resources = resource_claim_resources,
684767
)
685768

769+
if self.check_auto_delete(logger=logger, resource_handle=resource_handle, resource_provider=resource_provider):
770+
logger.info(f"auto-delete of {self} triggered")
771+
await self.delete()
772+
return
773+
774+
if self.check_auto_detach(logger=logger, resource_handle=resource_handle, resource_provider=resource_provider):
775+
logger.info(f"auto-detach of {self} triggered")
776+
await self.detach(resource_handle=resource_handle)
777+
return
778+
686779
await self.__manage_resource_handle(
687780
logger = logger,
688781
resource_claim_resources = resource_claim_resources,

operator/resourcehandle.py

+4-12
Original file line numberDiff line numberDiff line change
@@ -723,10 +723,6 @@ async def delete(self):
723723
if e.status != 404:
724724
raise
725725

726-
async def delete_resource_claim(self, logger: kopf.ObjectLogger) -> None:
727-
if not self.is_bound:
728-
return
729-
730726
async def get_resource_claim(self) -> Optional[ResourceClaimT]:
731727
if not self.is_bound:
732728
return None
@@ -807,14 +803,10 @@ async def handle_delete(self, logger: kopf.ObjectLogger) -> None:
807803

808804
if self.is_bound:
809805
try:
810-
await Poolboy.custom_objects_api.delete_namespaced_custom_object(
811-
group = Poolboy.operator_domain,
812-
name = self.resource_claim_name,
813-
namespace = self.resource_claim_namespace,
814-
plural = 'resourceclaims',
815-
version = Poolboy.operator_version,
816-
)
817-
logger.info(f"Propagated delete of ResourceHandle {self.name} to ResourceClaim {self.resource_claim_name} in {self.resource_claim_namespace}")
806+
resource_claim = await self.get_resource_claim()
807+
if not resource_claim.is_detached:
808+
await resource_claim.delete()
809+
logger.info(f"Propagated delete of ResourceHandle {self.name} to ResourceClaim {self.resource_claim_name} in {self.resource_claim_namespace}")
818810
except kubernetes_asyncio.client.exceptions.ApiException as e:
819811
if e.status != 404:
820812
raise

operator/resourcewatcher.py

+5
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ async def __watch(self, method, **kwargs):
197197
name = resource_claim_name,
198198
namespace = resource_claim_namespace,
199199
)
200+
201+
# Do not manage status for detached ResourceClaim
202+
if resource_claim.is_detached:
203+
continue
204+
200205
prev_state = resource_claim.status_resources[resource_index].get('state')
201206
prev_description = (
202207
f"{prev_state['apiVersion']} {prev_state['kind']} {resource_name} in {resource_namespace}"

0 commit comments

Comments
 (0)