From 39ac4dcf88c1879b724d0dcfa5bb8b2698df749b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 7 Jun 2022 21:26:38 +0200 Subject: [PATCH 1/6] feat: cover more cases when trying to determine generic types --- .../AnnotationControllerConfiguration.java | 8 +++- .../api/config/ControllerConfiguration.java | 8 ++++ .../api/config/ResourceConfiguration.java | 3 +- .../operator/api/config/Utils.java | 42 ++++++++++++++++--- .../informer/InformerConfiguration.java | 7 ++++ .../config/ControllerConfigurationTest.java | 8 +++- .../operator/api/config/UtilsTest.java | 27 +++++++++++- .../runtime/RuntimeControllerMetadata.java | 4 +- ...AnnotationControllerConfigurationTest.java | 7 ++-- 9 files changed, 99 insertions(+), 15 deletions(-) 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..98686f9425 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,6 +5,7 @@ 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; @@ -89,13 +90,44 @@ 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, + 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 Class getFirstTypeArgumentFromInterface(Class clazz) { - ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; - return (Class) type.getActualTypeArguments()[0]; + public static Class getFirstTypeArgumentFromSuperClassOrInterface(Class clazz, + Class expectedImplementedInterface) { + // first check super class if it exists + if (!clazz.getSuperclass().equals(Object.class)) { + try { + return getFirstTypeArgumentFromExtendedClass(clazz); + } catch (Exception e) { + // try interfaces + return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); + } + } + return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); } public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { 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..127f26915f 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,31 @@ 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") From bd0e7a1334dc94c1352656d9e0765eddf7a5e015 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 8 Jun 2022 13:56:06 +0200 Subject: [PATCH 2/6] fix: if first attempt failed, recursively check parent hierarchy --- .../operator/api/config/Utils.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) 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 98686f9425..97d1229517 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 @@ -119,15 +119,27 @@ public static Class getFirstTypeArgumentFromInterface(Class clazz, public static Class getFirstTypeArgumentFromSuperClassOrInterface(Class clazz, Class expectedImplementedInterface) { // first check super class if it exists - if (!clazz.getSuperclass().equals(Object.class)) { - try { - return getFirstTypeArgumentFromExtendedClass(clazz); - } catch (Exception e) { - // try interfaces - return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); + 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 RuntimeException( + "Couldn't retrieve generic parameter type from " + clazz.getSimpleName(), e); } - return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); } public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { From 1af3513d1289474087f12f63e355b64e55cb1616 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 9 Jun 2022 10:30:40 +0200 Subject: [PATCH 3/6] remove space --- .../io/javaoperatorsdk/operator/api/config/UtilsTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 127f26915f..b034001635 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 @@ -99,13 +99,9 @@ void getsFirstTypeArgumentFromInterfaceFromParent() { Reconciler.class)).isEqualTo(ConfigMap.class); } - public abstract static class AbstractReconciler

implements Reconciler

{ - - - } + public abstract static class AbstractReconciler

implements Reconciler

{} public static class ConcreteReconciler extends AbstractReconciler { - @Override public UpdateControl reconcile(ConfigMap resource, Context context) throws Exception { From d0e2969b0abfacaecf733096e08703b4318f6ebb Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 9 Jun 2022 10:31:50 +0200 Subject: [PATCH 4/6] format --- .../java/io/javaoperatorsdk/operator/api/config/UtilsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b034001635..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 @@ -99,7 +99,8 @@ void getsFirstTypeArgumentFromInterfaceFromParent() { Reconciler.class)).isEqualTo(ConfigMap.class); } - public abstract static class AbstractReconciler

implements Reconciler

{} + public abstract static class AbstractReconciler

implements Reconciler

{ + } public static class ConcreteReconciler extends AbstractReconciler { @Override From fe446d33aa8b8f9eb25443083fb4b89a83d7befb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 9 Jun 2022 10:52:22 +0200 Subject: [PATCH 5/6] fix: throw OperatorException --- .../java/io/javaoperatorsdk/operator/api/config/Utils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 97d1229517..a74be6bd28 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 @@ -8,11 +8,12 @@ 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); @@ -137,7 +138,7 @@ public static Class getFirstTypeArgumentFromSuperClassOrInterface(Class cl } return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface); } catch (Exception e) { - throw new RuntimeException( + throw new OperatorException( "Couldn't retrieve generic parameter type from " + clazz.getSimpleName(), e); } } From 1be2181056d48ddae04e8da9a814074e647cacb4 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 9 Jun 2022 10:52:44 +0200 Subject: [PATCH 6/6] chore: clean-up --- .../java/io/javaoperatorsdk/operator/api/config/Utils.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 a74be6bd28..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 @@ -61,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; } @@ -142,8 +144,4 @@ public static Class getFirstTypeArgumentFromSuperClassOrInterface(Class cl "Couldn't retrieve generic parameter type from " + clazz.getSimpleName(), e); } } - - public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { - return annotation == null ? defaultValue : mapper.apply(annotation); - } }