From e9b0fd04a0825da1d4b61ca14eeb4715c75805b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:31:12 +0100 Subject: [PATCH 1/9] chore(deps): bump com.diffplug.spotless:spotless-maven-plugin (#3242) Bumps [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/lib/3.3.0...maven/3.4.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- operator-framework-bom/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 5ca3705bc1..febe75f0fe 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -54,7 +54,7 @@ 3.2.8 3.4.0 3.12.0 - 3.3.0 + 3.4.0 0.10.0 diff --git a/pom.xml b/pom.xml index d1562cb1a0..163224549c 100644 --- a/pom.xml +++ b/pom.xml @@ -102,7 +102,7 @@ 3.1.4 9.0.2 3.5.1 - 3.3.0 + 3.4.0 From e808d1f903bcd2b67722d2c5574713fb543c0423 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:31:40 +0100 Subject: [PATCH 2/9] chore(deps): bump openrewrite.version from 8.46.1 to 8.75.9 (#3241) Bumps `openrewrite.version` from 8.46.1 to 8.75.9. Updates `org.openrewrite:rewrite-java` from 8.46.1 to 8.75.9 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.46.1...v8.75.9) Updates `org.openrewrite:rewrite-maven` from 8.46.1 to 8.75.9 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.46.1...v8.75.9) Updates `org.openrewrite:rewrite-test` from 8.46.1 to 8.75.9 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.46.1...v8.75.9) Updates `org.openrewrite:rewrite-java-17` from 8.46.1 to 8.75.9 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.46.1...v8.75.9) --- updated-dependencies: - dependency-name: org.openrewrite:rewrite-java dependency-version: 8.75.9 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-maven dependency-version: 8.75.9 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-test dependency-version: 8.75.9 dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-java-17 dependency-version: 8.75.9 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- migration/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/pom.xml b/migration/pom.xml index 1976274023..e05e6bd237 100644 --- a/migration/pom.xml +++ b/migration/pom.xml @@ -29,7 +29,7 @@ OpenRewrite migration recipes for Java Operator SDK - 8.46.1 + 8.75.9 From b308d3ca88023c9fc7dbbf0c2a95ae8a0130e279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 19 Mar 2026 09:58:48 +0100 Subject: [PATCH 3/9] improve: don't run sonar on dependabit PRs (#3243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .github/workflows/sonar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 3d4d2032f6..95419a1a1a 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -21,7 +21,7 @@ on: jobs: test: runs-on: ubuntu-latest - if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'operator-framework' ) }} + if: ${{ github.actor != 'dependabot[bot]' && (( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'operator-framework' )) }} steps: - uses: actions/checkout@v6 - name: Set up Java and Maven From 1a07a0455a382ce947d519abe8dcbc1037d073bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 19 Mar 2026 14:34:43 +0100 Subject: [PATCH 4/9] improve: metrics processing e2e test stability (#3244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- observability/install-observability.sh | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/observability/install-observability.sh b/observability/install-observability.sh index f9aacf85e7..dd57e7b352 100755 --- a/observability/install-observability.sh +++ b/observability/install-observability.sh @@ -92,6 +92,11 @@ else fi echo -e "${GREEN}✓ OpenTelemetry Operator installation started${NC}" +# Wait for OpenTelemetry Operator to be ready (webhook must be serving before we create CRs) +echo -e "${YELLOW}Waiting for OpenTelemetry Operator deployment to be ready...${NC}" +kubectl wait --for=condition=Available deployment/opentelemetry-operator -n observability --timeout=120s +echo -e "${GREEN}✓ OpenTelemetry Operator deployment is ready${NC}" + # Install kube-prometheus-stack (includes Prometheus + Grafana) echo -e "\n${YELLOW}Installing Prometheus and Grafana stack...${NC}" if helm list -n observability | grep -q kube-prometheus-stack; then @@ -110,9 +115,9 @@ else fi echo -e "${GREEN}✓ Prometheus and Grafana installation started${NC}" -# Create OpenTelemetry Collector instance +# Create OpenTelemetry Collector instance (retry until webhook is fully serving) echo -e "\n${YELLOW}Creating OpenTelemetry Collector...${NC}" -cat </dev/null; then + echo -e "${GREEN}✓ OpenTelemetry Collector created${NC}" + break + fi + if [ "$i" -eq 20 ]; then + echo -e "${RED}✗ Failed to create OpenTelemetry Collector after 20 attempts${NC}" + exit 1 + fi + echo -e "${YELLOW} Webhook not ready yet, retrying ($i/20)...${NC}" + sleep 5 +done # Create ServiceMonitor for OpenTelemetry Collector echo -e "\n${YELLOW}Creating ServiceMonitor for OpenTelemetry...${NC}" From 5a5be97448d75ed1ec15012bb08861e13e9ed61a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:13:56 +0100 Subject: [PATCH 5/9] chore(deps): bump openrewrite.version from 8.75.9 to 8.75.10 (#3247) Bumps `openrewrite.version` from 8.75.9 to 8.75.10. Updates `org.openrewrite:rewrite-java` from 8.75.9 to 8.75.10 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.9...v8.75.10) Updates `org.openrewrite:rewrite-maven` from 8.75.9 to 8.75.10 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.9...v8.75.10) Updates `org.openrewrite:rewrite-test` from 8.75.9 to 8.75.10 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.9...v8.75.10) Updates `org.openrewrite:rewrite-java-17` from 8.75.9 to 8.75.10 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.9...v8.75.10) --- updated-dependencies: - dependency-name: org.openrewrite:rewrite-java dependency-version: 8.75.10 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-maven dependency-version: 8.75.10 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-test dependency-version: 8.75.10 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-java-17 dependency-version: 8.75.10 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- migration/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/pom.xml b/migration/pom.xml index e05e6bd237..b57fdbdbd0 100644 --- a/migration/pom.xml +++ b/migration/pom.xml @@ -29,7 +29,7 @@ OpenRewrite migration recipes for Java Operator SDK - 8.75.9 + 8.75.10 From d402d7106704003c54dd51c4e4ce5b1a1ebf7441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:10:11 +0100 Subject: [PATCH 6/9] chore(deps): bump openrewrite.version from 8.75.10 to 8.76.0 (#3248) Bumps `openrewrite.version` from 8.75.10 to 8.76.0. Updates `org.openrewrite:rewrite-java` from 8.75.10 to 8.76.0 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.10...v8.76.0) Updates `org.openrewrite:rewrite-maven` from 8.75.10 to 8.76.0 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.10...v8.76.0) Updates `org.openrewrite:rewrite-test` from 8.75.10 to 8.76.0 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.10...v8.76.0) Updates `org.openrewrite:rewrite-java-17` from 8.75.10 to 8.76.0 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.75.10...v8.76.0) --- updated-dependencies: - dependency-name: org.openrewrite:rewrite-java dependency-version: 8.76.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-maven dependency-version: 8.76.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-test dependency-version: 8.76.0 dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: org.openrewrite:rewrite-java-17 dependency-version: 8.76.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- migration/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/pom.xml b/migration/pom.xml index b57fdbdbd0..29725e3934 100644 --- a/migration/pom.xml +++ b/migration/pom.xml @@ -29,7 +29,7 @@ OpenRewrite migration recipes for Java Operator SDK - 8.75.10 + 8.76.0 From 09ba1b91e8197bd269fbaa0300790013e54b514a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:56:10 +0100 Subject: [PATCH 7/9] chore(deps): bump openrewrite.version from 8.76.0 to 8.76.4 (#3251) Bumps `openrewrite.version` from 8.76.0 to 8.76.4. Updates `org.openrewrite:rewrite-java` from 8.76.0 to 8.76.4 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.76.0...v8.76.4) Updates `org.openrewrite:rewrite-maven` from 8.76.0 to 8.76.4 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.76.0...v8.76.4) Updates `org.openrewrite:rewrite-test` from 8.76.0 to 8.76.4 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.76.0...v8.76.4) Updates `org.openrewrite:rewrite-java-17` from 8.76.0 to 8.76.4 - [Release notes](https://github.com/openrewrite/rewrite/releases) - [Commits](https://github.com/openrewrite/rewrite/compare/v8.76.0...v8.76.4) --- updated-dependencies: - dependency-name: org.openrewrite:rewrite-java dependency-version: 8.76.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-maven dependency-version: 8.76.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-test dependency-version: 8.76.4 dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: org.openrewrite:rewrite-java-17 dependency-version: 8.76.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- migration/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/pom.xml b/migration/pom.xml index 29725e3934..14365092ad 100644 --- a/migration/pom.xml +++ b/migration/pom.xml @@ -29,7 +29,7 @@ OpenRewrite migration recipes for Java Operator SDK - 8.76.0 + 8.76.4 From 2c16143f6cdf509bb82d54bfa6759f2fecce4bad Mon Sep 17 00:00:00 2001 From: Sam Barker Date: Wed, 25 Mar 2026 21:10:17 +1300 Subject: [PATCH 8/9] fix: register event sources when dependents are marked for deletion (#3250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add failing IT reproducing NoEventSourceForClassException in NodeDeleteExecutor When a BulkDependentResource has an activationCondition and its parent dependent has a failing reconcilePrecondition, JOSDK's markDependentsForDelete() cascades to the bulk dependent and fires NodeDeleteExecutor for it. However, NodeDeleteExecutor does not call registerOrDeregisterEventSourceBasedOnActivation() before invoking delete(), so if NodeReconcileExecutor has never run for that node (e.g. on first reconciliation) the event source is never registered. The delete() path calls getSecondaryResources() → eventSourceRetriever .getEventSourceFor() → NoEventSourceForClassException. This IT demonstrates the bug with a minimal workflow: ConfigMapDependentResource (reconcilePrecondition = ALWAYS_FALSE) └── SecretBulkDependentResource (activationCondition = ALWAYS_TRUE) The fix is to call registerOrDeregisterEventSourceBasedOnActivation() in NodeDeleteExecutor.doRun() before calling dependent.delete(), mirroring what NodeReconcileExecutor already does. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Sam Barker * Address review comments on BulkActivationConditionIT - Make lastError and callCount instance fields on the reconciler; hold the reconciler instance as a static field in the IT so tests can access state without static leakage between tests - Add callCount so the test can wait for any reconciliation activity (reconcile() or updateErrorStatus()) then assert cleanly, rather than timing out if the bug is fixed - Add @Disabled linking to issue #3249 so this reproducer-only test does not break CI - Add @Sample annotation to match the pattern of other workflow ITs Assisted-by: Claude Sonnet 4.6 Signed-off-by: Sam Barker * fix: register event source when dependents are marked for deletion Fixes #3249 Signed-off-by: Chris Laprun --------- Signed-off-by: Sam Barker Signed-off-by: Chris Laprun Co-authored-by: Chris Laprun --- .../workflow/WorkflowReconcileExecutor.java | 4 + .../AlwaysFailingPrecondition.java | 34 +++++++ .../AlwaysTrueActivation.java | 34 +++++++ ...BulkActivationConditionCustomResource.java | 28 ++++++ .../BulkActivationConditionIT.java | 91 +++++++++++++++++++ .../BulkActivationConditionReconciler.java | 82 +++++++++++++++++ .../ConfigMapDependentResource.java | 43 +++++++++ .../SecretBulkDependentResource.java | 80 ++++++++++++++++ 8 files changed, 396 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysFailingPrecondition.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysTrueActivation.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/ConfigMapDependentResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/SecretBulkDependentResource.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index 9121cf4e07..d0d2435347 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -246,6 +246,10 @@ private void markDependentsForDelete( // so if the activation condition was false, this node is not meant to be deleted. var dependents = dependentResourceNode.getParents(); if (activationConditionMet) { + // make sure we register the dependent's event source if it hasn't been added already + // this might be needed in corner cases such as + // https://github.com/operator-framework/java-operator-sdk/issues/3249 + registerOrDeregisterEventSourceBasedOnActivation(true, dependentResourceNode); createOrGetResultFor(dependentResourceNode).markForDelete(); if (dependents.isEmpty()) { bottomNodes.add(dependentResourceNode); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysFailingPrecondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysFailingPrecondition.java new file mode 100644 index 0000000000..4c21fc019d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysFailingPrecondition.java @@ -0,0 +1,34 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +/** Reconcile precondition that always fails, simulating e.g. a missing prerequisite resource. */ +public class AlwaysFailingPrecondition + implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, + BulkActivationConditionCustomResource primary, + Context context) { + return false; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysTrueActivation.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysTrueActivation.java new file mode 100644 index 0000000000..bc3b70bbb1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/AlwaysTrueActivation.java @@ -0,0 +1,34 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +/** Activation condition that always returns true — event source should always be registered. */ +public class AlwaysTrueActivation + implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, + BulkActivationConditionCustomResource primary, + Context context) { + return true; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionCustomResource.java new file mode 100644 index 0000000000..da5d22f482 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionCustomResource.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("BulkActivationCondition") +public class BulkActivationConditionCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionIT.java new file mode 100644 index 0000000000..d0370927fb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionIT.java @@ -0,0 +1,91 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import java.time.Duration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Reproducer for the bug where NodeDeleteExecutor fires for a BulkDependentResource whose + * activationCondition-gated event source has never been registered. + * + *

Workflow under test: + * + *

+ * ConfigMapDependentResource  (reconcilePrecondition = AlwaysFailingPrecondition)
+ *   └── SecretBulkDependentResource  (activationCondition = AlwaysTrueActivation)
+ * 
+ * + *

On first reconciliation the ConfigMap precondition fails → JOSDK calls + * markDependentsForDelete() → NodeDeleteExecutor fires for SecretBulkDependent → + * SecretBulkDependent.getSecondaryResources() calls eventSourceRetriever.getEventSourceFor() → the + * Secret event source was never registered (NodeReconcileExecutor never ran) → + * NoEventSourceForClassException. + * + *

This test FAILS on unfixed JOSDK, demonstrating the bug. + */ +@Sample( + tldr = "Bulk Dependent Resource with Activation Condition Bug Reproducer", + description = + """ + Reproducer for a bug where NodeDeleteExecutor fires for a BulkDependentResource \ + with an activationCondition before its event source has been registered, \ + causing NoEventSourceForClassException. Triggered when a parent dependent \ + has a failing reconcilePrecondition on first reconciliation. + """) +public class BulkActivationConditionIT { + + static final BulkActivationConditionReconciler reconciler = + new BulkActivationConditionReconciler(); + + @RegisterExtension + static LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(reconciler).build(); + + @BeforeEach + void reset() { + reconciler.lastError.set(null); + reconciler.callCount.set(0); + } + + @Test + void nodeDeleteExecutorShouldNotThrowWhenEventSourceNotYetRegistered() { + var primary = new BulkActivationConditionCustomResource(); + primary.setMetadata( + new ObjectMetaBuilder() + .withName("test-primary") + .withNamespace(extension.getNamespace()) + .build()); + extension.create(primary); + + // Wait for reconcile() to be called. + // If the bug is present, SecretBulkDependentResource will be in error and lastError will be set + await().atMost(Duration.ofSeconds(10)).until(() -> reconciler.callCount.get() == 1); + + // On unfixed JOSDK this fails: lastError contains NoEventSourceForClassException. + assertThat(reconciler.lastError.get()).isNull(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionReconciler.java new file mode 100644 index 0000000000..0f658942ef --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/BulkActivationConditionReconciler.java @@ -0,0 +1,82 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.retry.GradualRetry; + +/** + * Workflow: + * + *

+ * ConfigMapDependentResource  (reconcilePrecondition = AlwaysFailingPrecondition)
+ *   └── SecretBulkDependentResource  (activationCondition = AlwaysTrueActivation)
+ * 
+ * + *

On first reconciliation: ConfigMap precondition fails → markDependentsForDelete cascades to + * SecretBulkDependentResource → NodeDeleteExecutor fires for Secret before its event source is ever + * registered → NoEventSourceForClassException. + */ +@Workflow( + dependents = { + @Dependent( + name = "configmap", + type = ConfigMapDependentResource.class, + reconcilePrecondition = AlwaysFailingPrecondition.class), + @Dependent( + name = SecretBulkDependentResource.NAME, + type = SecretBulkDependentResource.class, + activationCondition = AlwaysTrueActivation.class, + dependsOn = "configmap") + }, + handleExceptionsInReconciler = true) +@GradualRetry(maxAttempts = 0) +@ControllerConfiguration(maxReconciliationInterval = @MaxReconciliationInterval(interval = 0)) +public class BulkActivationConditionReconciler + implements Reconciler { + + /** Tracks how many times reconcile() or updateErrorStatus() has been called. */ + final AtomicInteger callCount = new AtomicInteger(); + + /** Set when updateErrorStatus() is invoked; null means no error occurred. */ + final AtomicReference lastError = new AtomicReference<>(); + + @Override + public UpdateControl reconcile( + BulkActivationConditionCustomResource primary, + Context context) { + final var workflowResult = + context + .managedWorkflowAndDependentResourceContext() + .getWorkflowReconcileResult() + .orElseThrow(); + final var erroredDependents = workflowResult.getErroredDependents(); + if (!erroredDependents.isEmpty()) { + final var exception = + erroredDependents.get( + workflowResult + .getDependentResourceByName(SecretBulkDependentResource.NAME) + .orElseThrow()); + lastError.set(exception); + } + callCount.incrementAndGet(); + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/ConfigMapDependentResource.java new file mode 100644 index 0000000000..26b45af98a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/ConfigMapDependentResource.java @@ -0,0 +1,43 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +/** + * Parent dependent resource. Its reconcile precondition always fails, which causes JOSDK to call + * markDependentsForDelete() on its children — triggering NodeDeleteExecutor for SecretBulkDependent + * before that resource's event source has ever been registered. + */ +public class ConfigMapDependentResource + extends CRUDKubernetesDependentResource { + + @Override + protected ConfigMap desired( + BulkActivationConditionCustomResource primary, + Context context) { + var cm = new ConfigMap(); + cm.setMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + return cm; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/SecretBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/SecretBulkDependentResource.java new file mode 100644 index 0000000000..d710a5a583 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/bulkactivationcondition/SecretBulkDependentResource.java @@ -0,0 +1,80 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.bulkactivationcondition; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.ResourceIDMapper; +import io.javaoperatorsdk.operator.processing.dependent.CRUDKubernetesBulkDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + * Child bulk dependent resource with an activationCondition. + * + *

The bug: when NodeDeleteExecutor fires for this resource (because its parent + * ConfigMapDependentResource has a failing reconcilePrecondition), the event source for Secret has + * never been registered — NodeReconcileExecutor never ran for this node. NodeDeleteExecutor does + * NOT call registerOrDeregisterEventSourceBasedOnActivation() before calling delete(), so + * getSecondaryResources() → eventSourceRetriever.getEventSourceFor(Secret.class) throws + * NoEventSourceForClassException. + * + *

This implementation intentionally has no try/catch — it exposes the raw bug. + */ +public class SecretBulkDependentResource + extends KubernetesDependentResource + implements CRUDKubernetesBulkDependentResource { + + static final String NAME = "secret"; + static final String LABEL_KEY = "reproducer"; + static final String LABEL_VALUE = "bulk-activation-condition"; + + @Override + public Map desiredResources( + BulkActivationConditionCustomResource primary, + Context context) { + var secret = new Secret(); + secret.setMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .withLabels(Map.of(LABEL_KEY, LABEL_VALUE)) + .build()); + return Map.of(ResourceIDMapper.kubernetesResourceIdMapper().idFor(secret), secret); + } + + @Override + public Map getSecondaryResources( + BulkActivationConditionCustomResource primary, + Context context) { + // Deliberately uses getEventSourceFor (singular) — not getSecondaryResourcesAsStream — + // because the singular variant throws NoEventSourceForClassException when the source is absent. + // This mirrors the Kroxylicious ClusterRouteDependentResource pattern and exposes the bug: + // on first reconciliation NodeDeleteExecutor fires before the event source is registered. + var secrets = + context + .eventSourceRetriever() + .getEventSourceFor(Secret.class) + .getSecondaryResources(primary); + return secrets.stream() + .collect(Collectors.toMap(ResourceID::fromResource, Function.identity())); + } +} From a932a3d28e136954234b97a3e7b7b80de11f34d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 25 Mar 2026 09:33:43 +0100 Subject: [PATCH 9/9] improve: config providers for yaml and properties logs only warning on missing file (#3245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve: config providers for yaml and properties logs only warning on missing file This it is easier to handle dynamic configurations coming from a ConfigMap Signed-off-by: Attila Mészáros * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Attila Mészáros --------- Signed-off-by: Attila Mészáros Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../provider/PropertiesConfigProvider.java | 23 ++++++++++++++++++- .../loader/provider/YamlConfigProvider.java | 23 ++++++++++++++++++- .../PropertiesConfigProviderTest.java | 9 +++----- .../provider/YamlConfigProviderTest.java | 9 +++----- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java index 35dd38f406..8670b88f06 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java @@ -23,6 +23,9 @@ import java.util.Optional; import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.javaoperatorsdk.operator.config.loader.ConfigProvider; /** @@ -34,6 +37,8 @@ */ public class PropertiesConfigProvider implements ConfigProvider { + private static final Logger log = LoggerFactory.getLogger(PropertiesConfigProvider.class); + private final Properties properties; /** Returns a {@link PropertiesConfigProvider} backed by {@link System#getProperties()}. */ @@ -44,7 +49,18 @@ public static PropertiesConfigProvider systemProperties() { /** * Loads properties from the given file path. * - * @throws UncheckedIOException if the file cannot be read + * @throws UncheckedIOException if the file cannot be read. Does not throw an exception if the + * file does not exist. + */ + public PropertiesConfigProvider(String path) { + this(Path.of(path)); + } + + /** + * Loads properties from the given file path. + * + * @throws UncheckedIOException if the file cannot be read. Does not throw an exception if the + * file does not exist. */ public PropertiesConfigProvider(Path path) { this.properties = load(path); @@ -68,6 +84,11 @@ public Optional getValue(String key, Class type) { } private static Properties load(Path path) { + if (!Files.exists(path)) { + log.warn("{} does not exist", path); + return new Properties(); + } + try (InputStream in = Files.newInputStream(path)) { Properties props = new Properties(); props.load(in); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java index 52b07b011d..d631496d59 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java @@ -23,6 +23,9 @@ import java.util.Map; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.javaoperatorsdk.operator.config.loader.ConfigProvider; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,6 +42,8 @@ */ public class YamlConfigProvider implements ConfigProvider { + private static final Logger log = LoggerFactory.getLogger(YamlConfigProvider.class); + private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); private final Map data; @@ -46,7 +51,18 @@ public class YamlConfigProvider implements ConfigProvider { /** * Loads YAML from the given file path. * - * @throws UncheckedIOException if the file cannot be read + * @throws UncheckedIOException if the file cannot be read. Does not throw an exception if the + * file does not exist. + */ + public YamlConfigProvider(String path) { + this(Path.of(path)); + } + + /** + * Loads YAML from the given file path. + * + * @throws UncheckedIOException if the file cannot be read. Does not throw an exception if the + * file does not exist. */ public YamlConfigProvider(Path path) { this.data = load(path); @@ -79,6 +95,11 @@ public Optional getValue(String key, Class type) { @SuppressWarnings("unchecked") private static Map load(Path path) { + if (!Files.exists(path)) { + log.warn("{} does not exist", path); + return Map.of(); + } + try (InputStream in = Files.newInputStream(path)) { Map result = MAPPER.readValue(in, Map.class); return result != null ? result : Map.of(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java index c44534eb3a..dd9fd7b599 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java @@ -16,7 +16,6 @@ package io.javaoperatorsdk.operator.config.loader.provider; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -27,7 +26,6 @@ import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; class PropertiesConfigProviderTest { @@ -120,10 +118,9 @@ void loadsFromFile(@TempDir Path dir) throws IOException { } @Test - void throwsUncheckedIOExceptionForMissingFile(@TempDir Path dir) { + void returnsEmptyForNonExistingFile(@TempDir Path dir) { Path missing = dir.resolve("does-not-exist.properties"); - assertThatExceptionOfType(UncheckedIOException.class) - .isThrownBy(() -> new PropertiesConfigProvider(missing)) - .withMessageContaining("does-not-exist.properties"); + var provider = new PropertiesConfigProvider(missing); + assertThat(provider.getValue("any.key", String.class)).isEmpty(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java index 4f8c53ac38..2362b85ea4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java @@ -16,7 +16,6 @@ package io.javaoperatorsdk.operator.config.loader.provider; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -27,7 +26,6 @@ import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; class YamlConfigProviderTest { @@ -135,10 +133,9 @@ void loadsFromFile(@TempDir Path dir) throws IOException { } @Test - void throwsUncheckedIOExceptionForMissingFile(@TempDir Path dir) { + void returnsEmptyForNonExistingFile(@TempDir Path dir) { Path missing = dir.resolve("does-not-exist.yaml"); - assertThatExceptionOfType(UncheckedIOException.class) - .isThrownBy(() -> new YamlConfigProvider(missing)) - .withMessageContaining("does-not-exist.yaml"); + var provider = new YamlConfigProvider(missing); + assertThat(provider.getValue("any.key", String.class)).isEmpty(); } }