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..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,11 @@
*/
package io.javaoperatorsdk.operator.config.runtime;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import javax.tools.StandardLocation;
+
import org.junit.jupiter.api.Test;
import com.google.testing.compile.Compilation;
@@ -22,6 +27,9 @@
import com.google.testing.compile.Compiler;
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 {
@Test
@@ -33,6 +41,9 @@ public void generateCorrectDoneableClassIfInterfaceIsSecond() {
JavaFileObjects.forResource(
"compile-fixtures/ReconcilerImplemented2Interfaces.java"));
CompilationSubject.assertThat(compilation).succeeded();
+ assertMapping(
+ compilation,
+ "io.ReconcilerImplemented2Interfaces,io.ReconcilerImplemented2Interfaces.MyCustomResource");
}
@Test
@@ -45,6 +56,9 @@ public void generateCorrectDoneableClassIfThereIsAbstractBaseController() {
JavaFileObjects.forResource(
"compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java"));
CompilationSubject.assertThat(compilation).succeeded();
+ assertMapping(
+ compilation,
+ "io.ReconcilerImplementedIntermediateAbstractClass,io.AbstractReconciler.MyCustomResource");
}
@Test
@@ -57,5 +71,74 @@ 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");
+ }
+
+ /**
+ * 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
+ * {@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);
+ }
+
+ /**
+ * 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}_$]*)*");
+ }
+ });
}
}
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();
+ }
+}
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