From 1eb00e72171ca216c2451e64d5e4819c8f5e6072 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 15 Nov 2021 15:48:57 +0100 Subject: [PATCH 1/2] feat: introduce declarative support for dependent resources --- .../io/javaoperatorsdk/operator/Operator.java | 2 +- .../operator/ReconcilerUtils.java | 3 +- .../operator/api/config/Cloner.java | 7 + .../api/config/ConfigurationService.java | 3 + .../api/config/ControllerConfiguration.java | 98 ++---------- .../ControllerConfigurationOverrider.java | 4 +- .../DefaultControllerConfiguration.java | 72 ++------- .../config/DefaultResourceConfiguration.java | 73 +++++++++ .../api/config/ExecutorServiceManager.java | 20 ++- .../api/config/ResourceConfiguration.java | 93 +++++++++++ .../operator/api/config/Utils.java | 6 + ...DefaultDependentResourceConfiguration.java | 68 ++++++++ .../DependentResourceConfiguration.java | 47 ++++++ .../operator/api/reconciler/Constants.java | 10 ++ .../operator/api/reconciler/Context.java | 8 +- .../reconciler/ControllerConfiguration.java | 25 +-- .../api/reconciler/DefaultContext.java | 26 ++- .../reconciler/EventSourceInitializer.java | 3 +- .../operator/api/reconciler/Reconciler.java | 5 +- .../api/reconciler/dependent/Builder.java | 8 + .../dependent/DependentResource.java | 53 +++++++ .../DependentResourceConfiguration.java | 110 +++++++++++++ .../api/reconciler/dependent/Fetcher.java | 19 +++ .../api/reconciler/dependent/Updater.java | 9 ++ .../operator/processing/Controller.java | 150 ++++++++++++++++-- .../processing/event/EventProcessor.java | 34 ++-- .../processing/event/EventSourceManager.java | 110 +++++++++++-- .../event/ReconciliationDispatcher.java | 16 +- .../event/source/AbstractEventSource.java | 28 +++- .../source/AbstractResourceEventSource.java | 139 ++++++++++++++++ .../event/source/AggregateResourceCache.java | 58 +++++++ .../source/AssociatedSecondaryIdentifier.java | 9 ++ .../source/AssociatedSecondaryRetriever.java | 8 + .../source/DependentResourceEventSource.java | 54 +++++++ .../processing/event/source/EventSource.java | 9 +- .../event/source/EventSourceRegistry.java | 10 +- .../event/source/EventSourceWrapper.java | 8 + .../source/InformerEventSourceWrapper.java | 50 ++++++ .../event/source/InformerResourceCache.java | 41 +++++ .../source/PrimaryResourcesRetriever.java | 11 ++ .../event/source/ResourceEventSource.java | 11 ++ .../ControllerResourceEventSource.java | 149 ++++------------- .../OnceWhitelistEventFilterEventFilter.java | 2 +- .../source/controller/ResourceEvent.java | 7 +- .../controller/ResourceEventFilter.java | 28 ++-- .../controller/ResourceEventFilters.java | 44 ++--- .../source/informer/InformerEventSource.java | 67 ++++---- .../event/source/informer/Mappers.java | 29 +++- .../event/source/timer/TimerEventSource.java | 7 +- .../operator/MockKubernetesClient.java | 41 +++++ .../operator/OperatorTest.java | 17 +- .../config/ControllerConfigurationTest.java | 3 + .../processing/event/EventProcessorTest.java | 14 +- .../event/EventSourceManagerTest.java | 8 +- .../event/ReconciliationDispatcherTest.java | 57 +++---- .../source/CustomResourceSelectorTest.java | 3 + .../event/source/ResourceEventFilterTest.java | 43 ++--- .../ControllerResourceEventSourceTest.java | 13 +- .../source/timer/TimerEventSourceTest.java | 23 +-- .../runtime/AnnotationConfiguration.java | 109 +++++++++++-- .../ErrorStatusHandlerTestReconciler.java | 5 +- ...formerEventSourceTestCustomReconciler.java | 9 +- .../ObservedGenerationTestReconciler.java | 2 +- .../retry/RetryTestCustomReconciler.java | 2 +- .../sample/DeploymentDependentResource.java | 47 ++++++ .../sample/ServiceDependentResource.java | 26 +++ .../operator/sample/TomcatReconciler.java | 130 +++------------ .../operator/sample/WebappReconciler.java | 59 ++++--- .../operator/sample/deployment.yaml | 5 - .../operator/sample/service.yaml | 5 - 70 files changed, 1791 insertions(+), 681 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DefaultDependentResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryIdentifier.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryRetriever.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 415e5c3505..c016f8c69e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -34,6 +34,7 @@ public Operator(ConfigurationService configurationService) { public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { this.kubernetesClient = kubernetesClient; this.configurationService = configurationService; + ExecutorServiceManager.init(configurationService); } /** Adds a shutdown hook that automatically calls {@link #close()} when the app shuts down. */ @@ -85,7 +86,6 @@ public void start() { throw new OperatorException(error, e); } - ExecutorServiceManager.init(configurationService); controllers.start(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 487c923349..dbf3d1a59e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -2,6 +2,7 @@ import java.util.Locale; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -19,7 +20,7 @@ public static String getNameFor(Class reconcilerClass) { final var annotation = reconcilerClass.getAnnotation(ControllerConfiguration.class); if (annotation != null) { final var name = annotation.name(); - if (!ControllerConfiguration.EMPTY_STRING.equals(name)) { + if (!Constants.EMPTY_STRING.equals(name)) { return name; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java index 30b2cee0e9..768c99c1a6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java @@ -4,6 +4,13 @@ public interface Cloner { + /** + * Returns a deep copy of the given object if not {@code null} or {@code null} otherwise. + * + * @param object the object to be cloned + * @param the type of the object to be cloned + * @return a deep copy of the given object if it isn't {@code null}, {@code null} otherwise + */ R clone(R object); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 2e68d7ff62..e02a376895 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -21,6 +21,9 @@ public interface ConfigurationService { @Override public HasMetadata clone(HasMetadata object) { + if (object == null) { + return null; + } try { return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), object.getClass()); } catch (JsonProcessingException e) { 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 31d701c03d..15ac6b221b 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 @@ -1,117 +1,45 @@ package io.javaoperatorsdk.operator.api.config; -import java.lang.reflect.ParameterizedType; import java.util.Collections; -import java.util.Set; +import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -public interface ControllerConfiguration { +public interface ControllerConfiguration extends + ResourceConfiguration> { default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getResourceTypeName() { - return CustomResource.getCRDName(getResourceClass()); - } - default String getFinalizer() { return ReconcilerUtils.getDefaultFinalizerName(getResourceTypeName()); } - /** - * Retrieves the label selector that is used to filter which custom resources are actually watched - * by the associated controller. See - * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on - * syntax. - * - * @return the label selector filtering watched custom resources - */ - default String getLabelSelector() { - return null; - } - default boolean isGenerationAware() { return true; } - default Class getResourceClass() { - ParameterizedType type = (ParameterizedType) getClass().getGenericInterfaces()[0]; - return (Class) type.getActualTypeArguments()[0]; - } - String getAssociatedReconcilerClassName(); - default Set getNamespaces() { - return Collections.emptySet(); - } - - default boolean watchAllNamespaces() { - return allNamespacesWatched(getNamespaces()); - } - - static boolean allNamespacesWatched(Set namespaces) { - return namespaces == null || namespaces.isEmpty(); - } - - default boolean watchCurrentNamespace() { - return currentNamespaceWatched(getNamespaces()); - } - - static boolean currentNamespaceWatched(Set namespaces) { - return namespaces != null - && namespaces.size() == 1 - && namespaces.contains( - io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.WATCH_CURRENT_NAMESPACE); - } - - /** - * Computes the effective namespaces based on the set specified by the user, in particular - * retrieves the current namespace from the client when the user specified that they wanted to - * watch the current namespace only. - * - * @return a Set of namespace names the associated controller will watch - */ - default Set getEffectiveNamespaces() { - var targetNamespaces = getNamespaces(); - if (watchCurrentNamespace()) { - final var parent = getConfigurationService(); - if (parent == null) { - throw new IllegalStateException( - "Parent ConfigurationService must be set before calling this method"); - } - targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); - } - return targetNamespaces; - } - default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } - ConfigurationService getConfigurationService(); - - default void setConfigurationService(ConfigurationService service) {} - default boolean useFinalizer() { - return !io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER - .equals(getFinalizer()); + return !Constants.NO_FINALIZER.equals(getFinalizer()); + } + + @Override + default ResourceEventFilter> getEventFilter() { + return ResourceConfiguration.super.getEventFilter(); } - /** - * Allow controllers to filter events before they are provided to the - * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided - * filter is combined with {@link #isGenerationAware()} to compute the final set of fiolters that - * should be applied; - * - * @return filter - */ - default ResourceEventFilter getEventFilter() { - return ResourceEventFilters.passthrough(); + default List getDependentResources() { + return Collections.emptyList(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index e8e2ef1162..97a06cdcda 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -14,7 +14,7 @@ public class ControllerConfigurationOverrider { private final Set namespaces; private RetryConfiguration retry; private String labelSelector; - private ResourceEventFilter customResourcePredicate; + private ResourceEventFilter> customResourcePredicate; private final ControllerConfiguration original; private ControllerConfigurationOverrider(ControllerConfiguration original) { @@ -69,7 +69,7 @@ public ControllerConfigurationOverrider withLabelSelector(String labelSelecto } public ControllerConfigurationOverrider withCustomResourcePredicate( - ResourceEventFilter customResourcePredicate) { + ResourceEventFilter> customResourcePredicate) { this.customResourcePredicate = customResourcePredicate; return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 860152745b..fcf2a39237 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -1,57 +1,43 @@ package io.javaoperatorsdk.operator.api.config; -import java.util.Collections; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration + extends DefaultResourceConfiguration> implements ControllerConfiguration { private final String associatedControllerClassName; private final String name; - private final String crdName; private final String finalizer; - private final boolean generationAware; - private final Set namespaces; - private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; - private final String labelSelector; - private final ResourceEventFilter resourceEventFilter; - private final Class resourceClass; - private ConfigurationService service; + private final ResourceEventFilter> resourceEventFilter; + private final boolean generationAware; public DefaultControllerConfiguration( String associatedControllerClassName, String name, - String crdName, + String resourceName, String finalizer, boolean generationAware, Set namespaces, RetryConfiguration retryConfiguration, String labelSelector, - ResourceEventFilter resourceEventFilter, + ResourceEventFilter> resourceEventFilter, Class resourceClass, ConfigurationService service) { + super(resourceName, resourceClass, namespaces, labelSelector, service); this.associatedControllerClassName = associatedControllerClassName; this.name = name; - this.crdName = crdName; this.finalizer = finalizer; this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); - this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; - this.labelSelector = labelSelector; this.resourceEventFilter = resourceEventFilter; - this.resourceClass = - resourceClass == null ? ControllerConfiguration.super.getResourceClass() - : resourceClass; - setConfigurationService(service); } @Override @@ -59,67 +45,33 @@ public String getName() { return name; } - @Override - public String getResourceTypeName() { - return crdName; - } - @Override public String getFinalizer() { return finalizer; } - @Override - public boolean isGenerationAware() { - return generationAware; - } - @Override public String getAssociatedReconcilerClassName() { return associatedControllerClassName; } - @Override - public Set getNamespaces() { - return namespaces; - } - - @Override - public boolean watchAllNamespaces() { - return watchAllNamespaces; - } - @Override public RetryConfiguration getRetryConfiguration() { return retryConfiguration; } @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - if (this.service != null) { - throw new RuntimeException("A ConfigurationService is already associated with '" + name - + "' ControllerConfiguration. Cannot change it once set!"); - } - this.service = service; - } - - @Override - public String getLabelSelector() { - return labelSelector; + public boolean isGenerationAware() { + return generationAware; } @Override - public Class getResourceClass() { - return resourceClass; + public ResourceEventFilter> getEventFilter() { + return resourceEventFilter; } @Override - public ResourceEventFilter getEventFilter() { - return resourceEventFilter; + protected String identifierForException() { + return "'" + name + "' ControllerConfiguration"; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java new file mode 100644 index 0000000000..6fe90f26f0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -0,0 +1,73 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class DefaultResourceConfiguration> + implements ResourceConfiguration { + + private final String labelSelector; + private final Class resourceClass; + private final String resourceName; + private final Set namespaces; + private final boolean watchAllNamespaces; + private ConfigurationService service; + + public DefaultResourceConfiguration(String resourceName, Class resourceClass, + Set namespaces, String labelSelector, ConfigurationService service) { + this.resourceName = resourceName; + this.namespaces = + namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); + this.watchAllNamespaces = this.namespaces.isEmpty(); + this.labelSelector = labelSelector; + this.resourceClass = + resourceClass == null ? ResourceConfiguration.super.getResourceClass() + : resourceClass; + setConfigurationService(service); + } + + @Override + public String getResourceTypeName() { + return resourceName; + } + + @Override + public Set getNamespaces() { + return namespaces; + } + + @Override + public boolean watchAllNamespaces() { + return watchAllNamespaces; + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + if (this.service != null) { + throw new RuntimeException("A ConfigurationService is already associated with " + + identifierForException() + ". Cannot change it once set!"); + } + this.service = service; + } + + protected String identifierForException() { + return getClass().getName(); + } + + @Override + public String getLabelSelector() { + return labelSelector; + } + + @Override + public Class getResourceClass() { + return resourceClass; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 62d44b66a2..ec6748e355 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -5,6 +5,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -19,7 +20,8 @@ public class ExecutorServiceManager { private final ExecutorService executor; private final int terminationTimeoutSeconds; - private ExecutorServiceManager(ExecutorService executor, int terminationTimeoutSeconds) { + private ExecutorServiceManager(InstrumentedExecutorService executor, + int terminationTimeoutSeconds) { this.executor = executor; this.terminationTimeoutSeconds = terminationTimeoutSeconds; } @@ -29,6 +31,9 @@ public static void init(ConfigurationService configuration) { instance = new ExecutorServiceManager( new InstrumentedExecutorService(configuration.getExecutorService()), configuration.getTerminationTimeoutSeconds()); + log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", + configuration.getExecutorService().getClass(), + configuration.getTerminationTimeoutSeconds()); } else { log.debug("Already started, reusing already setup instance!"); } @@ -43,10 +48,18 @@ public static void stop() { instance = null; } + public static void useTestInstance() { + if (instance == null) { + log.debug("Using test instance"); + instance = new ExecutorServiceManager( + new InstrumentedExecutorService(Executors.newFixedThreadPool(5)), 1); + } + } + public static ExecutorServiceManager instance() { if (instance == null) { throw new IllegalStateException( - "ExecutorServiceManager hasn't been started. Call start method before using!"); + "ExecutorServiceManager hasn't been started. Call init method before using!"); } return instance; } @@ -72,6 +85,9 @@ private static class InstrumentedExecutorService implements ExecutorService { private final ExecutorService executor; private InstrumentedExecutorService(ExecutorService executor) { + if (executor == null) { + throw new NullPointerException(); + } this.executor = executor; debug = Utils.debugThreadPool(); } 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 new file mode 100644 index 0000000000..985e37f916 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -0,0 +1,93 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; + +public interface ResourceConfiguration> { + + default String getResourceTypeName() { + return CustomResource.getCRDName(getResourceClass()); + } + + /** + * Retrieves the label selector that is used to filter which resources are actually watched by the + * associated event source. See + * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on + * syntax. + * + * @return the label selector filtering watched resources + */ + default String getLabelSelector() { + return null; + } + + @SuppressWarnings("unchecked") + default Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } + + default Set getNamespaces() { + return Collections.emptySet(); + } + + default boolean watchAllNamespaces() { + return allNamespacesWatched(getNamespaces()); + } + + static boolean allNamespacesWatched(Set namespaces) { + return namespaces == null || namespaces.isEmpty(); + } + + default boolean watchCurrentNamespace() { + return currentNamespaceWatched(getNamespaces()); + } + + static boolean currentNamespaceWatched(Set namespaces) { + return namespaces != null + && namespaces.size() == 1 + && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); + } + + /** + * Computes the effective namespaces based on the set specified by the user, in particular + * retrieves the current namespace from the client when the user specified that they wanted to + * watch the current namespace only. + * + * @return a Set of namespace names the associated controller will watch + */ + default Set getEffectiveNamespaces() { + var targetNamespaces = getNamespaces(); + if (watchCurrentNamespace()) { + final var parent = getConfigurationService(); + if (parent == null) { + throw new IllegalStateException( + "Parent ConfigurationService must be set before calling this method"); + } + targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); + } + return targetNamespaces; + } + + ConfigurationService getConfigurationService(); + + void setConfigurationService(ConfigurationService service); + + /** + * Allow controllers to filter events before they are provided to the + * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided + * filter is combined with {@link #isGenerationAware()} to compute the final set of filters that + * should be applied; + * + * @return filter + */ + default ResourceEventFilter getEventFilter() { + return ResourceEventFilters.passthrough(); + } + +} 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 b36c0468cd..3944cd3ecc 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; @@ -67,4 +68,9 @@ public static boolean shouldCheckCRDAndValidateLocalModel() { public static boolean debugThreadPool() { return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } + + public static Class getFirstTypeArgumentFromInterface(Class clazz) { + ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; + return (Class) type.getActualTypeArguments()[0]; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DefaultDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DefaultDependentResourceConfiguration.java new file mode 100644 index 0000000000..c7fda5f183 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DefaultDependentResourceConfiguration.java @@ -0,0 +1,68 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; + +public class DefaultDependentResourceConfiguration + extends DefaultResourceConfiguration> + implements DependentResourceConfiguration { + private final boolean creatable; + private final boolean updatable; + private final boolean owned; + private final PrimaryResourcesRetriever associatedPrimaries; + private final AssociatedSecondaryIdentifier

associatedSecondary; + private final boolean skipUpdateIfUnchanged; + + public DefaultDependentResourceConfiguration(String crdName, Class resourceClass, + Set namespaces, String labelSelector, + ConfigurationService service, boolean creatable, boolean updatable, boolean owned, + PrimaryResourcesRetriever associatedPrimaries, + AssociatedSecondaryIdentifier

associatedSecondary, boolean skipUpdateIfUnchanged) { + super(crdName, resourceClass, namespaces, labelSelector, service); + this.creatable = creatable; + this.updatable = updatable; + this.owned = owned; + this.associatedPrimaries = associatedPrimaries == null + ? DependentResourceConfiguration.super.getPrimaryResourcesRetriever() + : associatedPrimaries; + this.associatedSecondary = associatedSecondary == null + ? DependentResourceConfiguration.super.getAssociatedResourceIdentifier() + : associatedSecondary; + this.skipUpdateIfUnchanged = skipUpdateIfUnchanged; + } + + @Override + public boolean creatable() { + return creatable; + } + + @Override + public boolean updatable() { + return updatable; + } + + @Override + public boolean owned() { + return owned; + } + + @Override + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return associatedPrimaries; + } + + @Override + public AssociatedSecondaryIdentifier

getAssociatedResourceIdentifier() { + return associatedSecondary; + } + + @Override + public boolean skipUpdateIfUnchanged() { + return skipUpdateIfUnchanged; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java new file mode 100644 index 0000000000..58ff74fd57 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryRetriever; +import io.javaoperatorsdk.operator.processing.event.source.Mappers; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; + +import static io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.CREATABLE_DEFAULT; +import static io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.OWNED_DEFAULT; +import static io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.UPDATABLE_DEFAULT; + +public interface DependentResourceConfiguration + extends ResourceConfiguration> { + + default boolean creatable() { + return CREATABLE_DEFAULT; + } + + default boolean updatable() { + return UPDATABLE_DEFAULT; + } + + default boolean owned() { + return OWNED_DEFAULT; + } + + default PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return Mappers.fromOwnerReference(); + } + + default AssociatedSecondaryIdentifier

getAssociatedResourceIdentifier() { + return Mappers.sameNameAndNamespace(); + } + + default AssociatedSecondaryRetriever getAssociatedResourceRetriever() { + return (primary, registry) -> registry.getResourceEventSourceFor(getResourceClass()) + .getResourceCache() + .get(getAssociatedResourceIdentifier().associatedSecondaryID(primary, registry)) + .orElse(null); + } + + default boolean skipUpdateIfUnchanged() { + return true; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java new file mode 100644 index 0000000000..075b4e79a3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +public final class Constants { + + public static final String EMPTY_STRING = ""; + public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; + public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; + + private Constants() {} +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index bc8966f31c..c71d6324cb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,8 +2,14 @@ import java.util.Optional; -public interface Context { +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; + +public interface Context

{ Optional getRetryInfo(); + EventSourceRegistry

getEventSourceRegistry(); + + T getSecondaryResource(Class expectedType, String... qualifier); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index b5c6265ae1..9f26487b9d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -5,26 +5,24 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface ControllerConfiguration { - String EMPTY_STRING = ""; - String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; - String NO_FINALIZER = "JOSDK_NO_FINALIZER"; - - String name() default EMPTY_STRING; + String name() default Constants.EMPTY_STRING; /** * Optional finalizer name, if it is not provided, one will be automatically generated. If the - * provided value is the value specified by {@link #NO_FINALIZER}, then no finalizer will be added - * to custom resources. + * provided value is the value specified by {@link Constants#NO_FINALIZER}, then no finalizer will + * be added to custom resources. * * @return the finalizer name */ - String finalizerName() default EMPTY_STRING; + String finalizerName() default Constants.EMPTY_STRING; /** * If true, will dispatch new event to the controller if generation increased since the last @@ -50,7 +48,7 @@ * * @return the label selector */ - String labelSelector() default EMPTY_STRING; + String labelSelector() default Constants.EMPTY_STRING; /** @@ -60,4 +58,13 @@ */ @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + + /** + * Optional list of classes providing {@link DependentResource} implementations encapsulating + * logic to handle the associated {@link io.javaoperatorsdk.operator.processing.Controller}'s + * reconciliation of dependent resources + * + * @return the list of {@link DependentResource} implementations + */ + DependentResourceConfiguration[] dependents() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 8f73af29e7..009a8e1368 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -2,16 +2,38 @@ import java.util.Optional; -public class DefaultContext implements Context { +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.source.DependentResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; + +public class DefaultContext

implements Context

{ private final RetryInfo retryInfo; + private final Controller

controller; + private final P primaryResource; - public DefaultContext(RetryInfo retryInfo) { + public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; + this.controller = controller; + this.primaryResource = primaryResource; } @Override public Optional getRetryInfo() { return Optional.ofNullable(retryInfo); } + + @Override + public EventSourceRegistry

getEventSourceRegistry() { + return controller.getEventSourceRegistry(); + } + + @Override + public T getSecondaryResource(Class expectedType, + String... qualifier) { + final var eventSource = (DependentResourceEventSource) getEventSourceRegistry() + .getResourceEventSourceFor(expectedType, qualifier); + return eventSource == null ? null : eventSource.getAssociated(primaryResource); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 03eaf2b062..c0d9a3adff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; public interface EventSourceInitializer { @@ -12,6 +13,6 @@ public interface EventSourceInitializer { * @param eventSourceRegistry the {@link EventSourceRegistry} where event sources can be * registered. */ - void prepareEventSources(EventSourceRegistry eventSourceRegistry); + void prepareEventSources(EventSourceRegistry eventSourceRegistry, Cloner cloner); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 7a6977d940..4ef445d0fc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -28,7 +28,7 @@ public interface Reconciler { * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ - default DeleteControl cleanup(R resource, Context context) { + default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } @@ -46,6 +46,5 @@ default DeleteControl cleanup(R resource, Context context) { * be skipped. However we will always call an update if there is no finalizer on object * and it's not marked for deletion. */ - UpdateControl reconcile(R resource, Context context); - + UpdateControl reconcile(R resource, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java new file mode 100644 index 0000000000..010b33e6ad --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +@FunctionalInterface +public interface Builder { + R buildFor(P primary); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java new file mode 100644 index 0000000000..26a5c91a92 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DefaultDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; + +public class DependentResource + implements Builder, Updater { + + private final DefaultDependentResourceConfiguration configuration; + private final Builder builder; + private final Updater updater; + private final Fetcher fetcher; + private ResourceEventSource source; + + public DependentResource(DefaultDependentResourceConfiguration configuration, + Builder builder, Updater updater, Fetcher fetcher) { + this.configuration = configuration; + this.builder = builder; + this.updater = updater; + this.fetcher = fetcher; + } + + @Override + public R buildFor(P primary) { + return builder.buildFor(primary); + } + + public ResourceCache getCache() { + return source.getResourceCache(); + } + + public R fetchFor(HasMetadata owner) { + return fetcher != null ? fetcher.fetchFor(owner, getCache()) + : getCache().get(ResourceID.fromResource(owner)).orElse(null); + } + + public DependentResourceConfiguration getConfiguration() { + return configuration; + } + + @Override + public R update(R fetched, P primary) { + return updater.update(fetched, primary); + } + + public void setSource(ResourceEventSource source) { + this.source = source; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java new file mode 100644 index 0000000000..8492e3e2ba --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java @@ -0,0 +1,110 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; + +public @interface DependentResourceConfiguration { + boolean CREATABLE_DEFAULT = true; + boolean UPDATABLE_DEFAULT = false; + boolean OWNED_DEFAULT = true; + boolean SKIP_UPDATE_DEFAULT = true; + + boolean creatable() default CREATABLE_DEFAULT; + + boolean updatable() default UPDATABLE_DEFAULT; + + boolean owned() default OWNED_DEFAULT; + + Class resourceType(); + + Class builder() default DEFAULT_BUILDER.class; + + Class updater() default DEFAULT_UPDATER.class; + + Class fetcher() default DEFAULT_FETCHER.class; + + Class associatedPrimariesRetriever() default DEFAULT_PRIMARIES_RETRIEVER.class; + + Class associatedSecondaryIdentifier() default DEFAULT_SECONDARY_IDENTIFIER.class; + + boolean skipUpdateIfUnchanged() default SKIP_UPDATE_DEFAULT; + + /** + * Specified which namespaces this Controller monitors for custom resources events. If no + * namespace is specified then the controller will monitor all namespaces by default. + * + * @return the list of namespaces this controller monitors + */ + String[] namespaces() default {}; + + /** + * Optional label selector used to identify the set of custom resources the controller will acc + * upon. The label selector can be made of multiple comma separated requirements that acts as a + * logical AND operator. + * + * @return the label selector + */ + String labelSelector() default EMPTY_STRING; + + + /** + * Optional list of classes providing custom {@link ResourceEventFilter}. + * + * @return the list of event filters. + */ + @SuppressWarnings("rawtypes") + Class[] eventFilters() default {}; + + + final class DEFAULT_BUILDER implements Builder { + + @Override + public HasMetadata buildFor(HasMetadata primary) { + return null; + } + } + + final class DEFAULT_UPDATER implements Updater { + + @Override + public HasMetadata update(HasMetadata fetched, HasMetadata primary) { + return null; + } + } + + final class DEFAULT_FETCHER implements Fetcher { + + @Override + public HasMetadata fetchFor(HasMetadata owner, ResourceCache cache) { + return null; + } + } + + final class DEFAULT_PRIMARIES_RETRIEVER + implements PrimaryResourcesRetriever { + + @Override + public Set associatedPrimaryResources(HasMetadata dependentResource, + EventSourceRegistry registry) { + return null; + } + } + + final class DEFAULT_SECONDARY_IDENTIFIER implements AssociatedSecondaryIdentifier { + + @Override + public ResourceID associatedSecondaryID(HasMetadata primary, + EventSourceRegistry registry) { + return null; + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java new file mode 100644 index 0000000000..226df844e8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; + +@FunctionalInterface +public interface Fetcher { + Fetcher DEFAULT = + (owner, cache) -> cache.get(ResourceID.fromResource(owner)).orElse(null); + + @SuppressWarnings("unchecked") + static Fetcher defaultFetcher() { + return (Fetcher) DEFAULT; + } + + R fetchFor(HasMetadata owner, ResourceCache cache); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java new file mode 100644 index 0000000000..f3e27ff884 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +@FunctionalInterface +public interface Updater { + + R update(R fetched, P primary); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index c8c96cfd6a..93a224ad15 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -1,17 +1,27 @@ package io.javaoperatorsdk.operator.processing; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -19,14 +29,21 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.DependentResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceWrapper; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { + + private static final Logger log = LoggerFactory.getLogger(Controller.class); private final Reconciler reconciler; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private EventSourceManager eventSourceManager; + private final EventSourceManager eventSourceManager; + private final AtomicBoolean started = new AtomicBoolean(false); public Controller(Reconciler reconciler, ControllerConfiguration configuration, @@ -34,10 +51,32 @@ public Controller(Reconciler reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + + eventSourceManager = new EventSourceManager<>(this); + prepareEventSources(eventSourceManager, + configuration.getConfigurationService().getResourceCloner()); + } + + private void waitUntilStarted() { + if (!started.get()) { + AtomicInteger count = new AtomicInteger(0); + final var waitTime = 50; + while (!started.get()) { + try { + count.getAndIncrement(); + Thread.sleep(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + log.info("Waited {}ms for controller '{}' to finish initializing", count.get() * waitTime, + configuration.getName()); + } } @Override - public DeleteControl cleanup(R resource, Context context) { + public DeleteControl cleanup(R resource, Context context) { + waitUntilStarted(); return configuration.getConfigurationService().getMetrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -63,8 +102,31 @@ public DeleteControl execute() { } @Override - public UpdateControl reconcile(R resource, Context context) { - return configuration.getConfigurationService().getMetrics().timeControllerExecution( + public UpdateControl reconcile(R resource, Context context) { + waitUntilStarted(); + final var metrics = configuration.getConfigurationService().getMetrics(); + + configuration.getDependentResources().forEach(dependent -> { + final var conf = dependent.getConfiguration(); + + if (!conf.creatable() && !conf.updatable()) { + return; + } + + var dependentResource = dependent.fetchFor(resource); + if (conf.creatable() && dependentResource == null) { + // we need to create the dependent + dependentResource = dependent.buildFor(resource); + createOrReplaceDependent(resource, conf, dependentResource, "Creating"); + } else if (conf.updatable()) { + dependentResource = dependent.update(dependentResource, resource); + createOrReplaceDependent(resource, conf, dependentResource, "Updating"); + } else { + logOperationInfo(resource, conf, dependentResource, "Ignoring"); + } + }); + + return metrics.timeControllerExecution( new ControllerExecution<>() { @Override public String name() { @@ -95,9 +157,69 @@ public UpdateControl execute() { }); } + private void createOrReplaceDependent(R resource, DependentResourceConfiguration conf, + HasMetadata dependentResource, String operationDescription) { + addOwnerReferenceIfNeeded(resource, conf, dependentResource); + logOperationInfo(resource, conf, dependentResource, operationDescription); + // send the changes to the cluster + // todo: would be nice to be able to update informer directly… + // todo: add metrics timing for dependent resource + kubernetesClient.resource(dependentResource).createOrReplace(); + } + + private void logOperationInfo(R resource, DependentResourceConfiguration conf, + HasMetadata dependentResource, String operationDescription) { + log.info(operationDescription + " '{}' {} dependent in namespace {} for '{}' {}", + dependentResource.getMetadata().getName(), conf.getResourceTypeName(), + dependentResource.getMetadata().getNamespace(), resource.getMetadata().getName(), + configuration.getResourceTypeName()); + } + + private void addOwnerReferenceIfNeeded(R resource, DependentResourceConfiguration conf, + HasMetadata dependentResource) { + // todo: use owner reference support from fabric8 client to avoid adding several times the same + // reference + if (conf.owned()) { + final var metadata = resource.getMetadata(); + final var updatedMetadata = new ObjectMetaBuilder(dependentResource.getMetadata()) + .addNewOwnerReference() + .withUid(metadata.getUid()) + .withApiVersion(resource.getApiVersion()) + .withName(metadata.getName()) + .withKind(resource.getKind()) + .endOwnerReference().build(); + dependentResource.setMetadata(updatedMetadata); + } + } + @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - throw new UnsupportedOperationException("This method should never be called directly"); + public void prepareEventSources(EventSourceRegistry eventSourceRegistry, Cloner cloner) { + configuration.getDependentResources().forEach(dependent -> { + final var dependentConfiguration = dependent.getConfiguration(); + new AbstractResourceEventSource<>(dependentConfiguration, + kubernetesClient.resources(dependentConfiguration.getResourceClass()), cloner, + eventSourceRegistry) { + @Override + protected ResourceEventFilter initFilter(ResourceConfiguration configuration) { + return configuration.getEventFilter(); + } + + @Override + protected EventSourceWrapper wrapEventSource( + FilterWatchListDeletable filteredBySelectorClient, Cloner cloner) { + final var dependentSource = new DependentResourceEventSource(filteredBySelectorClient, + cloner, dependentConfiguration); + // make sure we're set to receive events + eventSourceRegistry.registerEventSource(dependentSource); + dependent.setSource(dependentSource); + return dependentSource; + } + }; + }); + + if (reconciler instanceof EventSourceInitializer) { + ((EventSourceInitializer) reconciler).prepareEventSources(eventSourceManager, cloner); + } } @Override @@ -152,10 +274,13 @@ public void start() throws OperatorException { final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; + log.info("Starting '{}' controller for reconciler: {}, resource: {}", controllerName, + reconciler.getClass().getCanonicalName(), resClass.getCanonicalName()); + final var configurationService = configuration.getConfigurationService(); try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { + if (configurationService.checkCRDAndValidateLocalModel()) { crd = kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) .get(); @@ -167,10 +292,6 @@ public void start() throws OperatorException { CustomResourceUtils.assertCustomResource(resClass, crd); } - eventSourceManager = new EventSourceManager<>(this); - if (reconciler instanceof EventSourceInitializer) { - ((EventSourceInitializer) reconciler).prepareEventSources(eventSourceManager); - } if (failOnMissingCurrentNS()) { throw new OperatorException( "Controller '" @@ -178,6 +299,8 @@ public void start() throws OperatorException { + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } eventSourceManager.start(); + started.set(true); + log.info("'{}' controller started, pending event sources initialization", controllerName); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); } @@ -220,5 +343,10 @@ public void stop() { if (eventSourceManager != null) { eventSourceManager.stop(); } + started.set(false); + } + + public EventSourceRegistry getEventSourceRegistry() { + return eventSourceManager; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 1f0161aeef..a9079119d3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -45,18 +45,17 @@ class EventProcessor implements EventHandler, LifecycleAw private final Map retryState = new HashMap<>(); private final ExecutorService executor; private final String controllerName; + private final String resourceType; private final ReentrantLock lock = new ReentrantLock(); private final Metrics metrics; private volatile boolean running; - private final ResourceCache resourceCache; private final EventSourceManager eventSourceManager; private final EventMarker eventMarker = new EventMarker(); EventProcessor(EventSourceManager eventSourceManager) { - this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), - ExecutorServiceManager.instance().executorService(), + this(ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), + eventSourceManager.getController().getConfiguration().getResourceTypeName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), GenericRetry.fromConfiguration( eventSourceManager.getController().getConfiguration().getRetryConfiguration()), @@ -66,19 +65,14 @@ class EventProcessor implements EventHandler, LifecycleAw } EventProcessor(ReconciliationDispatcher reconciliationDispatcher, - EventSourceManager eventSourceManager, - String relatedControllerName, - Retry retry) { - this(eventSourceManager.getControllerResourceEventSource().getResourceCache(), null, - relatedControllerName, - reconciliationDispatcher, retry, null, eventSourceManager); + EventSourceManager eventSourceManager, Retry retry) { + this(null, "Test", "Test", reconciliationDispatcher, retry, null, eventSourceManager); } - private EventProcessor(ResourceCache resourceCache, ExecutorService executor, - String relatedControllerName, + private EventProcessor(ExecutorService executor, + String relatedControllerName, String resourceTypeName, ReconciliationDispatcher reconciliationDispatcher, Retry retry, Metrics metrics, EventSourceManager eventSourceManager) { - this.running = true; this.executor = executor == null ? new ScheduledThreadPoolExecutor( @@ -87,9 +81,9 @@ private EventProcessor(ResourceCache resourceCache, ExecutorService executor, this.controllerName = relatedControllerName; this.reconciliationDispatcher = reconciliationDispatcher; this.retry = retry; - this.resourceCache = resourceCache; this.metrics = metrics != null ? metrics : Metrics.NOOP; this.eventSourceManager = eventSourceManager; + this.resourceType = resourceTypeName; } EventMarker getEventMarker() { @@ -102,7 +96,7 @@ public void handleEvent(Event event) { try { log.debug("Received event: {}", event); if (!this.running) { - log.debug("Skipping event: {} because the event handler is not started", event); + log.debug("Skipping event: {} because the event processor is not started", event); return; } final var resourceID = event.getRelatedCustomResourceID(); @@ -121,10 +115,14 @@ public void handleEvent(Event event) { } } + ResourceCache resourceCache() { + return eventSourceManager.getControllerResourceEventSource().getResourceCache(); + } + private void submitReconciliationExecution(ResourceID resourceID) { try { boolean controllerUnderExecution = isControllerUnderExecution(resourceID); - Optional latest = resourceCache.get(resourceID); + Optional latest = resourceCache().get(resourceID); latest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution && latest.isPresent()) { setUnderExecutionProcessing(resourceID); @@ -141,7 +139,7 @@ private void submitReconciliationExecution(ResourceID resourceID) { controllerUnderExecution, latest.isPresent()); if (latest.isEmpty()) { - log.warn("no custom resource found in cache for ResourceID: {}", resourceID); + log.warn("No {} resource found in cache for ResourceID: {}", resourceType, resourceID); } } } finally { @@ -220,7 +218,7 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution .getUpdatedCustomResource() .orElseThrow(() -> new IllegalStateException( "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = getVersion(resourceCache + String cachedCustomResourceVersion = getVersion(resourceCache() .get(executionScope.getCustomResourceID()) .orElseThrow(() -> new IllegalStateException( "Cached custom resource must be present at this point"))); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 6848fcbb37..8bceba4a4c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -1,6 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.*; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; @@ -13,6 +16,7 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; @@ -24,10 +28,8 @@ public class EventSourceManager private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); private final ReentrantLock lock = new ReentrantLock(); - // This needs to be a list since the event source must be started in a deterministic order. The - // controllerResourceEventSource must be always the first to have informers available for other - // informers to access the main controller cache. - private final List eventSources = Collections.synchronizedList(new ArrayList<>()); + private final ConcurrentNavigableMap> eventSources = + new ConcurrentSkipListMap<>(); private final EventProcessor eventProcessor; private TimerEventSource retryAndRescheduleTimerEventSource; private ControllerResourceEventSource controllerResourceEventSource; @@ -41,10 +43,11 @@ public class EventSourceManager public EventSourceManager(Controller controller) { this.controller = controller; - controllerResourceEventSource = new ControllerResourceEventSource<>(controller); + initRetryEventSource(); + + controllerResourceEventSource = new ControllerResourceEventSource<>(controller, this); this.eventProcessor = new EventProcessor<>(this); registerEventSource(controllerResourceEventSource); - initRetryEventSource(); } private void initRetryEventSource() { @@ -52,17 +55,27 @@ private void initRetryEventSource() { registerEventSource(retryAndRescheduleTimerEventSource); } + @Override + public EventHandler getEventHandler() { + return eventProcessor; + } + @Override public void start() throws OperatorException { eventProcessor.start(); lock.lock(); try { - log.debug("Starting event sources."); - for (var eventSource : eventSources) { + for (var eventSource : eventSources.values()) { try { + log.debug("Starting source {} for {}", eventSource.getClass(), + eventSource.getResourceClass()); eventSource.start(); + log.debug("Source {} started", eventSource.getClass()); } catch (Exception e) { - log.warn("Error starting {} -> {}", eventSource, e); + if (e instanceof MissingCRDException) { + throw e; + } + throw new OperatorException("Couldn't start source " + eventSource, e); } } } finally { @@ -75,7 +88,7 @@ public void stop() { lock.lock(); try { log.debug("Closing event sources."); - for (var eventSource : eventSources) { + for (var eventSource : eventSources.values()) { try { eventSource.stop(); } catch (Exception e) { @@ -95,8 +108,8 @@ public final void registerEventSource(EventSource eventSource) Objects.requireNonNull(eventSource, "EventSource must not be null"); lock.lock(); try { - eventSources.add(eventSource); - eventSource.setEventHandler(eventProcessor); + eventSources.put(keyFor(eventSource), eventSource); + eventSource.setEventRegistry(this); } catch (Throwable e) { if (e instanceof IllegalStateException || e instanceof MissingCRDException) { // leave untouched @@ -109,8 +122,41 @@ public final void registerEventSource(EventSource eventSource) } } + private String keyFor(EventSource source) { + String name; + if (source instanceof ResourceEventSource) { + ResourceEventSource resourceEventSource = (ResourceEventSource) source; + final var configuration = resourceEventSource.getConfiguration(); + // todo: extract qualifier from configuration + name = keyFor(configuration.getResourceClass()); + } else { + name = keyFor(source.getResourceClass()); + } + return name; + } + + private String keyFor(Class dependentType, String... qualifier) { + final var className = dependentType.getCanonicalName(); + var key = className; + if (qualifier != null && qualifier.length > 0) { + key += "-" + qualifier[0]; + } + + // make sure timer event source is started first, then controller event source + // this is needed so that these sources are set when informer sources start so that events can + // properly be processed + if (controllerResourceEventSource != null + && className.equals(controllerResourceEventSource.getResourceClass().getCanonicalName())) { + key = 1 + key; + } else if (retryAndRescheduleTimerEventSource != null && className + .equals(retryAndRescheduleTimerEventSource.getResourceClass().getCanonicalName())) { + key = 0 + key; + } + return key; + } + public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { - for (EventSource eventSource : this.eventSources) { + for (EventSource eventSource : this.eventSources.values()) { if (eventSource instanceof ResourceEventAware) { var lifecycleAwareES = ((ResourceEventAware) eventSource); switch (action) { @@ -129,8 +175,8 @@ public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldRes } @Override - public Set getRegisteredEventSources() { - return new HashSet<>(eventSources); + public Set> getRegisteredEventSources() { + return Set.copyOf(eventSources.values()); } @Override @@ -138,6 +184,38 @@ public ControllerResourceEventSource getControllerResourceEventSource() { return controllerResourceEventSource; } + @Override + public ResourceEventSource getResourceEventSourceFor( + Class dependentType, + String... qualifier) { + final var eventSource = eventSources.get(keyFor(dependentType, qualifier)); + if (eventSource == null) { + return null; + } + if (!(eventSource instanceof ResourceEventSource)) { + throw new IllegalArgumentException(eventSource + " associated with " + + keyAsString(dependentType, qualifier) + " is not a " + + ResourceEventSource.class.getSimpleName()); + } + final var source = (ResourceEventSource) eventSource; + final var configuration = source.getConfiguration(); + final var resourceClass = configuration.getResourceClass(); + if (!resourceClass.isAssignableFrom(dependentType)) { + throw new IllegalArgumentException(eventSource + " associated with " + + keyAsString(dependentType, qualifier) + + " is handling " + resourceClass.getName() + " resources but asked for " + + dependentType.getName()); + } + return (ResourceEventSource) eventSource; + } + + @SuppressWarnings("rawtypes") + private String keyAsString(Class dependentType, String... qualifier) { + return qualifier != null && qualifier.length > 0 + ? "(" + dependentType.getName() + ", " + qualifier[0] + ")" + : dependentType.getName(); + } + TimerEventSource retryEventSource() { return retryAndRescheduleTimerEventSource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 314698fd71..026d5ea463 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -28,8 +28,7 @@ class ReconciliationDispatcher { private final Controller controller; private final CustomResourceFacade customResourceFacade; - ReconciliationDispatcher(Controller controller, - CustomResourceFacade customResourceFacade) { + ReconciliationDispatcher(Controller controller, CustomResourceFacade customResourceFacade) { this.controller = controller; this.customResourceFacade = customResourceFacade; } @@ -67,8 +66,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = - new DefaultContext(executionScope.getRetryInfo()); + Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); if (markedForDeletion) { return handleCleanup(resource, context); } else { @@ -91,7 +89,7 @@ private boolean shouldNotDispatchToDelete(R resource) { } private PostExecutionControl handleReconcile( - ExecutionScope executionScope, R originalResource, Context context) { + ExecutionScope executionScope, R originalResource, Context context) { if (configuration().useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizer())) { /* @@ -121,7 +119,7 @@ private PostExecutionControl handleReconcile( * resource is changed during an execution, and it's much cleaner to have to original resource in * place for status update. */ - private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { + private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { return configuration().getConfigurationService().getResourceCloner().clone(resource); @@ -131,7 +129,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } private PostExecutionControl reconcileExecution(ExecutionScope executionScope, - R resourceForExecution, R originalResource, Context context) { + R resourceForExecution, R originalResource, Context context) { log.debug( "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", getName(resourceForExecution), @@ -161,7 +159,7 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } - private void handleErrorStatusHandler(R resource, Context context, + private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { if (isErrorStatusHandlerPresent()) { try { @@ -238,7 +236,7 @@ private void updatePostExecutionControlWithReschedule( baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); } - private PostExecutionControl handleCleanup(R resource, Context context) { + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index ddc787ad2d..8b23a3e878 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -1,14 +1,34 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.EventHandler; -public abstract class AbstractEventSource implements EventSource { +public abstract class AbstractEventSource

implements EventSource

{ - protected volatile EventHandler eventHandler; + private volatile EventSourceRegistry

eventSourceRegistry; + private final Class resourceClass; + + protected AbstractEventSource(Class resourceClass) { + this.resourceClass = resourceClass; + } + + @Override + public Class getResourceClass() { + return resourceClass; + } + + protected EventHandler getEventHandler() { + return eventSourceRegistry.getEventHandler(); + } + + @Override + public void setEventRegistry(EventSourceRegistry

registry) { + this.eventSourceRegistry = registry; + } @Override - public void setEventHandler(EventHandler eventHandler) { - this.eventHandler = eventHandler; + public EventSourceRegistry

getEventRegistry() { + return eventSourceRegistry; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java new file mode 100644 index 0000000000..1458e530b6 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java @@ -0,0 +1,139 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + +public abstract class AbstractResourceEventSource, V extends EventSourceWrapper, P extends HasMetadata> + extends AbstractEventSource

+ implements ResourceEventHandler { + + private static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; + private static final Logger log = LoggerFactory.getLogger(AbstractResourceEventSource.class); + + private final Map sources = new ConcurrentHashMap<>(); + private final ResourceEventFilter filter; + private final U configuration; + private final MixedOperation, Resource> client; + private final Cloner cloner; + private final ResourceCache cache; + + public AbstractResourceEventSource(U configuration, + MixedOperation, Resource> client, Cloner cloner, + EventSourceRegistry

registry) { + super(configuration.getResourceClass()); + this.configuration = configuration; + this.client = client; + this.filter = initFilter(configuration); + this.cloner = cloner; + setEventRegistry(registry); + + initSources(); + + this.cache = new AggregateResourceCache<>(sources); + } + + public U getConfiguration() { + return configuration; + } + + protected abstract ResourceEventFilter initFilter(U configuration); + + protected abstract V wrapEventSource( + FilterWatchListDeletable> filteredBySelectorClient, + Cloner cloner); + + void eventReceived(ResourceAction action, T resource, T oldResource) { + log.debug("Event received for resource: {}", getName(resource)); + if (filter.acceptChange(configuration, oldResource, resource)) { + getEventHandler().handleEvent(new ResourceEvent(action, ResourceID.fromResource(resource))); + } else { + log.debug( + "Skipping event handling resource {} with version: {}", + getUID(resource), + getVersion(resource)); + } + } + + @Override + public void onAdd(T resource) { + eventReceived(ResourceAction.ADDED, resource, null); + } + + @Override + public void onUpdate(T oldResource, T newResource) { + eventReceived(ResourceAction.UPDATED, newResource, oldResource); + } + + @Override + public void onDelete(T resource, boolean b) { + eventReceived(ResourceAction.DELETED, resource, null); + } + + @Override + public void start() throws OperatorException { + sources.values().parallelStream().forEach(LifecycleAware::start); + } + + private void initSources() { + final var targetNamespaces = configuration.getEffectiveNamespaces(); + final var labelSelector = configuration.getLabelSelector(); + + if (ResourceConfiguration.allNamespacesWatched(targetNamespaces)) { + final var filteredBySelectorClient = + client.inAnyNamespace().withLabelSelector(labelSelector); + final var source = createEventSource(filteredBySelectorClient, ANY_NAMESPACE_MAP_KEY); + log.debug("Registered {} -> {} for any namespace", this, source); + } else { + targetNamespaces.forEach( + ns -> { + final var source = + createEventSource(client.inNamespace(ns).withLabelSelector(labelSelector), ns); + log.debug("Registered {} -> {} for namespace: {}", this, source, + ns); + }); + } + } + + + private V createEventSource( + FilterWatchListDeletable> filteredBySelectorClient, String key) { + final var source = wrapEventSource(filteredBySelectorClient, cloner); + sources.put(key, source); + return source; + } + + @Override + public void stop() { + for (V source : sources.values()) { + try { + log.info("Stopping informer {} -> {}", this, source); + source.stop(); + } catch (Exception e) { + log.warn("Error stopping informer {} -> {}", this, source, e); + } + } + } + + public ResourceCache getResourceCache() { + return cache; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java new file mode 100644 index 0000000000..05204bc4bd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; + +public class AggregateResourceCache> + implements ResourceCache { + + private final Map sources; + + public AggregateResourceCache(Map sources) { + this.sources = sources; + } + + @Override + public Stream list(Predicate predicate) { + if (predicate == null) { + return sources.values().stream().flatMap(ResourceCache::list); + } + return sources.values().stream().flatMap(i -> i.list(predicate)); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + if (isWatchingAllNamespaces()) { + return getSource(ANY_NAMESPACE_MAP_KEY) + .map(source -> source.list(namespace, predicate)) + .orElse(Stream.empty()); + } else { + return getSource(namespace) + .map(source -> source.list(predicate)) + .orElse(Stream.empty()); + } + } + + @Override + public Optional get(ResourceID resourceID) { + return getSource(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .flatMap(source -> source.get(resourceID)); + } + + private boolean isWatchingAllNamespaces() { + return sources.containsKey(ANY_NAMESPACE_MAP_KEY); + } + + Optional getSource(String namespace) { + namespace = isWatchingAllNamespaces() || namespace == null ? ANY_NAMESPACE_MAP_KEY : namespace; + return Optional.ofNullable(sources.get(namespace)); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryIdentifier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryIdentifier.java new file mode 100644 index 0000000000..ddc1a51666 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryIdentifier.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@FunctionalInterface +public interface AssociatedSecondaryIdentifier

{ + ResourceID associatedSecondaryID(P primary, EventSourceRegistry

registry); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryRetriever.java new file mode 100644 index 0000000000..05bad8bfdb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryRetriever.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +@FunctionalInterface +public interface AssociatedSecondaryRetriever { + T associatedSecondary(P primary, EventSourceRegistry

registry); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java new file mode 100644 index 0000000000..168a309bbd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class DependentResourceEventSource + extends InformerEventSource + implements EventSourceWrapper, ResourceEventSource { + private final DependentResourceConfiguration configuration; + + public DependentResourceEventSource( + FilterWatchListDeletable> client, + Cloner cloner, DependentResourceConfiguration dependentConfiguration) { + super(client.runnableInformer(0), + dependentConfiguration.getPrimaryResourcesRetriever(), + dependentConfiguration.getAssociatedResourceRetriever(), + dependentConfiguration.skipUpdateIfUnchanged(), cloner); + this.configuration = dependentConfiguration; + } + + @Override + public Optional get(ResourceID resourceID) { + return getCache().get(resourceID); + } + + @Override + public Stream list(Predicate predicate) { + return getCache().list(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return getCache().list(namespace, predicate); + } + + @Override + public ResourceCache getResourceCache() { + return getCache(); + } + + @Override + public ResourceConfiguration getConfiguration() { + return configuration; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java index 18e47d03db..3d070c7180 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java @@ -1,10 +1,13 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.LifecycleAware; -import io.javaoperatorsdk.operator.processing.event.EventHandler; -public interface EventSource extends LifecycleAware { +public interface EventSource

extends LifecycleAware { - void setEventHandler(EventHandler eventHandler); + void setEventRegistry(EventSourceRegistry

registry); + EventSourceRegistry

getEventRegistry(); + + Class getResourceClass(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java index dca5436427..5538d9446a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.EventHandler; public interface EventSourceRegistry { @@ -16,11 +17,16 @@ public interface EventSourceRegistry { * registered. * @throws OperatorException if an error occurred during the registration process */ - void registerEventSource(EventSource eventSource) + void registerEventSource(EventSource eventSource) throws IllegalStateException, OperatorException; - Set getRegisteredEventSources(); + Set> getRegisteredEventSources(); ControllerResourceEventSource getControllerResourceEventSource(); + ResourceEventSource getResourceEventSourceFor( + Class dependentType, + String... qualifier); + + EventHandler getEventHandler(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java new file mode 100644 index 0000000000..239247ec84 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.LifecycleAware; + +public interface EventSourceWrapper + extends LifecycleAware, ResourceCache { +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java new file mode 100644 index 0000000000..ae6ce08163 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +class InformerEventSourceWrapper implements EventSourceWrapper { + + private final SharedIndexInformer informer; + private final InformerResourceCache cache; + + InformerEventSourceWrapper(SharedIndexInformer informer, Cloner cloner, + ResourceEventHandler parent) { + this.informer = informer; + this.informer.addEventHandler(parent); + this.cache = new InformerResourceCache<>(informer, cloner); + } + + @Override + public void start() throws OperatorException { + informer.run(); + } + + @Override + public void stop() throws OperatorException { + informer.stop(); + } + + @Override + public Optional get(ResourceID resourceID) { + return cache.get(resourceID); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return cache.list(namespace, predicate); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java new file mode 100644 index 0000000000..8dc1c7c6b1 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class InformerResourceCache implements ResourceCache { + + private final SharedInformer informer; + private final Cloner cloner; + + public InformerResourceCache(SharedInformer informer, Cloner cloner) { + this.informer = informer; + this.cloner = cloner; + } + + @Override + public Optional get(ResourceID resourceID) { + final var resource = informer.getStore().getByKey( + Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName())); + return Optional.ofNullable(cloner.clone(resource)); + } + + @Override + public Stream list(Predicate predicate) { + return informer.getStore().list().stream().filter(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + final var stream = informer.getStore().list().stream() + .filter(r -> namespace.equals(r.getMetadata().getNamespace())); + return predicate != null ? stream.filter(predicate) : stream; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java new file mode 100644 index 0000000000..805bea3c63 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@FunctionalInterface +public interface PrimaryResourcesRetriever { + Set associatedPrimaryResources(T dependentResource, EventSourceRegistry

registry); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java new file mode 100644 index 0000000000..7641e25312 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; + +public interface ResourceEventSource + extends EventSource

{ + ResourceCache getResourceCache(); + + ResourceConfiguration getConfiguration(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 2386cd4dbc..39a6aab9d1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -1,92 +1,70 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; -import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; - /** * This is a special case since is not bound to a single custom resource */ -public class ControllerResourceEventSource extends AbstractEventSource - implements ResourceEventHandler { +public class ControllerResourceEventSource + extends + AbstractResourceEventSource, InformerEventSourceWrapper, T> { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; - private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); - private final Controller controller; - private final Map> sharedIndexInformers = - new ConcurrentHashMap<>(); - - private final ResourceEventFilter filter; - private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - private final ControllerResourceCache cache; + private OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - public ControllerResourceEventSource(Controller controller) { + public ControllerResourceEventSource(Controller controller, EventSourceRegistry registry) { + super(controller.getConfiguration(), + controller.getCRClient(), + controller.getConfiguration().getConfigurationService().getResourceCloner(), + registry); this.controller = controller; - var cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); - this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); + } + @Override + protected ResourceEventFilter> initFilter( + ControllerConfiguration configuration) { var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), ResourceEventFilters.and( - controller.getConfiguration().getEventFilter(), + configuration.getEventFilter(), ResourceEventFilters.generationAware()), null }; - if (controller.getConfiguration().isGenerationAware()) { + if (configuration.isGenerationAware()) { onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; } else { onceWhitelistEventFilterEventFilter = null; } - filter = ResourceEventFilters.or(filters); + return ResourceEventFilters.or(filters); } @Override - public void start() { - final var configuration = controller.getConfiguration(); - final var targetNamespaces = configuration.getEffectiveNamespaces(); - final var client = controller.getCRClient(); - final var labelSelector = configuration.getLabelSelector(); + protected InformerEventSourceWrapper wrapEventSource( + FilterWatchListDeletable> filteredBySelectorClient, + Cloner cloner) { + return new InformerEventSourceWrapper<>(filteredBySelectorClient.runnableInformer(0), cloner, + this); + } + @Override + public void start() { try { - if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - final var informer = - createAndRunInformerFor(client.inAnyNamespace() - .withLabelSelector(labelSelector), ANY_NAMESPACE_MAP_KEY); - log.debug("Registered {} -> {} for any namespace", controller, informer); - } else { - targetNamespaces.forEach(ns -> { - final var informer = createAndRunInformerFor( - client.inNamespace(ns).withLabelSelector(labelSelector), ns); - log.debug("Registered {} -> {} for namespace: {}", controller, informer, ns); - }); - } + super.start(); } catch (Exception e) { if (e instanceof KubernetesClientException) { handleKubernetesClientException(e); @@ -95,83 +73,16 @@ public void start() { } } - private SharedIndexInformer createAndRunInformerFor( - FilterWatchListDeletable> filteredBySelectorClient, String key) { - var informer = filteredBySelectorClient.runnableInformer(0); - informer.addEventHandler(this); - sharedIndexInformers.put(key, informer); - informer.run(); - return informer; - } - - @Override - public void stop() { - for (SharedIndexInformer informer : sharedIndexInformers.values()) { - try { - log.info("Stopping informer {} -> {}", controller, informer); - informer.stop(); - } catch (Exception e) { - log.warn("Error stopping informer {} -> {}", controller, informer, e); - } - } - } - - public void eventReceived(ResourceAction action, T customResource, T oldResource) { + public void eventReceived(ResourceAction action, T resource, T oldResource) { try { - log.debug( - "Event received for resource: {}", getName(customResource)); - MDCUtils.addResourceInfo(customResource); - controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, - oldResource); - if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { - eventHandler.handleEvent( - new ResourceEvent(action, ResourceID.fromResource(customResource))); - } else { - log.debug( - "Skipping event handling resource {} with version: {}", - getUID(customResource), - getVersion(customResource)); - } + MDCUtils.addResourceInfo(resource); + controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); + super.eventReceived(action, resource, oldResource); } finally { MDCUtils.removeResourceInfo(); } } - @Override - public void onAdd(T resource) { - eventReceived(ResourceAction.ADDED, resource, null); - } - - @Override - public void onUpdate(T oldCustomResource, T newCustomResource) { - eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); - } - - @Override - public void onDelete(T resource, boolean b) { - eventReceived(ResourceAction.DELETED, resource, null); - } - - public Optional get(ResourceID resourceID) { - return cache.get(resourceID); - } - - public ControllerResourceCache getResourceCache() { - return cache; - } - - /** - * @return shared informers by namespace. If custom resource is not namespace scoped use - * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY - */ - public Map> getInformers() { - return Collections.unmodifiableMap(sharedIndexInformers); - } - - public SharedIndexInformer getInformer(String namespace) { - return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); - } - /** * This will ensure that the next event received after this method is called will not be filtered * out. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java index 8262ff1c21..ff0d65685b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java @@ -11,7 +11,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; public class OnceWhitelistEventFilterEventFilter - implements ResourceEventFilter { + implements ResourceEventFilter> { private static final Logger log = LoggerFactory.getLogger(OnceWhitelistEventFilterEventFilter.class); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java index ad1d85330c..ed92fb91ba 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java @@ -7,17 +7,14 @@ public class ResourceEvent extends Event { private final ResourceAction action; - public ResourceEvent(ResourceAction action, - ResourceID resourceID) { + public ResourceEvent(ResourceAction action, ResourceID resourceID) { super(resourceID); this.action = action; } @Override public String toString() { - return "CustomResourceEvent{" + - "action=" + action + - '}'; + return "ResourceEvent{action=" + action + '}'; } public ResourceAction getAction() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java index 497c9016b7..ff2c0c5fa6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; /** * A functional interface to determine whether resource events should be processed by the SDK. This @@ -11,7 +11,7 @@ * @param the type of custom resources handled by this filter */ @FunctionalInterface -public interface ResourceEventFilter { +public interface ResourceEventFilter> { /** * Determines whether the change between the old version of the resource and the new one needs to @@ -23,7 +23,7 @@ public interface ResourceEventFilter { * @return {@code true} if the change needs to be propagated to the controller, {@code false} * otherwise */ - boolean acceptChange(ControllerConfiguration configuration, T oldResource, T newResource); + boolean acceptChange(U configuration, T oldResource, T newResource); /** * Combines this filter with the provided one with an AND logic, i.e. the resulting filter will @@ -32,12 +32,11 @@ public interface ResourceEventFilter { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the AND logic between this and the provided filter */ - default ResourceEventFilter and(ResourceEventFilter other) { - return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result && other.acceptChange(configuration, oldResource, newResource); - }; + default ResourceEventFilter and(ResourceEventFilter other) { + return other == null ? this : (U configuration, T oldResource, T newResource) -> { + boolean result = acceptChange(configuration, oldResource, newResource); + return result && other.acceptChange(configuration, oldResource, newResource); + }; } /** @@ -48,11 +47,10 @@ default ResourceEventFilter and(ResourceEventFilter other) { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the OR logic between this and the provided filter */ - default ResourceEventFilter or(ResourceEventFilter other) { - return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result || other.acceptChange(configuration, oldResource, newResource); - }; + default ResourceEventFilter or(ResourceEventFilter other) { + return other == null ? this : (U configuration, T oldResource, T newResource) -> { + boolean result = acceptChange(configuration, oldResource, newResource); + return result || other.acceptChange(configuration, oldResource, newResource); + }; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index 43fe410fbc..e79823acd1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -3,13 +3,15 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; /** * Convenience implementations of, and utility methods for, {@link ResourceEventFilter}. */ public final class ResourceEventFilters { - private static final ResourceEventFilter USE_FINALIZER = + private static final ResourceEventFilter> USE_FINALIZER = (configuration, oldResource, newResource) -> { if (configuration.useFinalizer()) { final var finalizer = configuration.getFinalizer(); @@ -22,7 +24,7 @@ public final class ResourceEventFilters { } }; - private static final ResourceEventFilter GENERATION_AWARE = + private static final ResourceEventFilter> GENERATION_AWARE = (configuration, oldResource, newResource) -> { final var generationAware = configuration.isGenerationAware(); // todo: change this to check for HasStatus (or similar) when @@ -41,13 +43,13 @@ public final class ResourceEventFilters { oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; - private static final ResourceEventFilter PASSTHROUGH = + private static final ResourceEventFilter PASSTHROUGH = (configuration, oldResource, newResource) -> true; - private static final ResourceEventFilter NONE = + private static final ResourceEventFilter NONE = (configuration, oldResource, newResource) -> false; - private static final ResourceEventFilter MARKED_FOR_DELETION = + private static final ResourceEventFilter MARKED_FOR_DELETION = (configuration, oldResource, newResource) -> newResource.isMarkedForDeletion(); private ResourceEventFilters() {} @@ -59,8 +61,8 @@ private ResourceEventFilters() {} * @return a filter that accepts all events */ @SuppressWarnings("unchecked") - public static ResourceEventFilter passthrough() { - return (ResourceEventFilter) PASSTHROUGH; + public static > ResourceEventFilter passthrough() { + return (ResourceEventFilter) PASSTHROUGH; } /** @@ -70,8 +72,8 @@ public static ResourceEventFilter passthrough() { * @return a filter that reject all events */ @SuppressWarnings("unchecked") - public static ResourceEventFilter none() { - return (ResourceEventFilter) NONE; + public static > ResourceEventFilter none() { + return (ResourceEventFilter) NONE; } /** @@ -82,8 +84,8 @@ public static ResourceEventFilter none() { * @return a filter accepting changes based on generation information */ @SuppressWarnings("unchecked") - public static ResourceEventFilter generationAware() { - return (ResourceEventFilter) GENERATION_AWARE; + public static > ResourceEventFilter generationAware() { + return (ResourceEventFilter) GENERATION_AWARE; } /** @@ -95,8 +97,8 @@ public static ResourceEventFilter generationAware() { * applied */ @SuppressWarnings("unchecked") - public static ResourceEventFilter finalizerNeededAndApplied() { - return (ResourceEventFilter) USE_FINALIZER; + public static > ResourceEventFilter finalizerNeededAndApplied() { + return (ResourceEventFilter) USE_FINALIZER; } /** @@ -106,8 +108,8 @@ public static ResourceEventFilter finalizerNeededAndA * @return a filter accepting changes based on whether the Custom Resource is marked for deletion. */ @SuppressWarnings("unchecked") - public static ResourceEventFilter markedForDeletion() { - return (ResourceEventFilter) MARKED_FOR_DELETION; + public static > ResourceEventFilter markedForDeletion() { + return (ResourceEventFilter) MARKED_FOR_DELETION; } /** @@ -122,14 +124,14 @@ public static ResourceEventFilter markedForDeletion() * @return a combined filter implementing the AND logic combination of the provided filters */ @SafeVarargs - public static ResourceEventFilter and( - ResourceEventFilter... items) { + public static > ResourceEventFilter and( + ResourceEventFilter... items) { if (items == null) { return none(); } return (configuration, oldResource, newResource) -> { - for (ResourceEventFilter item : items) { + for (ResourceEventFilter item : items) { if (item == null) { continue; } @@ -156,14 +158,14 @@ public static ResourceEventFilter and( * @return a combined filter implementing the OR logic combination of both provided filters */ @SafeVarargs - public static ResourceEventFilter or( - ResourceEventFilter... items) { + public static > ResourceEventFilter or( + ResourceEventFilter... items) { if (items == null) { return none(); } return (configuration, oldResource, newResource) -> { - for (ResourceEventFilter item : items) { + for (ResourceEventFilter item : items) { if (item == null) { continue; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 8095289221..711881874a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,8 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Objects; -import java.util.Set; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,56 +9,59 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedInformer; -import io.fabric8.kubernetes.client.informers.cache.Cache; -import io.fabric8.kubernetes.client.informers.cache.Store; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; -public class InformerEventSource extends AbstractEventSource { +public class InformerEventSource + extends AbstractEventSource

{ private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); private final SharedInformer sharedInformer; - private final Function> secondaryToPrimaryResourcesIdSet; - private final Function associatedWith; + private final PrimaryResourcesRetriever associatedPrimaries; + private final AssociatedSecondaryRetriever associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; + private final ResourceCache cache; public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToTargetResourceIDSet) { - this(sharedInformer, resourceToTargetResourceIDSet, null, true); + PrimaryResourcesRetriever associatedPrimaries, Cloner cloner) { + this(sharedInformer, associatedPrimaries, null, true, cloner); } public InformerEventSource(KubernetesClient client, Class type, - Function> resourceToTargetResourceIDSet) { - this(client, type, resourceToTargetResourceIDSet, false); + PrimaryResourcesRetriever associatedPrimaries, Cloner cloner) { + this(client, type, associatedPrimaries, false, cloner); } InformerEventSource(KubernetesClient client, Class type, - Function> resourceToTargetResourceIDSet, - boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, - skipUpdateEventPropagationIfNoChange); + PrimaryResourcesRetriever associatedPrimaries, + boolean skipUpdateEventPropagationIfNoChange, Cloner cloner) { + this(client.informers().sharedIndexInformerFor(type, 0), associatedPrimaries, null, + skipUpdateEventPropagationIfNoChange, cloner); } public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToTargetResourceIDSet, - Function associatedWith, - boolean skipUpdateEventPropagationIfNoChange) { + PrimaryResourcesRetriever associatedPrimaries, + AssociatedSecondaryRetriever associatedWith, + boolean skipUpdateEventPropagationIfNoChange, + Cloner cloner) { + super(sharedInformer.getApiTypeClass()); this.sharedInformer = sharedInformer; - this.secondaryToPrimaryResourcesIdSet = resourceToTargetResourceIDSet; + this.associatedPrimaries = Objects.requireNonNull(associatedPrimaries, + () -> "Must specify a PrimaryResourcesRetriever for InformerEventSource for " + + sharedInformer.getApiTypeClass()); this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; if (sharedInformer.isRunning()) { log.warn( "Informer is already running on event source creation, this is not desirable and may " + "lead to non deterministic behavior."); } + this.cache = new InformerResourceCache<>(sharedInformer, cloner); - this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> cr -> { - final var metadata = cr.getMetadata(); - return getStore().getByKey(Cache.namespaceKeyFunc(metadata.getNamespace(), - metadata.getName())); - }); + this.associatedWith = Objects.requireNonNullElseGet(associatedWith, + () -> (cr, registry) -> cache.get(ResourceID.fromResource(cr)).orElse(null)); sharedInformer.addEventHandler(new ResourceEventHandler<>() { @Override @@ -86,7 +87,8 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.apply(object); + var primaryResourceIdSet = + associatedPrimaries.associatedPrimaryResources(object, getEventRegistry()); if (primaryResourceIdSet.isEmpty()) { return; } @@ -97,8 +99,9 @@ private void propagateEvent(T object) { * automatically started, what would cause a NullPointerException here, since an event might * be received between creation and registration. */ + final var eventHandler = getEventHandler(); if (eventHandler != null) { - this.eventHandler.handleEvent(event); + eventHandler.handleEvent(event); } }); } @@ -113,8 +116,8 @@ public void stop() { sharedInformer.close(); } - public Store getStore() { - return sharedInformer.getStore(); + public ResourceCache getCache() { + return cache; } /** @@ -124,12 +127,8 @@ public Store getStore() { * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ - public T getAssociated(HasMetadata resource) { - return associatedWith.apply(resource); + public T getAssociated(P resource) { + return associatedWith.associatedSecondary(resource, getEventRegistry()); } - - public SharedInformer getSharedInformer() { - return sharedInformer; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index c578490147..9ff575c21b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -2,36 +2,39 @@ import java.util.Collections; import java.util.Set; -import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; public class Mappers { - public static Function> fromAnnotation( + public static

AssociatedSecondaryIdentifier

sameNameAndNamespace() { + return (primary, registry) -> ResourceID.fromResource(primary); + } + + public static PrimaryResourcesRetriever fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); } - public static Function> fromAnnotation( + public static PrimaryResourcesRetriever fromAnnotation( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, false); } - public static Function> fromLabel( + public static PrimaryResourcesRetriever fromLabel( String nameKey) { return fromMetadata(nameKey, null, true); } - public static Function> fromLabel( + public static PrimaryResourcesRetriever fromLabel( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, true); } - private static Function> fromMetadata( + private static PrimaryResourcesRetriever fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { - return resource -> { + return (resource, registry) -> { final var metadata = resource.getMetadata(); if (metadata == null) { return Collections.emptySet(); @@ -44,4 +47,16 @@ private static Function> fromMetadata } }; } + + public static PrimaryResourcesRetriever fromOwnerReference() { + return (resource, registry) -> { + var ownerReferences = resource.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new ResourceID(ownerReferences.get(0).getName(), + resource.getMetadata().getNamespace())); + } else { + return Collections.emptySet(); + } + }; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index 2ab8b2f128..96440e3ee0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -23,6 +23,11 @@ public class TimerEventSource extends AbstractEventSource private final AtomicBoolean running = new AtomicBoolean(); private final Map onceTasks = new ConcurrentHashMap<>(); + public TimerEventSource() { + // we need to return the source class so that it can be used to register the event source + super(TimerEventSource.class); + } + public void scheduleOnce(R resource, long delay) { if (!running.get()) { @@ -73,7 +78,7 @@ public EventProducerTimeTask(ResourceID customResourceUid) { public void run() { if (running.get()) { log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new Event(customResourceUid)); + getEventHandler().handleEvent(new Event(customResourceUid)); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java new file mode 100644 index 0000000000..509182e5a2 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockKubernetesClient { + public static KubernetesClient client(Class clazz) { + final var client = mock(KubernetesClient.class); + MixedOperation, Resource> resources = + mock(MixedOperation.class); + NonNamespaceOperation, Resource> nonNamespaceOperation = + mock(NonNamespaceOperation.class); + FilterWatchListMultiDeletable> inAnyNamespace = mock( + FilterWatchListMultiDeletable.class); + FilterWatchListDeletable> filterable = + mock(FilterWatchListDeletable.class); + when(resources.inNamespace(anyString())).thenReturn(nonNamespaceOperation); + when(nonNamespaceOperation.withLabelSelector(nullable(String.class))).thenReturn(filterable); + when(resources.inAnyNamespace()).thenReturn(inAnyNamespace); + when(inAnyNamespace.withLabelSelector(nullable(String.class))).thenReturn(filterable); + SharedIndexInformer informer = mock(SharedIndexInformer.class); + when(filterable.runnableInformer(anyLong())).thenReturn(informer); + when(client.resources(clazz)).thenReturn(resources); + + + return client; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index 28a19d1081..1298da902c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -8,20 +8,25 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class OperatorTest { - private final KubernetesClient kubernetesClient = mock(KubernetesClient.class); + private final KubernetesClient kubernetesClient = + MockKubernetesClient.client(FooCustomResource.class); private final ConfigurationService configurationService = mock(ConfigurationService.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + static { + ExecutorServiceManager.useTestInstance(); + } private final Operator operator = new Operator(kubernetesClient, configurationService); private final FooReconciler fooReconciler = FooReconciler.create(); @@ -33,16 +38,14 @@ public void shouldRegisterReconcilerToController() { when(configurationService.getConfigurationFor(fooReconciler)).thenReturn(configuration); when(configuration.watchAllNamespaces()).thenReturn(true); when(configuration.getName()).thenReturn("FOO"); - when(configuration.getResourceClass()).thenReturn(FooReconciler.class); + when(configuration.getResourceClass()).thenReturn(FooCustomResource.class); + when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); + when(configuration.getConfigurationService()).thenReturn(configurationService); // when operator.register(fooReconciler); // then - verify(configuration).watchAllNamespaces(); - verify(configuration).getName(); - verify(configuration).getResourceClass(); - assertThat(operator.getControllers().size()).isEqualTo(1); assertThat(operator.getControllers().get(0).getReconciler()).isEqualTo(fooReconciler); } 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 3bc72d81f2..65c73b6b31 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 @@ -20,6 +20,9 @@ public String getAssociatedReconcilerClassName() { public ConfigurationService getConfigurationService() { return null; } + + @Override + public void setConfigurationService(ConfigurationService service) {} }; assertEquals(TestCustomResource.class, conf.getResourceClass()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index f837f5eea2..cc362f8346 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -42,8 +42,7 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceCache resourceCacheMock = - mock(ControllerResourceCache.class); + private AggregateResourceCache resourceCacheMock = mock(AggregateResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); @@ -58,13 +57,17 @@ public void setup() { when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor = - spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, null)); eventProcessorWithRetry = - spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, GenericRetry.defaultLimitedExponentialRetry())); when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); + + // start processors + eventProcessor.start(); + eventProcessorWithRetry.start(); } @Test @@ -218,6 +221,7 @@ public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); + when(mockCREventSource.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); @@ -237,6 +241,7 @@ public void dontWhitelistsEventWhenOtherChangeDuringExecution() { eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); + when(mockCREventSource.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); @@ -252,6 +257,7 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); + when(mockCREventSource.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(cr)); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 79311a7057..b72b7c533a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class EventSourceManagerTest { @@ -21,19 +22,22 @@ class EventSourceManagerTest { @Test public void registersEventSource() { EventSource eventSource = mock(EventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); eventSourceManager.registerEventSource(eventSource); Set registeredSources = eventSourceManager.getRegisteredEventSources(); assertThat(registeredSources).contains(eventSource); - verify(eventSource, times(1)).setEventHandler(eq(eventProcessorMock)); + verify(eventSource, times(1)).setEventRegistry(eq(eventSourceManager)); } @Test public void closeShouldCascadeToEventSources() throws IOException { EventSource eventSource = mock(EventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); EventSource eventSource2 = mock(EventSource.class); + when(eventSource2.getResourceClass()).thenReturn(Object.class); eventSourceManager.registerEventSource(eventSource); eventSourceManager.registerEventSource(eventSource2); @@ -46,7 +50,9 @@ public void closeShouldCascadeToEventSources() throws IOException { @Test public void startCascadesToEventSources() { EventSource eventSource = mock(EventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); EventSource eventSource2 = mock(EventSource.class); + when(eventSource2.getResourceClass()).thenReturn(Object.class); eventSourceManager.registerEventSource(eventSource); eventSourceManager.registerEventSource(eventSource2); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 4de29f596c..bed34f9d1f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -11,10 +11,13 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.Controller; @@ -37,37 +40,43 @@ class ReconciliationDispatcherTest { private ReconciliationDispatcher reconciliationDispatcher; private final Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(ErrorStatusHandler.class)); - private final ControllerConfiguration configuration = - mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); private final CustomResourceFacade customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); @BeforeEach void setup() { + ExecutorServiceManager.useTestInstance(); testCustomResource = TestUtils.testCustomResource(); reconciliationDispatcher = - init(testCustomResource, reconciler, configuration, customResourceFacade); + init(testCustomResource, reconciler, null, customResourceFacade, true); } private ReconciliationDispatcher init(R customResource, Reconciler reconciler, ControllerConfiguration configuration, - CustomResourceFacade customResourceFacade) { - when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); + CustomResourceFacade customResourceFacade, boolean useFinalizer) { + configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; + final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; + when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); + when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); when(configuration.getConfigurationService()).thenReturn(configService); + + when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configService.getResourceCloner()).thenReturn(new Cloner() { @Override + public R clone(R object) { return object; } }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); - Controller controller = - new Controller<>(reconciler, configuration, null); + Controller controller = new Controller<>(reconciler, configuration, + MockKubernetesClient.client(customResource.getClass())); + controller.start(); return new ReconciliationDispatcher<>(controller, customResourceFacade); } @@ -144,10 +153,12 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { */ @Test void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { - configureToNotUseFinalizer(); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, + null, customResourceFacade, false); markForDeletion(testCustomResource); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler).cleanup(eq(testCustomResource), any()); } @@ -161,23 +172,13 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } - private void configureToNotUseFinalizer() { - ControllerConfiguration configuration = - mock(ControllerConfiguration.class); - when(configuration.getName()).thenReturn("EventDispatcherTestController"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); - when(configuration.getConfigurationService()).thenReturn(configService); - when(configuration.useFinalizer()).thenReturn(false); - reconciliationDispatcher = - new ReconciliationDispatcher(new Controller(reconciler, configuration, null), - customResourceFacade); - } - @Test void doesNotAddFinalizerIfConfiguredNotTo() { - configureToNotUseFinalizer(); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, + null, customResourceFacade, false); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); } @@ -274,7 +275,7 @@ public boolean isLastAttempt() { ArgumentCaptor.forClass(Context.class); verify(reconciler, times(1)) .reconcile(any(), contextArgumentCaptor.capture()); - Context context = contextArgumentCaptor.getValue(); + Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); @@ -316,7 +317,7 @@ void setObservedGenerationForStatusIfNeeded() { ControllerConfiguration config = mock(ControllerConfiguration.class); CustomResourceFacade facade = mock(CustomResourceFacade.class); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) @@ -341,7 +342,7 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.noUpdate()); when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); @@ -362,7 +363,7 @@ void updateObservedGenerationOnCustomResourceUpdate() { .thenReturn(UpdateControl.updateResource(observedGenResource)); when(facade.replaceWithLock(any())).thenReturn(observedGenResource); when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index 22be8649e6..e690beafeb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -154,6 +154,9 @@ public Set getNamespaces() { public ConfigurationService getConfigurationService() { return service; } + + @Override + public void setConfigurationService(ConfigurationService service) {} } @ControllerConfiguration(namespaces = NAMESPACE) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index be6d9f803b..909954f59f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -7,10 +7,8 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -34,10 +32,12 @@ class ResourceEventFilterTest { public static final String FINALIZER = "finalizer"; private EventHandler eventHandler; + private EventSourceRegistry registry = mock(EventSourceRegistry.class); @BeforeEach public void before() { this.eventHandler = mock(EventHandler.class); + when(registry.getEventHandler()).thenReturn(eventHandler); } @Test @@ -50,8 +50,7 @@ public void eventFilteredByCustomPredicate() { newResource.getStatus().getConfigMapStatus())); var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + var eventSource = new ControllerResourceEventSource<>(controller, registry); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setFinalizers(List.of(FINALIZER)); @@ -78,8 +77,7 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { newResource.getStatus().getConfigMapStatus())); var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + var eventSource = new ControllerResourceEventSource<>(controller, registry); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setFinalizers(List.of(FINALIZER)); @@ -108,8 +106,7 @@ public void observedGenerationFiltering() { .thenReturn(ConfigurationService.DEFAULT_CLONER); var controller = new ObservedGenController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + var eventSource = new ControllerResourceEventSource<>(controller, registry); ObservedGenCustomResource cr = new ObservedGenCustomResource(); cr.setMetadata(new ObjectMeta()); @@ -139,8 +136,7 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { .thenReturn(ConfigurationService.DEFAULT_CLONER); var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + var eventSource = new ControllerResourceEventSource<>(controller, registry); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setGeneration(1L); @@ -158,23 +154,24 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { private static class TestControllerConfig extends ControllerConfig { public TestControllerConfig(String finalizer, boolean generationAware, - ResourceEventFilter eventFilter) { + ResourceEventFilter> eventFilter) { super(finalizer, generationAware, eventFilter, TestCustomResource.class); } } private static class ObservedGenControllerConfig extends ControllerConfig { public ObservedGenControllerConfig(String finalizer, boolean generationAware, - ResourceEventFilter eventFilter) { + ResourceEventFilter> eventFilter) { super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class); } } - private static class ControllerConfig extends - DefaultControllerConfiguration { + private static class ControllerConfig + extends DefaultControllerConfiguration { public ControllerConfig(String finalizer, boolean generationAware, - ResourceEventFilter eventFilter, Class customResourceClass) { + ResourceEventFilter> eventFilter, + Class customResourceClass) { super( null, null, @@ -196,18 +193,13 @@ public ControllerConfig(String finalizer, boolean generationAware, private static class TestController extends Controller { public TestController(ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(TestCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } private static class ObservedGenController @@ -215,17 +207,12 @@ private static class ObservedGenController public ObservedGenController( ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(ObservedGenCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 5eb26db524..68c5b4f084 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -30,16 +31,18 @@ class ControllerResourceEventSourceTest { public static final String FINALIZER = "finalizer"; private static final MixedOperation, Resource> client = - mock(MixedOperation.class); + MockKubernetesClient.client(TestCustomResource.class).resources(TestCustomResource.class); EventHandler eventHandler = mock(EventHandler.class); + EventSourceRegistry registry = mock(EventSourceRegistry.class); - private TestController testController = new TestController(true); + private final TestController testController = new TestController(true); private ControllerResourceEventSource controllerResourceEventSource = - new ControllerResourceEventSource<>(testController); + new ControllerResourceEventSource<>(testController, registry); @BeforeEach public void setup() { - controllerResourceEventSource.setEventHandler(eventHandler); + when(registry.getEventHandler()).thenReturn(eventHandler); + controllerResourceEventSource.setEventRegistry(registry); } @Test @@ -92,7 +95,7 @@ public void normalExecutionIfGenerationChanges() { @Test public void handlesAllEventIfNotGenerationAware() { controllerResourceEventSource = - new ControllerResourceEventSource<>(new TestController(false)); + new ControllerResourceEventSource<>(new TestController(false), registry); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 0f259f77cb..65372bceac 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class TimerEventSourceTest { @@ -26,14 +28,17 @@ class TimerEventSourceTest { public static final int PERIOD = 50; private TimerEventSource timerEventSource; - private CapturingEventHandler eventHandlerMock; + private CapturingEventHandler eventHandler; @BeforeEach public void setup() { - eventHandlerMock = new CapturingEventHandler(); + eventHandler = new CapturingEventHandler(); timerEventSource = new TimerEventSource<>(); - timerEventSource.setEventHandler(eventHandlerMock); + EventSourceRegistry registryMock = mock(EventSourceRegistry.class); + when(registryMock.getEventHandler()).thenReturn(eventHandler); + + timerEventSource.setEventRegistry(registryMock); timerEventSource.start(); } @@ -43,8 +48,8 @@ public void schedulesOnce() { timerEventSource.scheduleOnce(customResource, PERIOD); - untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(1)); - untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandlerMock.events).hasSize(1)); + untilAsserted(() -> assertThat(eventHandler.events).hasSize(1)); + untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandler.events).hasSize(1)); } @Test @@ -54,7 +59,7 @@ public void canCancelOnce() { timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.cancelOnceSchedule(ResourceID.fromResource(customResource)); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } @Test @@ -64,7 +69,7 @@ public void canRescheduleOnceEvent() { timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.scheduleOnce(customResource, 2 * PERIOD); - untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandlerMock.events).hasSize(1)); + untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandler.events).hasSize(1)); } @Test @@ -75,7 +80,7 @@ public void deRegistersOnceEventSources() { timerEventSource .onResourceDeleted(customResource); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } @Test @@ -92,7 +97,7 @@ public void eventNotFiredIfStopped() throws IOException { timerEventSource.scheduleOnce(TestUtils.testCustomResource(), PERIOD); timerEventSource.stop(); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } private void untilAsserted(ThrowingRunnable assertion) { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 9a5577a90d..60d9ae6cf3 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,13 +1,25 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.dependent.DefaultDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.DEFAULT_PRIMARIES_RETRIEVER; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.DEFAULT_SECONDARY_IDENTIFIER; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -17,6 +29,7 @@ public class AnnotationConfiguration private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; + private List dependents; public AnnotationConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -75,17 +88,17 @@ public String getAssociatedReconcilerClassName() { @SuppressWarnings("unchecked") @Override - public ResourceEventFilter getEventFilter() { - ResourceEventFilter answer = null; + public ResourceEventFilter> getEventFilter() { + ResourceEventFilter> answer = + null; - Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, - new Object[] {}); + var filterTypes = + (Class>>[]) valueOrDefault( + annotation, ControllerConfiguration::eventFilters, new Class[] {}); if (filterTypes.length > 0) { for (var filterType : filterTypes) { try { - ResourceEventFilter filter = filterType.getConstructor().newInstance(); + var filter = filterType.getConstructor().newInstance(); if (answer == null) { answer = filter; @@ -102,13 +115,81 @@ public ResourceEventFilter getEventFilter() { : ResourceEventFilters.passthrough(); } - public static T valueOrDefault(ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); + @Override + public List getDependentResources() { + if (dependents == null) { + final var dependentConfigs = valueOrDefault(annotation, + ControllerConfiguration::dependents, new DependentResourceConfiguration[] {}); + if (dependentConfigs.length > 0) { + dependents = new ArrayList<>(dependentConfigs.length); + for (DependentResourceConfiguration dependentConfig : dependentConfigs) { + final var creatable = dependentConfig.creatable(); + final var updatable = dependentConfig.updatable(); + final var owned = dependentConfig.owned(); + + final var resourceType = dependentConfig.resourceType(); + final var crdName = CustomResource.getCRDName(resourceType); + final var namespaces = Set.of( + valueOrDefault(dependentConfig, DependentResourceConfiguration::namespaces, + new String[] {})); + final var labelSelector = dependentConfig.labelSelector(); + + final PrimaryResourcesRetriever primariesMapper = + valueIfPresentOrNull( + dependentConfig, DependentResourceConfiguration::associatedPrimariesRetriever, + DEFAULT_PRIMARIES_RETRIEVER.class); + final AssociatedSecondaryIdentifier secondaryMapper = + valueIfPresentOrNull( + dependentConfig, DependentResourceConfiguration::associatedSecondaryIdentifier, + DEFAULT_SECONDARY_IDENTIFIER.class); + + + final DefaultDependentResourceConfiguration configuration = + new DefaultDependentResourceConfiguration( + crdName, resourceType, namespaces, labelSelector, service, creatable, updatable, + owned, primariesMapper, secondaryMapper, + dependentConfig.skipUpdateIfUnchanged()); + + final var builder = + valueIfPresentOrNull(dependentConfig, DependentResourceConfiguration::builder, + DependentResourceConfiguration.DEFAULT_BUILDER.class); + final var updater = + valueIfPresentOrNull(dependentConfig, DependentResourceConfiguration::updater, + DependentResourceConfiguration.DEFAULT_UPDATER.class); + final var fetcher = + valueIfPresentOrNull(dependentConfig, DependentResourceConfiguration::fetcher, + DependentResourceConfiguration.DEFAULT_FETCHER.class); + + final var dependent = new DependentResource(configuration, builder, updater, fetcher); + dependents.add(dependent); + } + } else { + dependents = Collections.emptyList(); + } + } + return dependents; + } + + private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { + return annotation == null ? defaultValue : mapper.apply(annotation); + } + + private static T valueIfPresentOrNull(DependentResourceConfiguration annotation, + Function> mapper, + Class defaultValue) { + if (annotation == null) { + return null; + } + + final var value = mapper.apply(annotation); + if (defaultValue.equals(value)) { + return null; + } + try { + return value.getConstructor().newInstance(); + } catch (InstantiationException | NoSuchMethodException | InvocationTargetException + | IllegalAccessException e) { + throw new RuntimeException(e); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 8f20c44353..c3b8632e02 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -9,7 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class ErrorStatusHandlerTestReconciler @@ -22,7 +22,8 @@ public class ErrorStatusHandlerTestReconciler @Override public UpdateControl reconcile( - ErrorStatusHandlerTestCustomResource resource, Context context) { + ErrorStatusHandlerTestCustomResource resource, + Context context) { var number = numberOfExecutions.addAndGet(1); var retryAttempt = -1; if (context.getRetryInfo().isPresent()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index b6807358f1..4624241549 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; @@ -15,7 +16,7 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; /** * Copies the config map value from spec into status. The main purpose is to test and demonstrate @@ -33,12 +34,12 @@ public class InformerEventSourceTestCustomReconciler implements public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; - private InformerEventSource eventSource; + private InformerEventSource eventSource; @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + public void prepareEventSources(EventSourceRegistry eventSourceRegistry, Cloner cloner) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_NAME)); + Mappers.fromAnnotation(RELATED_RESOURCE_NAME), cloner); eventSourceRegistry.registerEventSource(eventSource); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 01a1705fe4..65d7be4851 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class ObservedGenerationTestReconciler diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index b8b0cacb04..59c17be8c6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -32,7 +32,7 @@ public RetryTestCustomReconciler(int numberOfExecutionFails) { @Override public UpdateControl reconcile( - RetryTestCustomResource resource, Context context) { + RetryTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java new file mode 100644 index 0000000000..03446cb60b --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +public class DeploymentDependentResource + implements Builder, Updater { + + @Override + public Deployment buildFor(Tomcat tomcat) { + Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final String tomcatName = tomcatMetadata.getName(); + deployment = new DeploymentBuilder(deployment) + .editMetadata() + .withName(tomcatName) + .withNamespace(tomcatMetadata.getNamespace()) + .addToLabels("app", tomcatName) + .addToLabels("app.kubernetes.io/part-of", tomcatName) + .addToLabels("app.kubernetes.io/managed-by", "tomcat-operator") + .endMetadata() + .editSpec() + .editSelector().addToMatchLabels("app", tomcatName).endSelector() + .withReplicas(tomcat.getSpec().getReplicas()) + // set tomcat version + .editTemplate() + // make sure label selector matches label (which has to be matched by service selector too) + .editMetadata().addToLabels("app", tomcatName).endMetadata() + .editSpec() + .editFirstContainer().withImage("tomcat:" + tomcat.getSpec().getVersion()).endContainer() + .endSpec() + .endTemplate() + .endSpec() + .build(); + return deployment; + } + + @Override + public Deployment update(Deployment fetched, Tomcat tomcat) { + return new DeploymentBuilder(fetched).editSpec().editTemplate().editSpec().editFirstContainer() + .withImage("tomcat:" + tomcat.getSpec().getVersion()) + .endContainer().endSpec().endTemplate().endSpec().build(); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java new file mode 100644 index 0000000000..e1d1c57732 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; + +public class ServiceDependentResource + implements Builder { + + @Override + public Service buildFor(Tomcat tomcat) { + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final Service service = + new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + .editMetadata() + .withName(tomcatMetadata.getName()) + .withNamespace(tomcatMetadata.getNamespace()) + .endMetadata() + .editSpec() + .addToSelector("app", tomcatMetadata.getName()) + .endSpec() + .build(); + return service; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 1d2c8d187b..288d42d120 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -3,68 +3,50 @@ import java.io.IOException; import java.io.InputStream; import java.util.Objects; -import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +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; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; -import static java.util.Collections.EMPTY_SET; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; /** * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class TomcatReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + dependents = { + @DependentResourceConfiguration(resourceType = Deployment.class, + labelSelector = "app.kubernetes.io/managed-by=tomcat-operator", + updatable = true, builder = DeploymentDependentResource.class, + updater = DeploymentDependentResource.class), + @DependentResourceConfiguration(resourceType = Service.class, + builder = ServiceDependentResource.class) + }) +public class TomcatReconciler implements Reconciler { private final Logger log = LoggerFactory.getLogger(getClass()); private final KubernetesClient kubernetesClient; - private volatile InformerEventSource informerEventSource; - public TomcatReconciler(KubernetesClient client) { this.kubernetesClient = client; } @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - SharedIndexInformer deploymentInformer = - kubernetesClient.apps().deployments().inAnyNamespace() - .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") - .runnableInformer(0); - - this.informerEventSource = new InformerEventSource<>(deploymentInformer, d -> { - var ownerReferences = d.getMetadata().getOwnerReferences(); - if (!ownerReferences.isEmpty()) { - return Set.of(new ResourceID(ownerReferences.get(0).getName(), - d.getMetadata().getNamespace())); - } else { - return EMPTY_SET; - } - }); - eventSourceRegistry.registerEventSource(this.informerEventSource); - } - - @Override - public UpdateControl reconcile(Tomcat tomcat, Context context) { - createOrUpdateDeployment(tomcat); - createOrUpdateService(tomcat); - - Deployment deployment = informerEventSource.getAssociated(tomcat); + public UpdateControl reconcile(Tomcat tomcat, Context context) { + Deployment deployment = context.getSecondaryResource(Deployment.class); if (deployment != null) { Tomcat updatedTomcat = @@ -89,78 +71,8 @@ private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { return tomcat; } - private void createOrUpdateDeployment(Tomcat tomcat) { - String ns = tomcat.getMetadata().getNamespace(); - Deployment existingDeployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(ns) - .withName(tomcat.getMetadata().getName()) - .get(); - if (existingDeployment == null) { - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(tomcat.getMetadata().getName()); - deployment.getMetadata().setNamespace(ns); - deployment.getMetadata().getLabels().put("app.kubernetes.io/part-of", - tomcat.getMetadata().getName()); - deployment.getMetadata().getLabels().put("app.kubernetes.io/managed-by", "tomcat-operator"); - // set tomcat version - deployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - deployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - - // make sure label selector matches label (which has to be matched by service selector too) - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", tomcat.getMetadata().getName()); - deployment - .getSpec() - .getSelector() - .getMatchLabels() - .put("app", tomcat.getMetadata().getName()); - - OwnerReference ownerReference = deployment.getMetadata().getOwnerReferences().get(0); - ownerReference.setName(tomcat.getMetadata().getName()); - ownerReference.setUid(tomcat.getMetadata().getUid()); - - log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); - kubernetesClient.apps().deployments().inNamespace(ns).create(deployment); - } else { - existingDeployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - existingDeployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(existingDeployment); - } - } - - private void createOrUpdateService(Tomcat tomcat) { - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(tomcat.getMetadata().getName()); - String ns = tomcat.getMetadata().getNamespace(); - service.getMetadata().setNamespace(ns); - service.getMetadata().getOwnerReferences().get(0).setName(tomcat.getMetadata().getName()); - service.getMetadata().getOwnerReferences().get(0).setUid(tomcat.getMetadata().getUid()); - service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); - log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); - kubernetesClient.services().inNamespace(ns).createOrReplace(service); - } - - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { + static T loadYaml(Class clazz, String yaml) { + try (InputStream is = TomcatReconciler.class.getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); } catch (IOException ex) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 2c71dd9815..3f9bce1f81 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -3,6 +3,7 @@ import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -17,21 +18,27 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.sample.WebappReconciler.TomcatIdentifier; +import io.javaoperatorsdk.operator.sample.WebappReconciler.WebappRetriever; import okhttp3.Response; -@ControllerConfiguration -public class WebappReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration( + dependents = @DependentResourceConfiguration( + creatable = false, resourceType = Tomcat.class, + associatedPrimariesRetriever = WebappRetriever.class, + associatedSecondaryIdentifier = TomcatIdentifier.class)) +public class WebappReconciler implements Reconciler { private KubernetesClient kubernetesClient; @@ -41,22 +48,26 @@ public WebappReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - private InformerEventSource tomcatEventSource; + public static class WebappRetriever implements PrimaryResourcesRetriever { + @Override + public Set associatedPrimaryResources(Tomcat t, + EventSourceRegistry registry) { + // To create an event to a related WebApp resource and trigger the reconciliation + // we need to find which WebApp this Tomcat custom resource is related to. + // To find the related customResourceId of the WebApp resource we traverse the cache to + // and identify it based on naming convention. + return registry.getControllerResourceEventSource().getResourceCache() + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet()); + } + } - @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - tomcatEventSource = - new InformerEventSource<>(kubernetesClient, Tomcat.class, t -> { - // To create an event to a related WebApp resource and trigger the reconciliation - // we need to find which WebApp this Tomcat custom resource is related to. - // To find the related customResourceId of the WebApp resource we traverse the cache to - // and identify it based on naming convention. - return eventSourceRegistry.getControllerResourceEventSource().getResourceCache() - .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(ResourceID::fromResource) - .collect(Collectors.toSet()); - }); - eventSourceRegistry.registerEventSource(tomcatEventSource); + public static class TomcatIdentifier implements AssociatedSecondaryIdentifier { + @Override + public ResourceID associatedSecondaryID(Webapp primary, EventSourceRegistry registry) { + return new ResourceID(primary.getSpec().getTomcat(), primary.getMetadata().getNamespace()); + } } /** @@ -64,15 +75,13 @@ public void prepareEventSources(EventSourceRegistry eventSourceRegistry) * change. */ @Override - public UpdateControl reconcile(Webapp webapp, Context context) { + public UpdateControl reconcile(Webapp webapp, Context context) { if (webapp.getStatus() != null && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) { return UpdateControl.noUpdate(); } - Tomcat tomcat = tomcatEventSource.getStore() - .getByKey(Cache.namespaceKeyFunc(webapp.getMetadata().getNamespace(), - webapp.getSpec().getTomcat())); + Tomcat tomcat = context.getSecondaryResource(Tomcat.class); if (tomcat == null) { throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + " for Webapp " + webapp.getMetadata().getName() + " in namespace " @@ -108,7 +117,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { } @Override - public DeleteControl cleanup(Webapp webapp, Context context) { + public DeleteControl cleanup(Webapp webapp, Context context) { String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"}; String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml index 55d1a6be7d..aa38eb3619 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -5,11 +5,6 @@ metadata: labels: app.kubernetes.io/part-of: "" app.kubernetes.io/managed-by: "" # used for filtering of Deployments created by the controller - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: matchLabels: diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml index a807d277a7..ab198643ed 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -2,11 +2,6 @@ apiVersion: v1 kind: Service metadata: name: "" - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: app: "" From 591c97f8564131981a2725dbb14d71f20ddf54e0 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 15 Dec 2021 15:24:24 +0100 Subject: [PATCH 2/2] chore: update after rebase --- .../api/config/ControllerConfiguration.java | 2 +- .../api/config/ResourceConfiguration.java | 4 +-- .../DependentResourceConfiguration.java | 2 +- .../dependent/DependentResource.java | 2 +- .../DependentResourceConfiguration.java | 4 +-- .../api/reconciler/dependent/Fetcher.java | 2 +- .../operator/processing/Controller.java | 2 +- .../processing/event/EventSourceManager.java | 2 +- .../source/AbstractResourceEventSource.java | 6 ++++- .../event/source/AggregateResourceCache.java | 3 ++- .../event/source/CachingEventSource.java | 11 +++++--- .../source/DependentResourceEventSource.java | 2 ++ .../event/source/EventSourceRegistry.java | 2 +- .../event/source/EventSourceWrapper.java | 1 + .../event/source/InformerResourceCache.java | 1 + .../source/LifecycleAwareEventSource.java | 8 +++++- .../event/source/ResourceEventSource.java | 1 + .../ControllerResourceEventSource.java | 3 ++- .../InformerEventSourceWrapper.java | 4 ++- .../inbound/CachingInboundEventSource.java | 7 ++--- .../inbound/SimpleInboundEventSource.java | 9 +++++-- .../source/informer/InformerEventSource.java | 4 +++ .../event/source/informer/Mappers.java | 2 ++ .../PerResourcePollingEventSource.java | 11 ++++---- .../source/polling/PollingEventSource.java | 7 ++--- .../processing/event/EventProcessorTest.java | 2 +- .../event/source/CachingEventSourceTest.java | 27 ++++++++++--------- .../ControllerResourceEventSourceTest.java | 1 + .../PerResourcePollingEventSourceTest.java | 23 ++++++++++------ .../polling/PollingEventSourceTest.java | 19 ++++++++----- .../source/timer/TimerEventSourceTest.java | 1 + .../sample/MySQLSchemaReconciler.java | 8 +++--- 32 files changed, 118 insertions(+), 65 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/InformerEventSourceWrapper.java (85%) 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 15ac6b221b..a6b55d7b5f 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 @@ -5,9 +5,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public interface ControllerConfiguration extends ResourceConfiguration> { 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 985e37f916..b66f2318b0 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 @@ -6,8 +6,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; public interface ResourceConfiguration> { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java index 58ff74fd57..da9732ac9b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java @@ -4,8 +4,8 @@ import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryRetriever; -import io.javaoperatorsdk.operator.processing.event.source.Mappers; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.CREATABLE_DEFAULT; import static io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfiguration.OWNED_DEFAULT; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 26a5c91a92..9ce633d40f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -4,8 +4,8 @@ import io.javaoperatorsdk.operator.api.config.dependent.DefaultDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; public class DependentResource implements Builder, Updater { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java index 8492e3e2ba..1801e455b7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfiguration.java @@ -7,8 +7,8 @@ import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java index 226df844e8..e87f2a33b9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Fetcher.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; @FunctionalInterface public interface Fetcher { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 93a224ad15..f05e55940f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -33,7 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.source.DependentResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.EventSourceWrapper; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 8bceba4a4c..26c1282013 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -16,8 +16,8 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java index 1458e530b6..cd1cc431af 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java @@ -17,6 +17,10 @@ import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -61,7 +65,7 @@ protected abstract V wrapEventSource( FilterWatchListDeletable> filteredBySelectorClient, Cloner cloner); - void eventReceived(ResourceAction action, T resource, T oldResource) { + protected void eventReceived(ResourceAction action, T resource, T oldResource) { log.debug("Event received for resource: {}", getName(resource)); if (filter.acceptChange(configuration, oldResource, resource)) { getEventHandler().handleEvent(new ResourceEvent(action, ResourceID.fromResource(resource))); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java index 05204bc4bd..a3178d489a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AggregateResourceCache.java @@ -7,8 +7,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; -import static io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; +import static io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; public class AggregateResourceCache> implements ResourceCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 9a2be41a70..dc94100a7a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -23,13 +24,15 @@ * * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ -public abstract class CachingEventSource extends LifecycleAwareEventSource { +public abstract class CachingEventSource + extends LifecycleAwareEventSource

{ private static final Logger log = LoggerFactory.getLogger(CachingEventSource.class); protected Cache cache; - public CachingEventSource(Cache cache) { + public CachingEventSource(Cache cache, Class

resourceClass) { + super(resourceClass); this.cache = cache; } @@ -41,7 +44,7 @@ protected void handleDelete(ResourceID relatedResourceID) { cache.remove(relatedResourceID); // we only propagate event if the resource was previously in cache if (cachedValue != null) { - eventHandler.handleEvent(new Event(relatedResourceID)); + getEventHandler().handleEvent(new Event(relatedResourceID)); } } @@ -52,7 +55,7 @@ protected void handleEvent(T value, ResourceID relatedResourceID) { var cachedValue = cache.get(relatedResourceID); if (cachedValue == null || !cachedValue.equals(value)) { cache.put(relatedResourceID, value); - eventHandler.handleEvent(new Event(relatedResourceID)); + getEventHandler().handleEvent(new Event(relatedResourceID)); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java index 168a309bbd..507b995e14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/DependentResourceEventSource.java @@ -11,6 +11,8 @@ import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; public class DependentResourceEventSource extends InformerEventSource diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java index 5538d9446a..883ff62dbc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; public interface EventSourceRegistry { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java index 239247ec84..755b8e5609 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceWrapper.java @@ -2,6 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; public interface EventSourceWrapper extends LifecycleAware, ResourceCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java index 8dc1c7c6b1..8830f96288 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerResourceCache.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; public class InformerResourceCache implements ResourceCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java index 6b2d79fd1a..9267959ebb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java @@ -1,11 +1,17 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -public abstract class LifecycleAwareEventSource extends AbstractEventSource { +public abstract class LifecycleAwareEventSource

+ extends AbstractEventSource

{ private volatile boolean running = false; + protected LifecycleAwareEventSource(Class

resourceClass) { + super(resourceClass); + } + public boolean isRunning() { return running; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java index 7641e25312..83dde88552 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java @@ -2,6 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; public interface ResourceEventSource extends EventSource

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 39a6aab9d1..676fa0c4f3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -10,7 +10,8 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; /** * This is a special case since is not bound to a single custom resource diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InformerEventSourceWrapper.java similarity index 85% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InformerEventSourceWrapper.java index ae6ce08163..70f23c3cd3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSourceWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InformerEventSourceWrapper.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.Optional; import java.util.function.Predicate; @@ -10,6 +10,8 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceWrapper; +import io.javaoperatorsdk.operator.processing.event.source.InformerResourceCache; class InformerEventSourceWrapper implements EventSourceWrapper { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 51d1ed287d..cfba3260d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -2,13 +2,14 @@ import javax.cache.Cache; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class CachingInboundEventSource extends CachingEventSource { +public class CachingInboundEventSource extends CachingEventSource { - public CachingInboundEventSource(Cache cache) { - super(cache); + public CachingInboundEventSource(Cache cache, Class

resourceClass) { + super(cache, resourceClass); } public void handleResourceEvent(T resource, ResourceID relatedResourceID) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java index 475cfee916..3a6ddccb8b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java @@ -3,17 +3,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.LifecycleAwareEventSource; -public class SimpleInboundEventSource extends LifecycleAwareEventSource { +public class SimpleInboundEventSource

extends LifecycleAwareEventSource

{ private static final Logger log = LoggerFactory.getLogger(SimpleInboundEventSource.class); + protected SimpleInboundEventSource(Class

resourceClass) { + super(resourceClass); + } + public void propagateEvent(ResourceID resourceID) { if (isRunning()) { - eventHandler.handleEvent(new Event(resourceID)); + getEventHandler().handleEvent(new Event(resourceID)); } else { log.debug("Event source not started yet, not propagating event for: {}", resourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 711881874a..36d051814a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -13,6 +13,10 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryRetriever; +import io.javaoperatorsdk.operator.processing.event.source.InformerResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; public class InformerEventSource extends AbstractEventSource

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 9ff575c21b..7c76b74cdc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -5,6 +5,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; public class Mappers { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java index bf9b41cf0e..60a1a154a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java @@ -31,7 +31,7 @@ * @param related custom resource */ public class PerResourcePollingEventSource - extends CachingEventSource + extends CachingEventSource implements ResourceEventAware { private static final Logger log = LoggerFactory.getLogger(PerResourcePollingEventSource.class); @@ -44,14 +44,15 @@ public class PerResourcePollingEventSource private final long period; public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - ResourceCache resourceCache, long period, Cache cache) { - this(resourceSupplier, resourceCache, period, cache, null); + ResourceCache resourceCache, long period, Cache cache, + Class resourceClass) { + this(resourceSupplier, resourceCache, period, cache, null, resourceClass); } public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, ResourceCache resourceCache, long period, Cache cache, - Predicate registerPredicate) { - super(cache); + Predicate registerPredicate, Class resourceClass) { + super(cache, resourceClass); this.resourceSupplier = resourceSupplier; this.resourceCache = resourceCache; this.period = period; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index b2c3fdff78..4a99ce99bd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -9,11 +9,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class PollingEventSource extends CachingEventSource { +public class PollingEventSource extends CachingEventSource { private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); @@ -22,8 +23,8 @@ public class PollingEventSource extends CachingEventSource { private final long period; public PollingEventSource(Supplier> supplier, - long period, Cache cache) { - super(cache); + long period, Cache cache, Class

resourceClass) { + super(cache, resourceClass); this.supplierToPoll = supplier; this.period = period; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index cc362f8346..0a5ec00590 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.AggregateResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java index 15fcca7253..79af5de7d7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -21,18 +22,20 @@ class CachingEventSourceTest { - private CachingEventSource cachingEventSource; - private Cache cache; - private EventHandler eventHandlerMock = mock(EventHandler.class); + private CachingEventSource cachingEventSource; + private final EventHandler eventHandler = mock(EventHandler.class); @BeforeEach public void setup() { CachingProvider cachingProvider = new CaffeineCachingProvider(); CacheManager cacheManager = cachingProvider.getCacheManager(); - cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); + Cache cache = cacheManager.createCache("test-caching", + new MutableConfiguration<>()); cachingEventSource = new SimpleCachingEventSource(cache); - cachingEventSource.setEventHandler(eventHandlerMock); + EventSourceRegistry registry = mock(EventSourceRegistry.class); + when(registry.getEventHandler()).thenReturn(eventHandler); + cachingEventSource.setEventRegistry(registry); cachingEventSource.start(); } @@ -45,7 +48,7 @@ public void tearDown() { public void putsNewResourceIntoCacheAndProducesEvent() { cachingEventSource.handleEvent(testResource1(), testResource1ID()); - verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(1)).handleEvent(eq(new Event(testResource1ID()))); assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); } @@ -57,7 +60,7 @@ public void propagatesEventIfResourceChanged() { cachingEventSource.handleEvent(res2, testResource1ID()); - verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(2)).handleEvent(eq(new Event(testResource1ID()))); assertThat(cachingEventSource.getCachedValue(testResource1ID()).get()).isEqualTo(res2); } @@ -66,7 +69,7 @@ public void noEventPropagatedIfTheResourceIsNotChanged() { cachingEventSource.handleEvent(testResource1(), testResource1ID()); cachingEventSource.handleEvent(testResource1(), testResource1ID()); - verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(1)).handleEvent(eq(new Event(testResource1ID()))); assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); } @@ -75,7 +78,7 @@ public void propagatesEventOnDeleteIfThereIsPrevResourceInCache() { cachingEventSource.handleEvent(testResource1(), testResource1ID()); cachingEventSource.handleDelete(testResource1ID()); - verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(2)).handleEvent(eq(new Event(testResource1ID()))); assertThat(cachingEventSource.getCachedValue(testResource1ID())).isNotPresent(); } @@ -83,14 +86,14 @@ public void propagatesEventOnDeleteIfThereIsPrevResourceInCache() { public void noEventOnDeleteIfResourceWasNotInCacheBefore() { cachingEventSource.handleDelete(testResource1ID()); - verify(eventHandlerMock, times(0)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(0)).handleEvent(eq(new Event(testResource1ID()))); } public static class SimpleCachingEventSource - extends CachingEventSource { + extends CachingEventSource { public SimpleCachingEventSource(Cache cache) { - super(cache); + super(cache, HasMetadata.class); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 68c5b4f084..4b3fcb159a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -18,6 +18,7 @@ import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.eq; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index 6eb4b98e3b..fda7c39d95 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -13,6 +13,7 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -29,12 +30,12 @@ class PerResourcePollingEventSourceTest { public static final int PERIOD = 80; private PerResourcePollingEventSource pollingEventSource; - private PerResourcePollingEventSource.ResourceSupplier supplier = + private final PerResourcePollingEventSource.ResourceSupplier supplier = mock(PerResourcePollingEventSource.ResourceSupplier.class); - private ResourceCache resourceCache = mock(ResourceCache.class); + private final ResourceCache resourceCache = mock(ResourceCache.class); private Cache cache; - private EventHandler eventHandler = mock(EventHandler.class); - private TestCustomResource testCustomResource = TestUtils.testCustomResource(); + private final EventHandler eventHandler = mock(EventHandler.class); + private final TestCustomResource testCustomResource = TestUtils.testCustomResource(); @BeforeEach public void setup() { @@ -47,8 +48,11 @@ public void setup() { .thenReturn(Optional.of(SampleExternalResource.testResource1())); pollingEventSource = - new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache); - pollingEventSource.setEventHandler(eventHandler); + new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache, + TestCustomResource.class); + EventSourceRegistry registry = mock(EventSourceRegistry.class); + when(registry.getEventHandler()).thenReturn(eventHandler); + pollingEventSource.setEventRegistry(registry); } @Test @@ -64,8 +68,11 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { @Test public void registeringTaskOnAPredicate() throws InterruptedException { pollingEventSource = new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache, - testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1); - pollingEventSource.setEventHandler(eventHandler); + testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1, + TestCustomResource.class); + EventSourceRegistry registry = mock(EventSourceRegistry.class); + when(registry.getEventHandler()).thenReturn(eventHandler); + pollingEventSource.setEventRegistry(registry); pollingEventSource.start(); pollingEventSource.onResourceCreated(testCustomResource); Thread.sleep(2 * PERIOD); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java index 177760f94e..0e0441ce11 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java @@ -13,8 +13,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; @@ -24,19 +26,22 @@ class PollingEventSourceTest { - private PollingEventSource pollingEventSource; - private Supplier> supplier = mock(Supplier.class); - private Cache cache; - private EventHandler eventHandler = mock(EventHandler.class); + private PollingEventSource pollingEventSource; + private final Supplier> supplier = mock(Supplier.class); + private final EventHandler eventHandler = mock(EventHandler.class); @BeforeEach public void setup() { CachingProvider cachingProvider = new CaffeineCachingProvider(); CacheManager cacheManager = cachingProvider.getCacheManager(); - cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); + Cache cache = cacheManager.createCache("test-caching", + new MutableConfiguration<>()); - pollingEventSource = new PollingEventSource<>(supplier, 50, cache); - pollingEventSource.setEventHandler(eventHandler); + pollingEventSource = new PollingEventSource<>(supplier, 50, cache, HasMetadata.class); + EventSourceRegistry registry = mock(EventSourceRegistry.class); + when(registry.getEventHandler()).thenReturn(eventHandler); + pollingEventSource.setEventRegistry(registry); + pollingEventSource.start(); } @AfterEach diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 65372bceac..adf6d87ad7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 33b8df52df..f0bd43fcd8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -19,6 +19,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; @@ -49,7 +50,8 @@ public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig my } @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + public void prepareEventSources(EventSourceRegistry eventSourceRegistry, + Cloner cloner) { CachingProvider cachingProvider = new CaffeineCachingProvider(); CacheManager cacheManager = cachingProvider.getCacheManager(); Cache schemaCache = @@ -58,7 +60,7 @@ public void prepareEventSources(EventSourceRegistry eventSourceRegi perResourcePollingEventSource = new PerResourcePollingEventSource<>(new SchemaPollingResourceSupplier(mysqlDbConfig), eventSourceRegistry.getControllerResourceEventSource().getResourceCache(), POLL_PERIOD, - schemaCache); + schemaCache, MySQLSchema.class); eventSourceRegistry.registerEventSource(perResourcePollingEventSource); } @@ -169,6 +171,4 @@ private void createSecret(MySQLSchema schema, String password, String secretName .inNamespace(schema.getMetadata().getNamespace()) .create(credentialsSecret); } - - }