Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-delete and auto-detach feature for ResourceClaims #101

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions helm/crds/resourceclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ spec:
description: ResourceClaim specification
type: object
properties:
autoDelete:
description: |
Configuration for auto-delete of the ResourceClaim.
type: object
properties:
when:
description: |
Condition to check which triggers deletion.
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
type: string
autoDetach:
description: |
Configuration for auto-delete of the ResourceClaim.
type: object
properties:
when:
description: |
Condition to check which triggers detach of ResourceHandle from the ResourceClaim.
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
type: string
lifespan:
description: >-
Lifespan configuration for the ResourceClaim.
Expand Down Expand Up @@ -203,6 +223,10 @@ spec:
properties:
apiVersion:
type: string
detached:
description: >-
If true then ResourceHandle is no longer associated to this ResourceClaim.
type: boolean
kind:
type: string
name:
Expand Down
24 changes: 24 additions & 0 deletions helm/templates/crds/resourceclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ spec:
description: ResourceClaim specification
type: object
properties:
autoDelete:
description: |
Configuration for auto-delete of the ResourceClaim.
type: object
properties:
when:
description: |
Condition to check which triggers deletion.
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
type: string
autoDetach:
description: |
Configuration for auto-delete of the ResourceClaim.
type: object
properties:
when:
description: |
Condition to check which triggers detach of ResourceHandle from the ResourceClaim.
Condition is given in Jinja2 syntax similar to ansible "when" clauses.
type: string
lifespan:
description: >-
Lifespan configuration for the ResourceClaim.
Expand Down Expand Up @@ -204,6 +224,10 @@ spec:
properties:
apiVersion:
type: string
detached:
description: >-
If true then ResourceHandle is no longer associated to this ResourceClaim.
type: boolean
kind:
type: string
name:
Expand Down
99 changes: 96 additions & 3 deletions operator/resourceclaim.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ def approval_state(self) -> Optional[str]:
"""Return approval state of this ResourceClaim."""
return self.status.get('approval', {}).get('state')

@property
def auto_delete_when(self) -> Optional[str]:
"""Return condition which triggers automatic delete if defined."""
return self.spec.get('autoDelete', {}).get('when')

@property
def auto_detach_when(self) -> Optional[str]:
"""Return condition which triggers automatic detach if defined."""
return self.spec.get('autoDetach', {}).get('when')

@property
def claim_is_initialized(self) -> bool:
return f"{Poolboy.operator_domain}/resource-claim-init-timestamp" in self.annotations
Expand Down Expand Up @@ -181,6 +191,13 @@ def is_approved(self) -> bool:
If claim is already has a resource hadle then it is considered approved."""
return self.has_resource_handle or self.approval_state == 'approved'

@property
def is_detached(self) -> bool:
"""Return whether this ResourceClaim has been detached from its ResourceHandle."""
if not self.status:
return False
return self.status.get('resourceHandle', {}).get('detached', False)

@property
def lifespan_end_datetime(self) -> Optional[datetime]:
"""Return datetime object representing when this ResourceClaim will be automatically deleted.
Expand Down Expand Up @@ -365,6 +382,53 @@ async def bind_resource_handle(self,

return resource_handle

def check_condition(self, when_condition, resource_handle, resource_provider):
# resource_provider may be None if there is no top-level provider.
resource_provider_vars = resource_provider.vars if resource_provider else {}
vars_ = {
**resource_provider_vars,
**resource_handle.vars,
"resource_claim": self,
"resource_handle": resource_handle,
"resource_provider": resource_provider,
"metadata": self.meta,
"spec": self.spec,
"status": self.status,
}
check_value = recursive_process_template_strings(
'{{(' + when_condition + ')|bool}}',
variables = vars_
)
return check_value

def check_auto_delete(self, logger, resource_handle, resource_provider) -> bool:
if not self.auto_delete_when:
return False
try:
check_value = self.check_condition(
resource_handle=resource_handle,
resource_provider=resource_provider,
when_condition=self.auto_delete_when
)
return check_value
except Exception as exception:
logger.warning(f"Auto delete check failed for {self}: {exception}")
return False

def check_auto_detach(self, logger, resource_handle, resource_provider):
if not self.auto_detach_when:
return False
try:
check_value = self.check_condition(
resource_handle=resource_handle,
resource_provider=resource_provider,
when_condition=self.auto_detach_when
)
return check_value
except Exception as exception:
logger.warning(f"Auto detach check failed for {self}: {exception}")
return False

def get_resource_state_from_status(self, resource_number):
if not self.status \
or not 'resources' in self.status \
Expand Down Expand Up @@ -522,6 +586,14 @@ async def delete(self):
version = Poolboy.operator_version,
)

