From c352204d5bd8660738946d822160f0f5b857e3a0 Mon Sep 17 00:00:00 2001 From: Johnathan Kupferer Date: Tue, 3 Feb 2026 18:25:03 -0500 Subject: [PATCH 1/4] Update build and odo config --- .odoignore | 3 ++ .s2i/bin/assemble | 10 +++++++ Containerfile | 4 +-- devfile.yaml | 3 +- ...disable-creation-resource-provider.yaml.j2 | 29 ------------------- 5 files changed, 17 insertions(+), 32 deletions(-) create mode 100644 .odoignore create mode 100755 .s2i/bin/assemble delete mode 100644 test/roles/poolboy_test_simple/templates/test-disable-creation-resource-provider.yaml.j2 diff --git a/.odoignore b/.odoignore new file mode 100644 index 0000000..8c60b53 --- /dev/null +++ b/.odoignore @@ -0,0 +1,3 @@ +Containerfile +helm +test diff --git a/.s2i/bin/assemble b/.s2i/bin/assemble new file mode 100755 index 0000000..4d07f36 --- /dev/null +++ b/.s2i/bin/assemble @@ -0,0 +1,10 @@ +#!/bin/bash + +set -x +set -eo pipefail +shopt -s dotglob + +# Install customized kopf version with label selectors and deprecated finalizer support. +pip install git+https://github.com/rhpds/kopf.git@add-deprecated-finalizer-support + +/usr/libexec/s2i/assemble diff --git a/Containerfile b/Containerfile index 1ecad06..b418ccd 100644 --- a/Containerfile +++ b/Containerfile @@ -8,10 +8,10 @@ RUN rm -rf /tmp/src/.git* && \ chown -R 1001 /tmp/src && \ chgrp -R 0 /tmp/src && \ chmod -R g+w /tmp/src && \ - pip install git+https://github.com/rhpds/kopf.git@add-deprecated-finalizer-support + cp -rp /tmp/src/.s2i/bin /tmp/scripts USER 1001 -RUN /usr/libexec/s2i/assemble +RUN /tmp/scripts/assemble CMD ["/usr/libexec/s2i/run"] diff --git a/devfile.yaml b/devfile.yaml index 9ad1068..4d6cd16 100644 --- a/devfile.yaml +++ b/devfile.yaml @@ -1,6 +1,7 @@ commands: - exec: - commandLine: /usr/libexec/s2i/assemble + commandLine: >- + rm -rf /tmp/src && cp /tmp/projects -r /tmp/src && /tmp/src/.s2i/bin/assemble component: s2i-builder group: isDefault: true diff --git a/test/roles/poolboy_test_simple/templates/test-disable-creation-resource-provider.yaml.j2 b/test/roles/poolboy_test_simple/templates/test-disable-creation-resource-provider.yaml.j2 deleted file mode 100644 index e0c624e..0000000 --- a/test/roles/poolboy_test_simple/templates/test-disable-creation-resource-provider.yaml.j2 +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: {{ poolboy_domain }}/v1 -kind: ResourceProvider -metadata: - name: test-disable-creation - namespace: {{ poolboy_namespace }} -spec: - disableCreation: true - override: - apiVersion: {{ poolboy_domain }}/v1 - kind: ResourceClaimTest - metadata: - generateName: test-disable-creation- - namespace: {{ poolboy_test_namespace }} - validation: - openAPIV3Schema: - type: object - required: - - spec - additionalProperties: false - properties: - spec: - type: object - required: - - testvalue - additionalProperties: false - properties: - testvalue: - type: string From add4f91b0fc961cfa073620ca31464e63360c14a Mon Sep 17 00:00:00 2001 From: Johnathan Kupferer Date: Tue, 3 Feb 2026 18:26:58 -0500 Subject: [PATCH 2/4] Fix resource init bug --- operator/resourceclaim.py | 4 ++-- operator/resourcehandle.py | 12 ++++++++++++ .../poolboy_test_simple/tasks/test-pool-02.yaml | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/operator/resourceclaim.py b/operator/resourceclaim.py index e92d6d7..2317069 100644 --- a/operator/resourceclaim.py +++ b/operator/resourceclaim.py @@ -377,7 +377,7 @@ async def assign_resource_handler(self): async def bind_resource_handle(self, logger: kopf.ObjectLogger, resource_claim_resources: List[Mapping], - ): + ) -> ResourceHandleT|None: resource_handle = await resourcehandle.ResourceHandle.bind_handle_to_claim( logger = logger, resource_claim = self, @@ -988,7 +988,7 @@ async def refetch(self) -> ResourceClaimT|None: return self except kubernetes_asyncio.client.exceptions.ApiException as e: if e.status == 404: - self.unregister(name=self.name, namespace=self.namespace) + await self.unregister(name=self.name, namespace=self.namespace) return None raise diff --git a/operator/resourcehandle.py b/operator/resourcehandle.py index e0e5974..5737804 100644 --- a/operator/resourcehandle.py +++ b/operator/resourcehandle.py @@ -185,6 +185,14 @@ async def bind_handle_to_claim( matched_resource_handle = match.resource_handle patch = [ { + "op": "add", + "path": f"/metadata/labels/{Poolboy.resource_claim_name_label.replace('/', '~1')}", + "value": resource_claim.name, + }, { + "op": "add", + "path": f"/metadata/labels/{Poolboy.resource_claim_namespace_label.replace('/', '~1')}", + "value": resource_claim.namespace, + }, { "op": "add", "path": "/spec/resourceClaim", "value": { @@ -904,6 +912,9 @@ async def __manage_init_status_resources(self, entry['name'] = resource['name'] set_resources.append(entry) + if self.status_resources == set_resources: + return + patch = [] if not self.status: patch.extend(({ @@ -943,6 +954,7 @@ async def __manage_init_status_resources(self, if attempt > 2: logger.exception(f"{self} failed status patch: {patch}") raise + await self.refresh() attempt += 1 async def __manage_check_delete(self, diff --git a/test/roles/poolboy_test_simple/tasks/test-pool-02.yaml b/test/roles/poolboy_test_simple/tasks/test-pool-02.yaml index c89dda1..6ed16b0 100644 --- a/test/roles/poolboy_test_simple/tasks/test-pool-02.yaml +++ b/test/roles/poolboy_test_simple/tasks/test-pool-02.yaml @@ -23,6 +23,9 @@ enable: true validation: openAPIV3Schema: + type: object + required: + - spec additionalProperties: false properties: spec: From 2e04b34a097ea950145704c81a8aa563660a5748 Mon Sep 17 00:00:00 2001 From: Johnathan Kupferer Date: Tue, 3 Feb 2026 18:27:30 -0500 Subject: [PATCH 3/4] Add disableCreation feature to ResourceClaims --- helm/crds/resourceclaims.yaml | 5 + helm/templates/crds/resourceclaims.yaml | 5 + operator/resourceclaim.py | 24 +++- .../tasks/test-disable-creation-01.yaml | 130 ++++++++++++++++++ .../tasks/test-disable-creation-02.yaml | 130 ++++++++++++++++++ .../roles/poolboy_test_simple/tasks/test.yaml | 89 +----------- 6 files changed, 289 insertions(+), 94 deletions(-) create mode 100644 test/roles/poolboy_test_simple/tasks/test-disable-creation-01.yaml create mode 100644 test/roles/poolboy_test_simple/tasks/test-disable-creation-02.yaml diff --git a/helm/crds/resourceclaims.yaml b/helm/crds/resourceclaims.yaml index 6aa5b16..fd76258 100644 --- a/helm/crds/resourceclaims.yaml +++ b/helm/crds/resourceclaims.yaml @@ -79,6 +79,11 @@ spec: Condition to check which triggers detach of ResourceHandle from the ResourceClaim. Condition is given in Jinja2 syntax similar to ansible "when" clauses. type: string + disableCreation: + description: >- + If set to true, then ResourceHandle creation is disabled for this ResourceClaim + and it will only match to existing ResourceHandles such as are created by ResourcePools. + type: boolean lifespan: description: >- Lifespan configuration for the ResourceClaim. diff --git a/helm/templates/crds/resourceclaims.yaml b/helm/templates/crds/resourceclaims.yaml index db2f992..9574fd2 100644 --- a/helm/templates/crds/resourceclaims.yaml +++ b/helm/templates/crds/resourceclaims.yaml @@ -80,6 +80,11 @@ spec: Condition to check which triggers detach of ResourceHandle from the ResourceClaim. Condition is given in Jinja2 syntax similar to ansible "when" clauses. type: string + disableCreation: + description: >- + If set to true, then ResourceHandle creation is disabled for this ResourceClaim + and it will only match to existing ResourceHandles such as are created by ResourcePools. + type: boolean lifespan: description: >- Lifespan configuration for the ResourceClaim. diff --git a/operator/resourceclaim.py b/operator/resourceclaim.py index 2317069..6c37ccc 100644 --- a/operator/resourceclaim.py +++ b/operator/resourceclaim.py @@ -162,6 +162,10 @@ def auto_detach_when(self) -> str|None: def claim_is_initialized(self) -> bool: return f"{Poolboy.operator_domain}/resource-claim-init-timestamp" in self.annotations + @property + def create_disabled(self) -> bool: + return self.spec.get('disableCreation', False) + @property def has_resource_handle(self) -> bool: """Return whether this ResourceClaim is bound to a ResourceHandle.""" @@ -385,13 +389,18 @@ async def bind_resource_handle(self, ) if not resource_handle: + create_disabled = self.create_disabled for provider in await self.get_resource_providers(resource_claim_resources): if provider.create_disabled: - raise kopf.TemporaryError( - f"Found no matching ResourceHandles for {self} and " - f"ResourceHandle creation is disabled for {provider}", - delay=600 - ) + create_disabled = True + if create_disabled: + logger.info( + "Found no matching ResourceHandles for %s and " + "ResourceHandle creation is disabled for %s", + self, provider + ) + return None + resource_handle = await resourcehandle.ResourceHandle.create_for_claim( logger = logger, resource_claim = self, @@ -865,12 +874,13 @@ async def manage(self, logger) -> None: else: resource_claim_resources = self.resources - if not resource_handle: + if resource_handle is None: resource_handle = await self.bind_resource_handle( logger = logger, resource_claim_resources = resource_claim_resources, ) - if resource_handle.ignore: + + if resource_handle is None or resource_handle.ignore: return if self.check_auto_delete(logger=logger, resource_handle=resource_handle, resource_provider=resource_provider): diff --git a/test/roles/poolboy_test_simple/tasks/test-disable-creation-01.yaml b/test/roles/poolboy_test_simple/tasks/test-disable-creation-01.yaml new file mode 100644 index 0000000..8f5b074 --- /dev/null +++ b/test/roles/poolboy_test_simple/tasks/test-disable-creation-01.yaml @@ -0,0 +1,130 @@ +--- +# Test ResourceProvider with disableCreation set +- name: Create ResourceProvider test-disable-creation-01 + kubernetes.core.k8s: + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + metadata: + name: test-disable-creation-01 + namespace: "{{ poolboy_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + disableCreation: true + override: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceClaimTest + metadata: + name: "test-pool-02-{% raw %}{{ guid }}{% endraw %}" + namespace: "{{ poolboy_test_namespace }}" + template: + enable: true + validation: + openAPIV3Schema: + additionalProperties: false + properties: + spec: + additionalProperties: false + properties: + value: + type: string + required: + - value + type: object + required: + - spec + type: object + +- name: Create ResourceClaim for test-disable-creation-01 + kubernetes.core.k8s: + state: present + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + metadata: + name: test-disable-creation-01 + namespace: "{{ poolboy_test_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + resources: + - provider: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + name: test-disable-creation-01 + namespace: "{{ poolboy_namespace }}" + template: + spec: + value: "foo" + +- name: Pause to avoid testing before poolboy could respond. + pause: + seconds: 1 + +- name: Verify ResourceClaim test-disable-creation-01 unbound + kubernetes.core.k8s_info: + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-01 + namespace: "{{ poolboy_test_namespace }}" + register: r_get_resource_claim + vars: + __claim: "{{ r_get_resource_claim.resources[0] | default({}) }}" + failed_when: >- + "provider" not in __claim.status.resources[0] | default({}) or + "resourceHandle" in __claim.status | default({}) + until: r_get_resource_claim is success + delay: 2 + retries: 5 + +- name: Create ResourceHandle for test-disable-creation-01 + kubernetes.core.k8s: + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceHandle + metadata: + name: guid-tdc01 + namespace: "{{ poolboy_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + resources: + - provider: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + name: test-disable-creation-01 + namespace: "{{ poolboy_namespace }}" + template: + spec: + value: "foo" + +- name: Verify ResourceClaim test-disable-creation-01 binds + kubernetes.core.k8s_info: + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-01 + namespace: "{{ poolboy_test_namespace }}" + register: r_get_resource_claim + vars: + __claim: "{{ r_get_resource_claim.resources[0] | default({}) }}" + failed_when: >- + "resourceHandle" not in __claim.status + until: r_get_resource_claim is success + delay: 1 + retries: 30 + +- name: Delete ResourceClaim test-disable-creation-01 + kubernetes.core.k8s: + state: absent + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-01 + namespace: "{{ poolboy_test_namespace }}" +... diff --git a/test/roles/poolboy_test_simple/tasks/test-disable-creation-02.yaml b/test/roles/poolboy_test_simple/tasks/test-disable-creation-02.yaml new file mode 100644 index 0000000..eef6e74 --- /dev/null +++ b/test/roles/poolboy_test_simple/tasks/test-disable-creation-02.yaml @@ -0,0 +1,130 @@ +--- +# Test ResourceProvider with disableCreation set +- name: Create ResourceProvider test-disable-creation-02 + kubernetes.core.k8s: + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + metadata: + name: test-disable-creation-02 + namespace: "{{ poolboy_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + override: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceClaimTest + metadata: + name: "test-pool-02-{% raw %}{{ guid }}{% endraw %}" + namespace: "{{ poolboy_test_namespace }}" + template: + enable: true + validation: + openAPIV3Schema: + additionalProperties: false + properties: + spec: + additionalProperties: false + properties: + value: + type: string + required: + - value + type: object + required: + - spec + type: object + +- name: Create ResourceClaim for test-disable-creation-02 + kubernetes.core.k8s: + state: present + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + metadata: + name: test-disable-creation-02 + namespace: "{{ poolboy_test_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + disableCreation: true + resources: + - provider: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + name: test-disable-creation-02 + namespace: "{{ poolboy_namespace }}" + template: + spec: + value: "foo" + +- name: Pause to avoid testing before poolboy could respond. + pause: + seconds: 1 + +- name: Verify ResourceClaim test-disable-creation-02 unbound + kubernetes.core.k8s_info: + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-02 + namespace: "{{ poolboy_test_namespace }}" + register: r_get_resource_claim + vars: + __claim: "{{ r_get_resource_claim.resources[0] | default({}) }}" + failed_when: >- + "provider" not in __claim.status.resources[0] | default({}) or + "resourceHandle" in __claim.status | default({}) + until: r_get_resource_claim is success + delay: 2 + retries: 5 + +- name: Create ResourceHandle for test-disable-creation-02 + kubernetes.core.k8s: + definition: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceHandle + metadata: + name: guid-tdc02 + namespace: "{{ poolboy_namespace }}" + labels: >- + {{ { + poolboy_domain ~ "/test": "simple" + } }} + spec: + resources: + - provider: + apiVersion: "{{ poolboy_domain }}/v1" + kind: ResourceProvider + name: test-disable-creation-02 + namespace: "{{ poolboy_namespace }}" + template: + spec: + value: "foo" + +- name: Verify ResourceClaim test-disable-creation-02 binds + kubernetes.core.k8s_info: + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-02 + namespace: "{{ poolboy_test_namespace }}" + register: r_get_resource_claim + vars: + __claim: "{{ r_get_resource_claim.resources[0] | default({}) }}" + failed_when: >- + "resourceHandle" not in __claim.status + until: r_get_resource_claim is success + delay: 1 + retries: 30 + +- name: Delete ResourceClaim test-disable-creation-02 + kubernetes.core.k8s: + state: absent + api_version: "{{ poolboy_domain }}/v1" + kind: ResourceClaim + name: test-disable-creation-02 + namespace: "{{ poolboy_test_namespace }}" +... diff --git a/test/roles/poolboy_test_simple/tasks/test.yaml b/test/roles/poolboy_test_simple/tasks/test.yaml index c07367f..c5e11be 100644 --- a/test/roles/poolboy_test_simple/tasks/test.yaml +++ b/test/roles/poolboy_test_simple/tasks/test.yaml @@ -6,6 +6,8 @@ - test-01.yaml - test-02.yaml - test-approval-01.yaml + - test-disable-creation-01.yaml + - test-disable-creation-02.yaml - test-ignore-01.yaml - test-finalizers-01.yaml - test-lifespan-start-01.yaml @@ -990,93 +992,6 @@ delay: 5 retries: 10 -- name: Create test-disable-creation ResourceProvider - kubernetes.core.k8s: - state: present - definition: "{{ lookup('template', 'test-disable-creation-resource-provider.yaml.j2') | from_yaml }}" - -- name: Create ResourceClaim for test-disable-creation - kubernetes.core.k8s: - state: present - definition: "{{ resource_definition | from_yaml }}" - vars: - resource_definition: | - apiVersion: {{ poolboy_domain }}/v1 - kind: ResourceClaim - metadata: - name: test-disable-creation - namespace: {{ poolboy_test_namespace }} - labels: - {{ poolboy_domain }}/test: simple - spec: - resources: - - provider: - apiVersion: {{ poolboy_domain }}/v1 - kind: ResourceProvider - name: test-disable-creation - namespace: {{ poolboy_namespace }} - template: - spec: - testvalue: "foo" - -- name: Verify ResourceClaim test-disable-creation unbound - kubernetes.core.k8s_info: - api_version: "{{ poolboy_domain }}/v1" - kind: ResourceClaim - name: test-disable-creation - namespace: "{{ poolboy_test_namespace }}" - register: r_get_disable_creation - vars: - __claim: "{{ r_get_disable_creation.resources[0] | default({}) }}" - failed_when: >- - "provider" not in __claim.status.resources[0] | default({}) or - "resourceHandle" in __claim.status | default({}) - until: r_get_disable_creation is success - delay: 2 - retries: 5 - -- name: Create ResourceHandle for test-disable-creation - kubernetes.core.k8s: - definition: - apiVersion: "{{ poolboy_domain }}/v1" - kind: ResourceHandle - metadata: - name: guid-test-disable-creation - namespace: "{{ poolboy_namespace }}" - spec: - resources: - - provider: - apiVersion: "{{ poolboy_domain }}/v1" - kind: ResourceProvider - name: test-disable-creation - namespace: "{{ poolboy_namespace }}" - template: - spec: - testvalue: "foo" - -- name: Verify ResourceClaim test-disable-creation binds - kubernetes.core.k8s_info: - api_version: "{{ poolboy_domain }}/v1" - kind: ResourceClaim - name: test-disable-creation - namespace: "{{ poolboy_test_namespace }}" - register: r_get_disable_creation - vars: - __claim: "{{ r_get_disable_creation.resources[0] | default({}) }}" - failed_when: >- - "resourceHandle" not in __claim.status - until: r_get_disable_creation is success - delay: 3 - retries: 30 - -- name: Delete ResourceClaim test-disable-creation - kubernetes.core.k8s: - state: absent - api_version: "{{ poolboy_domain }}/v1" - kind: ResourceClaim - name: test-disable-creation - namespace: "{{ poolboy_test_namespace }}" - - name: Create test-base ResourceProvider kubernetes.core.k8s: definition: "{{ lookup('template', 'test-base-resource-provider.yaml.j2') | from_yaml }}" From 7a334175b2d30707917c25bb047965c61090aef9 Mon Sep 17 00:00:00 2001 From: Johnathan Kupferer Date: Tue, 3 Feb 2026 22:15:33 -0500 Subject: [PATCH 4/4] Remove preference of match to unknown readiness state - Preferring unknown readiness adds a race condition where a new ResourceHandle is preferred simply because readiness has not been tested. --- operator/resourcehandle.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/operator/resourcehandle.py b/operator/resourcehandle.py index 5737804..2603bee 100644 --- a/operator/resourcehandle.py +++ b/operator/resourcehandle.py @@ -52,6 +52,7 @@ def __lt__(self, cmp): return False # Prefer healthy resources to unknown health state + # Unhealthy resource handles would not be considered for a potential match. if self.resource_handle.is_healthy and cmp.resource_handle.is_healthy is None: return True if self.resource_handle.is_healthy is None and cmp.resource_handle.is_healthy: @@ -63,12 +64,6 @@ def __lt__(self, cmp): if not self.resource_handle.is_ready and cmp.resource_handle.is_ready: return False - # Prefer unknown readiness state to known unready state - if self.resource_handle.is_ready is None and cmp.resource_handle.is_ready is False: - return True - if not self.resource_handle.is_ready is False and cmp.resource_handle.is_ready is None: - return False - # Prefer older matches return self.resource_handle.creation_timestamp < cmp.resource_handle.creation_timestamp @@ -175,7 +170,7 @@ async def bind_handle_to_claim( # Match with (possibly empty) difference list match.template_difference_count += len(diff_patch) - if match: + if match is not None: matches.append(match) # Bind the oldest ResourceHandle with the smallest difference score