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 feature to reset lifespan start from first ready #123

Merged
merged 2 commits into from
Jan 15, 2025
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
5 changes: 5 additions & 0 deletions helm/crds/resourceclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ spec:
Effective timestamp for end of lifespan for ResourceClaim.
type: string
format: date-time
firstReady:
description: >-
First timestamp when ResourceClaim indicated ready status.
type: string
format: date-time
maximum:
description: >-
Maximum lifespan which may be requested in the ResourceClaim relative to the creation timestamp.
Expand Down
5 changes: 5 additions & 0 deletions helm/templates/crds/resourceclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ spec:
Effective timestamp for end of lifespan for ResourceClaim.
type: string
format: date-time
firstReady:
description: >-
First timestamp when ResourceClaim indicated ready status.
type: string
format: date-time
maximum:
description: >-
Maximum lifespan which may be requested in the ResourceClaim relative to the creation timestamp.
Expand Down
18 changes: 18 additions & 0 deletions operator/poolboy_templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ def seconds_to_interval(seconds:int) -> str:
else:
return f"{int(seconds)}s"

def timedelta_to_str(td:timedelta) -> str:
total_seconds = int(td.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60

ret = ""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkupferer wdt?

total_seconds = int(td.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that looks better to me too 😄

if days > 0:
ret += f"{days}d"
if hours > 0:
ret += f"{hours:d}h"
if minutes > 0:
ret += f"{minutes}m"
if seconds > 0:
ret += f"{seconds}s"
return ret

jinja2envs = {
'jinja2': jinja2.Environment(
finalize = error_if_undefined,
Expand Down
51 changes: 51 additions & 0 deletions operator/resourceclaim.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ def lifespan_end_timestamp(self) -> Optional[str]:
if lifespan:
return lifespan.get('end')

@property
def lifespan_first_ready_datetime(self) -> Optional[datetime]:
timestamp = self.lifespan_first_ready_timestamp
if timestamp:
return datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S%z")

@property
def lifespan_first_ready_timestamp(self) -> Optional[str]:
timestamp = self.status.get('lifespan', {}).get('firstReady')
if timestamp:
return timestamp

@property
def lifespan_maximum(self) -> Optional[str]:
lifespan = self.status.get('lifespan')
Expand Down Expand Up @@ -340,6 +352,10 @@ async def bind_resource_handle(self,
"maximum": resource_handle.get_lifespan_maximum(resource_claim=self),
"relativeMaximum": resource_handle.get_lifespan_relative_maximum(resource_claim=self),
}

if resource_handle.is_ready:
lifespan_value['firstReady'] = datetime.now(timezone.utc).strftime('%FT%TZ')

status_patch.append({
"op": "add",
"path": "/status/lifespan",
Expand Down Expand Up @@ -405,13 +421,38 @@ def get_resource_state_from_status(self, resource_index):
return None
return self.status['resources'][resource_index].get('state')

async def set_requested_lifespan_end(self,
end_datetime,
) -> None:
await self.merge_patch({
"spec": {
"lifespan": {
"end": end_datetime.strftime('%FT%TZ')
}
}
})

async def update_status_from_handle(self,
logger: kopf.ObjectLogger,
resource_handle: ResourceHandleT
) -> None:
async with self.lock:
patch = []

# Reset lifespan from default on first ready
if resource_handle.is_ready and not self.lifespan_first_ready_timestamp:
logger.info(f"{self} is first ready")
lifespan_default_timedelta = resource_handle.get_lifespan_default_timedelta(resource_claim=self)
if lifespan_default_timedelta:
logger.info(f"{self} has lifespan_default_timedelta")
# Adjust requested end if unchanged from default
if not self.requested_lifespan_end_datetime \
or lifespan_default_timedelta == self.requested_lifespan_end_datetime - self.lifespan_start_datetime:
logger.info(f"Setting requested lifespan end")
await self.set_requested_lifespan_end(
datetime.now(timezone.utc) + lifespan_default_timedelta
)

lifespan_maximum = resource_handle.get_lifespan_maximum(resource_claim=self)
lifespan_relative_maximum = resource_handle.get_lifespan_relative_maximum(resource_claim=self)
if 'lifespan' in resource_handle.spec \
Expand All @@ -421,6 +462,8 @@ async def update_status_from_handle(self,
"maximum": lifespan_maximum,
"relativeMaximum": lifespan_relative_maximum,
}
if resource_handle.is_ready:
lifespan_value['firstReady'] = datetime.now(timezone.utc).strftime('%FT%TZ')
patch.append({
"op": "add",
"path": "/status/lifespan",
Expand All @@ -441,6 +484,14 @@ async def update_status_from_handle(self,
"value": resource_handle.lifespan_end_timestamp,
})

if resource_handle.is_ready and not self.lifespan_first_ready_timestamp:
# FIXME - Reset ResourceHandle lifespan default!
patch.append({
"op": "add",
"path": "/status/lifespan/firstReady",
"value": datetime.now(timezone.utc).strftime('%FT%TZ'),
})

if lifespan_maximum:
if lifespan_maximum != self.lifespan_maximum:
patch.append({
Expand Down
29 changes: 19 additions & 10 deletions operator/resourcehandle.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from kopfobject import KopfObject
from poolboy import Poolboy
from poolboy_templating import recursive_process_template_strings, seconds_to_interval
from poolboy_templating import recursive_process_template_strings, seconds_to_interval, timedelta_to_str

ResourceClaimT = TypeVar('ResourceClaimT', bound='ResourceClaim')
ResourceHandleT = TypeVar('ResourceHandleT', bound='ResourceHandle')
Expand Down Expand Up @@ -366,19 +366,17 @@ async def create_for_claim(cls,
)
lifespan_end = lifespan_start_datetime + lifespan_maximum_timedelta

if lifespan_end_datetime \
or lifespan_maximum_timedelta \
or lifespan_relative_maximum_timedelta:
definition['spec']['lifespan'] = {}
if lifespan_default_timedelta:
definition['spec'].setdefault('lifespan', {})['default'] = timedelta_to_str(lifespan_default_timedelta)

if lifespan_end_datetime:
definition['spec']['lifespan']['end'] = lifespan_end_datetime.strftime('%FT%TZ')
definition['spec'].setdefault('lifespan', {})['end'] = lifespan_end_datetime.strftime('%FT%TZ')

if lifespan_maximum:
definition['spec']['lifespan']['maximum'] = lifespan_maximum
definition['spec'].setdefault('lifespan', {})['maximum'] = lifespan_maximum

if lifespan_relative_maximum:
definition['spec']['lifespan']['relativeMaximum'] = lifespan_relative_maximum
definition['spec'].setdefault('lifespan', {})['relativeMaximum'] = lifespan_relative_maximum

definition = await Poolboy.custom_objects_api.create_namespaced_custom_object(
body = definition,
Expand Down Expand Up @@ -785,10 +783,21 @@ def get_lifespan_relative_maximum_timedelta(self, resource_claim=None):

def get_lifespan_end_maximum_datetime(self, resource_claim=None):
lifespan_start_datetime = resource_claim.lifespan_start_datetime if resource_claim else self.creation_datetime

maximum_timedelta = self.get_lifespan_maximum_timedelta(resource_claim=resource_claim)
maximum_end = lifespan_start_datetime + maximum_timedelta if maximum_timedelta else None
if maximum_timedelta:
if resource_claim.lifespan_first_ready_timestamp:
maximum_end = resource_claim.lifespan_first_ready_datetime + maximum_timedelta
else:
maximum_end = lifespan_start_datetime + maximum_timedelta
else:
maximum_end = None

relative_maximum_timedelta = self.get_lifespan_relative_maximum_timedelta(resource_claim=resource_claim)
relative_maximum_end = datetime.now(timezone.utc) + relative_maximum_timedelta if relative_maximum_timedelta else None
if relative_maximum_timedelta:
relative_maximum_end = datetime.now(timezone.utc) + relative_maximum_timedelta
else:
relative_maximum_end = None

if relative_maximum_end \
and (not maximum_end or relative_maximum_end < maximum_end):
Expand Down
4 changes: 4 additions & 0 deletions test/roles/poolboy_test_simple/tasks/test-pool-01.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
{{ r_get_resource_handles.resources | json_query('[?spec.resourceClaim==null]') }}
failed_when: >-
__unbound_handles | length != 2
retries: 5
delay: 1

- name: Create ResourceClaim test-pool-01
kubernetes.core.k8s:
Expand Down Expand Up @@ -131,6 +133,8 @@
{{ r_get_resource_handles.resources | json_query('[?spec.resourceClaim==null]') }}
failed_when: >-
__unbound_handles | length != 2
delay: 1
retries: 5

- name: Delete ResourcePool test-pool-01
kubernetes.core.k8s:
Expand Down
137 changes: 137 additions & 0 deletions test/roles/poolboy_test_simple/tasks/test-ready-01.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
- name: Create ResourceProvider test-ready-01
kubernetes.core.k8s:
definition:
apiVersion: "{{ poolboy_domain }}/v1"
kind: ResourceProvider
metadata:
name: test-ready-01
namespace: "{{ poolboy_namespace }}"
labels: >-
{{ {
poolboy_domain ~ "/test": "simple"
} }}
spec:
healthCheck: >-
spec.numbervalue >= 0
lifespan:
default: 1d
maximum: 7d
relativeMaximum: 3d
override:
apiVersion: "{{ poolboy_domain }}/v1"
kind: ResourceClaimTest
metadata:
name: "test-ready-01-{% raw %}{{ guid }}{% endraw %}"
namespace: "{{ poolboy_test_namespace }}"
parameters:
- name: numbervar
allowUpdate: true
validation:
openAPIV3Schema:
type: integer
default: 0
minimum: 0
readinessCheck: >-
spec.numbervalue > 0
template:
definition:
spec:
numbervalue: "{% raw %}{{ numbervar | int }}{% endraw %}"
enable: true
updateFilters:
- pathMatch: /spec/.*
allowedOps:
- replace

- name: Create ResourceClaim test-ready-01
kubernetes.core.k8s:
definition:
apiVersion: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
metadata:
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
labels: >-
{{ {
poolboy_domain ~ "/test": "simple"
} }}
spec:
provider:
name: test-ready-01
parameterValues:
numbervar: 0

- name: Verify handling of ResourceClaim test-ready-01
kubernetes.core.k8s_info:
api_version: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
register: r_get_resource_claim
failed_when: >-
r_get_resource_claim.resources[0].status.ready != false or
r_get_resource_claim.resources[0].status.lifespan.firstReady is defined or
(
r_get_resource_claim.resources[0].status.lifespan.end | to_datetime("%Y-%m-%dT%H:%M:%SZ") -
r_get_resource_claim.resources[0].status.lifespan.start | to_datetime("%Y-%m-%dT%H:%M:%SZ")
).total_seconds() != 24 * 60 * 60
until: r_get_resource_claim is success
delay: 1
retries: 10

- name: Update ResourceClaim test-ready-01 to become ready
kubernetes.core.k8s:
api_version: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
state: patched
definition:
spec:
provider:
parameterValues:
numbervar: 1

- name: Pause to ensure timestamp change
pause:
seconds: 1

- name: Verify update handling of ResourceClaim test-ready-01
kubernetes.core.k8s_info:
api_version: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
register: r_get_resource_claim
failed_when: >-
r_get_resource_claim.resources[0].status.ready != true or
r_get_resource_claim.resources[0].status.lifespan.firstReady is undefined or
(
r_get_resource_claim.resources[0].status.lifespan.end | to_datetime("%Y-%m-%dT%H:%M:%SZ") -
r_get_resource_claim.resources[0].status.lifespan.firstReady | to_datetime("%Y-%m-%dT%H:%M:%SZ")
).total_seconds() != 24 * 60 * 60
until: r_get_resource_claim is success
delay: 1
retries: 10

- name: Delete ResourceClaim test-ready-01
kubernetes.core.k8s:
api_version: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
state: absent

- name: Verify delete of ResourceClaim test-ready-01
kubernetes.core.k8s_info:
api_version: "{{ poolboy_domain }}/v1"
kind: ResourceClaim
name: test-ready-01
namespace: "{{ poolboy_test_namespace }}"
register: r_get_resource_claim
failed_when: r_get_resource_claim.resources | length != 0
until: r_get_resource_claim is success
retries: 5
delay: 1
...
1 change: 1 addition & 0 deletions test/roles/poolboy_test_simple/tasks/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- test-pool-02.yaml
- test-pool-03.yaml
- test-pool-04.yaml
- test-ready-01.yaml
- test-recreate-01.yaml
- test-vars-01.yaml
- test-vars-02.yaml
Expand Down
Loading