async def detach(self, resource_handle):
await self.merge_patch_status({
"resourceHandle": {
"detached": True
}
})
await resource_handle.delete()

async def get_resource_handle(self):
return await resourcehandle.ResourceHandle.get(self.resource_handle_name)

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

if self.is_detached:
# Normally lifespan end is tracked by the ResourceHandle.
# Detached ResourceClaims have no handle.
if self.lifespan_end_datetime \
and self.lifespan_start_datetime < datetime.utcnow():
logger.info(f"Deleting detacthed {self} at end of lifespan")
await self.delete()
# No further processing for detached ResourceClaim
return

resource_provider = None
if self.has_resource_provider:
if self.has_spec_resources:
raise kopf.TemporaryError(
Expand All @@ -619,7 +702,7 @@ async def manage(self, logger) -> None:
)

try:
provider = await self.get_resource_provider()
resource_provider = await self.get_resource_provider()
except kubernetes_asyncio.client.exceptions.ApiException as e:
if e.status == 404:
raise kopf.TemporaryError(
Expand All @@ -629,11 +712,11 @@ async def manage(self, logger) -> None:
else:
raise

if provider.approval_required:
if resource_provider.approval_required:
if not 'approval' in self.status:
await self.merge_patch_status({
"approval": {
"message": provider.approval_pending_message,
"message": resource_provider.approval_pending_message,
"state": "pending"
}
})
Expand Down Expand Up @@ -683,6 +766,16 @@ async def manage(self, logger) -> None:
resource_claim_resources = resource_claim_resources,
)

if self.check_auto_delete(logger=logger, resource_handle=resource_handle, resource_provider=resource_provider):
logger.info(f"auto-delete of {self} triggered")
await self.delete()
return

if self.check_auto_detach(logger=logger, resource_handle=resource_handle, resource_provider=resource_provider):
logger.info(f"auto-detach of {self} triggered")
await self.detach(resource_handle=resource_handle)
return

await self.__manage_resource_handle(
logger = logger,
resource_claim_resources = resource_claim_resources,
Expand Down
16 changes: 4 additions & 12 deletions operator/resourcehandle.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,10 +723,6 @@ async def delete(self):
if e.status != 404:
raise

async def delete_resource_claim(self, logger: kopf.ObjectLogger) -> None:
if not self.is_bound:
return

async def get_resource_claim(self) -> Optional[ResourceClaimT]:
if not self.is_bound:
return None
Expand Down Expand Up @@ -807,14 +803,10 @@ async def handle_delete(self, logger: kopf.ObjectLogger) -> None:

if self.is_bound:
try:
await Poolboy.custom_objects_api.delete_namespaced_custom_object(
group = Poolboy.operator_domain,
name = self.resource_claim_name,
namespace = self.resource_claim_namespace,
plural = 'resourceclaims',
version = Poolboy.operator_version,
)
logger.info(f"Propagated delete of ResourceHandle {self.name} to ResourceClaim {self.resource_claim_name} in {self.resource_claim_namespace}")
resource_claim = await self.get_resource_claim()
if not resource_claim.is_detached:
await resource_claim.delete()
logger.info(f"Propagated delete of ResourceHandle {self.name} to ResourceClaim {self.resource_claim_name} in {self.resource_claim_namespace}")
except kubernetes_asyncio.client.exceptions.ApiException as e:
if e.status != 404:
raise
Expand Down
5 changes: 5 additions & 0 deletions operator/resourcewatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ async def __watch(self, method, **kwargs):
name = resource_claim_name,
namespace = resource_claim_namespace,
)

# Do not manage status for detached ResourceClaim
if resource_claim.is_detached:
continue

prev_state = resource_claim.status_resources[resource_index].get('state')
prev_description = (
f"{prev_state['apiVersion']} {prev_state['kind']} {resource_name} in {resource_namespace}"
Expand Down
Loading