From 348865483a76ea0813c51d8092d9befdae48e404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 11 Aug 2026 19:13:11 +0200 Subject: [PATCH 1/3] chore: remove the JavaPoet dependency JavaPoet was only used in ControllerConfigurationAnnotationProcessor to turn the resolved resource TypeMirror into its fully qualified name, which the standard annotation processing API can do on its own: the resolved type is always a declared type, so its element is a TypeElement and its qualified name is exactly the dot separated name ClassMappingProvider loads at runtime. JavaPoet has not been released since 2024, which makes it unusable for users whose organizations do not approve unmaintained dependencies. Also assert the content of the generated mapping resource in the processor tests, which previously only checked that compilation succeeded and so could not have caught a wrong resource class name. --- operator-framework/pom.xml | 5 ---- ...ollerConfigurationAnnotationProcessor.java | 9 ++++---- ...rConfigurationAnnotationProcessorTest.java | 23 +++++++++++++++++++ pom.xml | 6 ----- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index d3e3df7b19..60bca8c8ff 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -40,11 +40,6 @@ org.apache.commons commons-lang3 - - com.squareup - javapoet - compile - org.slf4j slf4j-api diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java index d94233d8d8..700d5a40c0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java @@ -34,8 +34,6 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import com.squareup.javapoet.TypeName; - import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; @SupportedAnnotationTypes("io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration") @@ -103,9 +101,12 @@ private void recordCRType(TypeElement controllerClassSymbol) { + "': ignoring!"); return; } - final TypeName customResourceType = TypeName.get(resourceType); + // the resolved resource type is always a declared type, so its element is a TypeElement + final var customResourceType = + (TypeElement) processingEnv.getTypeUtils().asElement(resourceType); controllersResourceWriter.add( - controllerClassSymbol.getQualifiedName().toString(), customResourceType.toString()); + controllerClassSymbol.getQualifiedName().toString(), + customResourceType.getQualifiedName().toString()); } catch (Exception ioException) { log.error("Error", ioException); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java index 058709aeb3..35e3375270 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java @@ -15,6 +15,8 @@ */ package io.javaoperatorsdk.operator.config.runtime; +import javax.tools.StandardLocation; + import org.junit.jupiter.api.Test; import com.google.testing.compile.Compilation; @@ -22,6 +24,8 @@ import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; +import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; + class ControllerConfigurationAnnotationProcessorTest { @Test @@ -33,6 +37,9 @@ public void generateCorrectDoneableClassIfInterfaceIsSecond() { JavaFileObjects.forResource( "compile-fixtures/ReconcilerImplemented2Interfaces.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.ReconcilerImplemented2Interfaces,io.ReconcilerImplemented2Interfaces.MyCustomResource"); } @Test @@ -45,6 +52,9 @@ public void generateCorrectDoneableClassIfThereIsAbstractBaseController() { JavaFileObjects.forResource( "compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.ReconcilerImplementedIntermediateAbstractClass,io.AbstractReconciler.MyCustomResource"); } @Test @@ -57,5 +67,18 @@ public void generateDoneableClassWithMultilevelHierarchy() { JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractReconciler.java"), JavaFileObjects.forResource("compile-fixtures/MultilevelReconciler.java")); CompilationSubject.assertThat(compilation).succeeded(); + assertMapping(compilation, "io.MultilevelReconciler,io.MultilevelReconciler.MyCustomResource"); + } + + /** + * Checks that the generated mapping resource contains the expected {@code + * reconciler,resource-class} line, using the same fully qualified, dot separated names that + * {@link ClassMappingProvider} expects to be able to load at runtime. + */ + private static void assertMapping(Compilation compilation, String expectedMapping) { + CompilationSubject.assertThat(compilation) + .generatedFile(StandardLocation.CLASS_OUTPUT, RECONCILERS_RESOURCE_PATH) + .contentsAsUtf8String() + .contains(expectedMapping); } } diff --git a/pom.xml b/pom.xml index 78d2c7a35b..c144225241 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,6 @@ 5.23.0 3.20.0 0.23.0 - 1.13.0 3.27.7 4.3.0 2.7.3 @@ -148,11 +147,6 @@ micrometer-core ${micrometer-core.version} - - com.squareup - javapoet - ${javapoet.version} - org.awaitility awaitility From b391b3f94f15ed6f11d18c350323074a24a530c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 11 Aug 2026 22:48:05 +0200 Subject: [PATCH 2/3] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- ...rConfigurationAnnotationProcessorTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java index 35e3375270..e623bf61be 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java @@ -15,6 +15,9 @@ */ package io.javaoperatorsdk.operator.config.runtime; +import java.io.IOException; +import java.io.UncheckedIOException; + import javax.tools.StandardLocation; import org.junit.jupiter.api.Test; @@ -25,6 +28,7 @@ import com.google.testing.compile.JavaFileObjects; import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; +import static org.assertj.core.api.Assertions.assertThat; class ControllerConfigurationAnnotationProcessorTest { @@ -70,6 +74,26 @@ public void generateDoneableClassWithMultilevelHierarchy() { assertMapping(compilation, "io.MultilevelReconciler,io.MultilevelReconciler.MyCustomResource"); } + /** + * When the reconciled resource is itself generic, the resolved type is a parameterized {@code + * DeclaredType}. Only its erasure may be written to the mapping resource: {@link + * ClassMappingProvider} loads the recorded name with {@code ClassUtils.getClass(String)}, which + * cannot parse type arguments. + */ + @Test + public void writesErasureOfGenericResourceType() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ControllerConfigurationAnnotationProcessor()) + .compile( + JavaFileObjects.forResource("compile-fixtures/GenericResourceReconciler.java")); + CompilationSubject.assertThat(compilation).succeeded(); + assertMapping( + compilation, + "io.GenericResourceReconciler,io.GenericResourceReconciler.MyGenericCustomResource"); + assertLoadableMapping(compilation); + } + /** * Checks that the generated mapping resource contains the expected {@code * reconciler,resource-class} line, using the same fully qualified, dot separated names that @@ -81,4 +105,40 @@ private static void assertMapping(Compilation compilation, String expectedMappin .contentsAsUtf8String() .contains(expectedMapping); } + + /** + * Checks that every recorded name in the generated mapping resource is a plain binary-ish class + * name, i.e. one that {@code ClassUtils.getClass(String)} can actually resolve, rather than a + * generic type signature such as {@code io.Foo}. + */ + private static void assertLoadableMapping(Compilation compilation) { + final var contents = + compilation + .generatedFile(StandardLocation.CLASS_OUTPUT, RECONCILERS_RESOURCE_PATH) + .map( + file -> { + try { + return file.getCharContent(true).toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .orElseThrow(() -> new AssertionError("no mapping resource was generated")); + contents + .lines() + .filter(line -> !line.isBlank()) + .forEach( + line -> { + final var names = line.split(","); + assertThat(names).as("mapping line '%s'", line).hasSize(2); + for (String name : names) { + assertThat(name) + .as("recorded class name '%s' must be loadable at runtime", name) + .doesNotContain("<") + .doesNotContain(">") + .doesNotContain(" ") + .matches("[\\p{L}_$][\\p{L}\\p{N}_$]*(\\.[\\p{L}_$][\\p{L}\\p{N}_$]*)*"); + } + }); + } } From c72eb81106cd4f6ce34b7ec4bf964049864b92bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 11 Aug 2026 22:48:19 +0200 Subject: [PATCH 3/3] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../GenericResourceReconciler.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java diff --git a/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java b/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java new file mode 100644 index 0000000000..d17f97b1f9 --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/GenericResourceReconciler.java @@ -0,0 +1,42 @@ +/* + * 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; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +/** + * The reconciled resource is itself a generic type, so the resolved resource type is a + * parameterized {@code DeclaredType}. Only its erasure can be written to the mapping resource, + * because that is the only form {@code ClassMappingProvider} is able to load at runtime. + */ +@ControllerConfiguration +public class GenericResourceReconciler implements + Reconciler> { + + public static class MyGenericCustomResource extends CustomResource { + } + + @Override + public UpdateControl> reconcile( + MyGenericCustomResource customResource, + Context> context) { + return UpdateControl.noUpdate(); + } +}