diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index c86c3f95b6..2407189c1c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -34,6 +34,7 @@ public class AnnotationControllerConfiguration protected final Reconciler reconciler; private final ControllerConfiguration annotation; private List specs; + private Class resourceClass; public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -81,7 +82,12 @@ public Set getNamespaces() { @Override @SuppressWarnings("unchecked") public Class getResourceClass() { - return (Class) Utils.getFirstTypeArgumentFromInterface(reconciler.getClass()); + if (resourceClass == null) { + resourceClass = + (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(reconciler.getClass(), + Reconciler.class); + } + return resourceClass; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 7fc0ca27b7..33e5f0218f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -55,7 +55,15 @@ default Optional reconciliationMaxInterval() { return Optional.of(Duration.ofHours(10L)); } + @SuppressWarnings("unused") default ConfigurationService getConfigurationService() { return ConfigurationServiceProvider.instance(); } + + @SuppressWarnings("unchecked") + @Override + default Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), + ControllerConfiguration.class); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java index ca59e738d8..70d2b765a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -31,7 +31,8 @@ default String getLabelSelector() { @SuppressWarnings("unchecked") default Class getResourceClass() { - return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), + ResourceConfiguration.class); } default Set getNamespaces() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index d74d27f2d4..c9f636101a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -5,13 +5,15 @@ import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.time.Instant; +import java.util.Arrays; import java.util.Date; import java.util.Properties; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.OperatorException; + public class Utils { private static final Logger log = LoggerFactory.getLogger(Utils.class); @@ -59,6 +61,8 @@ public static Version loadFromProperties() { builtTime); } + @SuppressWarnings("unused") + // this is used in the Quarkus extension public static boolean isValidateCustomResourcesEnvVarSet() { return System.getProperty(CHECK_CRD_ENV_KEY) != null; } @@ -89,16 +93,55 @@ static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean d } public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { - Type type = clazz.getGenericSuperclass(); - return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + try { + Type type = clazz.getGenericSuperclass(); + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } catch (Exception e) { + throw new RuntimeException("Couldn't retrieve generic parameter type from " + + clazz.getSimpleName() + + " because it doesn't extend a class that is parameterized with the type we want to retrieve", + e); + } } - public static Class getFirstTypeArgumentFromInterface(Class clazz) { - ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; - return (Class) type.getActualTypeArguments()[0]; + public static Class getFirstTypeArgumentFromInterface(Class clazz, + Class expectedImplementedInterface) { + return Arrays.stream(clazz.getGenericInterfaces()) + .filter(type -> type.getTypeName().startsWith(expectedImplementedInterface.getName()) + && type instanceof ParameterizedType) + .map(ParameterizedType.class::cast) + .findFirst() + .map(t -> (Class) t.getActualTypeArguments()[0]) + .orElseThrow(() -> new RuntimeException( + "Couldn't retrieve generic parameter type from " + clazz.getSimpleName() + + " because it doesn't implement " + + expectedImplementedInterface.getSimpleName() + + " directly")); } - public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { - return annotation == null ? defaultValue : mapper.apply(annotation); + public static Class getFirstTypeArgumentFromSuperClassOrInterface(Class clazz, + Class expectedImplementedInterface) { + // first check super class if it exists + try { + final Class superclass = clazz.getSuperclass(); + if (!superclass.equals(Object.class)) { + try { + return getFirstTypeArgumentFromExtendedClass(clazz); + } catch (Exception e) { + // try interfaces + try { + return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); + } catch (Exception ex) { + // try on the parent + return getFirstTypeArgumentFromSuperClassOrInterface(superclass, + expectedImplementedInterface); + } + } + } + return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); + } catch (Exception e) { + throw new OperatorException( + "Couldn't retrieve generic parameter type from " + clazz.getSimpleName(), e); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 2fc74aa010..9cb0f1ee1d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -160,4 +161,10 @@ static InformerConfigurationBuilder from( .withNamespacesInheritedFromController(eventSourceContext); } + @SuppressWarnings("unchecked") + @Override + default Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), + InformerConfiguration.class); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 05aa637f1f..1e78288232 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -4,12 +4,18 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class ControllerConfigurationTest { @Test void getCustomResourceClass() { + final ControllerConfiguration lambdasCannotBeUsedToExtractGenericParam = + () -> null; + assertThrows(RuntimeException.class, + lambdasCannotBeUsedToExtractGenericParam::getResourceClass); + final ControllerConfiguration conf = new ControllerConfiguration<>() { @Override public String getAssociatedReconcilerClassName() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 05ab4d1258..ccdc69ee9b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -4,8 +4,12 @@ import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -84,10 +88,28 @@ void getsFirstTypeArgumentFromExtendedClass() { @Test void getsFirstTypeArgumentFromInterface() { - assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class)) + assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class, + DependentResource.class)) .isEqualTo(Deployment.class); } + @Test + void getsFirstTypeArgumentFromInterfaceFromParent() { + assertThat(Utils.getFirstTypeArgumentFromSuperClassOrInterface(ConcreteReconciler.class, + Reconciler.class)).isEqualTo(ConfigMap.class); + } + + public abstract static class AbstractReconciler

implements Reconciler

{ + } + + public static class ConcreteReconciler extends AbstractReconciler { + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + public static class TestDependentResource implements DependentResource { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java index 1595f88f97..068d762c03 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java @@ -17,8 +17,8 @@ public class RuntimeControllerMetadata { RECONCILERS_RESOURCE_PATH, Reconciler.class, HasMetadata.class); } - static Class getResourceClass( - Reconciler reconciler) { + @SuppressWarnings("unchecked") + static Class getResourceClass(Reconciler reconciler) { final Class resourceClass = controllerToCustomResourceMappings.get(reconciler.getClass()); if (resourceClass == null) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java index fc7fb887db..de54d2f3f4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -81,16 +81,15 @@ void getDependentResources() { @Test void missingAnnotationThrowsException() { - Assertions.assertThrows(OperatorException.class, () -> { - new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler()); - }); + Assertions.assertThrows(OperatorException.class, + () -> new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler())); } @SuppressWarnings("rawtypes") private DependentResourceSpec findByName( List dependentResourceSpecList, String name) { return dependentResourceSpecList.stream().filter(d -> d.getName().equals(name)).findFirst() - .get(); + .orElseThrow(); } @SuppressWarnings("rawtypes")