From 9118afd30a36efba73cb1e4c96f37b117c64a95b Mon Sep 17 00:00:00 2001 From: hej090224 Date: Sun, 26 Jul 2026 01:58:08 +0900 Subject: [PATCH 1/2] test: add Kotlin smoke test for checked exception handling Adds a Maven-compiled Kotlin test source set to operator-framework-core and a smoke test verifying that a checked (non-RuntimeException) Exception thrown from a Kotlin DependentResource is properly caught and reported by the workflow executor, so that retries are triggered as expected. Kotlin does not have checked exceptions, so Kotlin code can throw a checked Exception from an overridden method without declaring it, even though the Java DependentResource#reconcile signature does not declare `throws Exception`. Before #2965 this exception would not have been caught by NodeExecutor, since it only handled RuntimeException, silently swallowing the error and preventing retries. Closes #2967 --- operator-framework-core/pom.xml | 43 +++++++++ ...inCheckedExceptionDependentResourceTest.kt | 88 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 5763c5490a..c2463fcee2 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -30,6 +30,11 @@ Operator SDK - Framework - Core Core framework for implementing Kubernetes operators + + + 2.4.10 + + io.github.java-diff-utils @@ -101,6 +106,13 @@ kube-api-test-client-inject test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + test + @@ -147,6 +159,37 @@ + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + ${java.version} + + + + kotlin-test-compile + + test-compile + + process-test-sources + + + ${project.basedir}/src/test/kotlin + + ${project.basedir}/src/test/java + + + + + diff --git a/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt b/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt new file mode 100644 index 0000000000..230e0e6af4 --- /dev/null +++ b/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt @@ -0,0 +1,88 @@ +/* + * 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.processing.dependent.workflow + +import io.javaoperatorsdk.operator.AggregatedOperatorException +import io.javaoperatorsdk.operator.api.reconciler.Context +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource +import java.util.concurrent.Executors +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +/** + * Smoke test verifying that JOSDK works properly when used from Kotlin. + * + * Unlike Java, Kotlin does not distinguish between checked and unchecked exceptions, so a Kotlin + * [DependentResource] can throw a plain [java.lang.Exception] from `reconcile` without declaring + * it, even though the Java `DependentResource.reconcile` method does not declare `throws + * Exception`. Before https://github.com/operator-framework/java-operator-sdk/pull/2965 such an + * exception was not caught by [NodeExecutor], since it only handled [RuntimeException], so it + * would not have been recorded as an error and retries would not have been triggered. This test + * makes sure such an exception is properly caught and reported, see + * https://github.com/operator-framework/java-operator-sdk/issues/2967. + */ +class KotlinCheckedExceptionDependentResourceTest { + + private class CheckedException(message: String) : Exception(message) + + private class ThrowingDependentResource : + DependentResource { + override fun reconcile( + primary: TestCustomResource, + context: Context + ): ReconcileResult { + throw CheckedException("checked exception thrown from Kotlin") + } + + override fun resourceType(): Class = String::class.java + } + + @Test + fun checkedExceptionThrownFromKotlinDependentResourceTriggersRetry() { + val dependentResource = ThrowingDependentResource() + + val workflow = + WorkflowBuilder() + .addDependentResource(dependentResource) + .withThrowExceptionFurther(false) + .build() + + @Suppress("UNCHECKED_CAST") + val context = mock(Context::class.java) as Context + `when`(context.managedWorkflowAndDependentResourceContext()) + .thenReturn(mock(ManagedWorkflowAndDependentResourceContext::class.java)) + `when`(context.workflowExecutorService).thenReturn(Executors.newCachedThreadPool()) + @Suppress("UNCHECKED_CAST") + `when`(context.eventSourceRetriever()) + .thenReturn(mock(EventSourceRetriever::class.java) as EventSourceRetriever) + + val result = workflow.reconcile(TestCustomResource(), context) + + // the checked exception was caught by the workflow executor, not left uncaught, so it is + // reported as an error for the dependent resource, which is what allows JOSDK to retry the + // reconciliation. + assertThat(result.erroredDependents).containsOnlyKeys(dependentResource) + assertThat(result.erroredDependents[dependentResource]).isInstanceOf(CheckedException::class.java) + assertThrows { result.throwAggregateExceptionIfErrorsPresent() } + } +} From 65dc96f0c7272e82264d2d3cef27fdbcc28034e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EC=9D=98=EC=A4=80?= Date: Sun, 26 Jul 2026 13:00:45 +0900 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ...inCheckedExceptionDependentResourceTest.kt | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt b/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt index 230e0e6af4..5ade12da36 100644 --- a/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt +++ b/operator-framework-core/src/test/kotlin/io/javaoperatorsdk/operator/processing/dependent/workflow/KotlinCheckedExceptionDependentResourceTest.kt @@ -69,20 +69,25 @@ class KotlinCheckedExceptionDependentResourceTest { @Suppress("UNCHECKED_CAST") val context = mock(Context::class.java) as Context - `when`(context.managedWorkflowAndDependentResourceContext()) - .thenReturn(mock(ManagedWorkflowAndDependentResourceContext::class.java)) - `when`(context.workflowExecutorService).thenReturn(Executors.newCachedThreadPool()) - @Suppress("UNCHECKED_CAST") - `when`(context.eventSourceRetriever()) - .thenReturn(mock(EventSourceRetriever::class.java) as EventSourceRetriever) + val executorService = Executors.newSingleThreadExecutor() + try { + `when`(context.managedWorkflowAndDependentResourceContext()) + .thenReturn(mock(ManagedWorkflowAndDependentResourceContext::class.java)) + `when`(context.workflowExecutorService).thenReturn(executorService) + @Suppress("UNCHECKED_CAST") + `when`(context.eventSourceRetriever()) + .thenReturn(mock(EventSourceRetriever::class.java) as EventSourceRetriever) - val result = workflow.reconcile(TestCustomResource(), context) + val result = workflow.reconcile(TestCustomResource(), context) - // the checked exception was caught by the workflow executor, not left uncaught, so it is - // reported as an error for the dependent resource, which is what allows JOSDK to retry the - // reconciliation. - assertThat(result.erroredDependents).containsOnlyKeys(dependentResource) - assertThat(result.erroredDependents[dependentResource]).isInstanceOf(CheckedException::class.java) - assertThrows { result.throwAggregateExceptionIfErrorsPresent() } - } + // the checked exception was caught by the workflow executor, not left uncaught, so it is + // reported as an error for the dependent resource, which is what allows JOSDK to retry the + // reconciliation. + assertThat(result.erroredDependents).containsOnlyKeys(dependentResource) + assertThat(result.erroredDependents[dependentResource]) + .isInstanceOf(CheckedException::class.java) + assertThrows { result.throwAggregateExceptionIfErrorsPresent() } + } finally { + executorService.shutdownNow() + } }