From ef9d3256e36f7d2791557b2fe2a46ebd03f88962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 23 Nov 2022 09:04:10 +0100 Subject: [PATCH 01/14] fix: deprecate install webhook (#1616) --- .../src/main/java/io/javaoperatorsdk/operator/Operator.java | 1 + .../io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java | 1 - .../operator/sample/LeaderElectionTestOperator.java | 1 - .../io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java | 1 - .../java/io/javaoperatorsdk/operator/sample/TomcatOperator.java | 1 - .../java/io/javaoperatorsdk/operator/sample/WebPageOperator.java | 1 - 6 files changed, 1 insertion(+), 5 deletions(-) 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 37a1a501d0..0ac158496f 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 @@ -72,6 +72,7 @@ public Operator(KubernetesClient kubernetesClient, ConfigurationService configur } /** Adds a shutdown hook that automatically calls {@link #stop()} when the app shuts down. */ + @Deprecated(forRemoval = true) public void installShutdownHook() { if (!leaderElectionManager.isLeaderElectionEnabled()) { Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java index f28a6b968f..bddfe2c428 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java @@ -211,7 +211,6 @@ Operator startOperator(boolean stopOnInformerErrorDuringStartup, boolean addStop } }); operator.register(reconciler); - operator.installShutdownHook(); operator.start(); return operator; } diff --git a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java index 6bb171cfd5..262c0a7c70 100644 --- a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java @@ -27,7 +27,6 @@ public static void main(String[] args) { new Operator(client, c -> c.withLeaderElectionConfiguration(leaderElectionConfiguration)); operator.register(new LeaderElectionTestReconciler(identity)); - operator.installShutdownHook(); operator.start(); } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index b2fb0aab65..4128dd0ea8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -34,7 +34,6 @@ public static void main(String[] args) throws IOException { configOverrider -> configOverrider.replacingNamedDependentResourceConfig( SchemaDependentResource.NAME, new ResourcePollerConfig(300, MySQLDbConfig.loadFromEnvironmentVars()))); - operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 3973379769..054626eb8b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -22,7 +22,6 @@ public static void main(String[] args) throws IOException { Operator operator = new Operator(client); operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); - operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index e86bcbb120..a157df82f3 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -34,7 +34,6 @@ public static void main(String[] args) throws IOException { } else { operator.register(new WebPageStandaloneDependentsReconciler(client)); } - operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); From c0d64c8c217659d57d2377c73de167cd9045420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 23 Nov 2022 12:40:37 +0100 Subject: [PATCH 02/14] feat: runtime info for health probes (#1594) --- docs/documentation/features.md | 12 +++ .../io/javaoperatorsdk/operator/Operator.java | 11 ++- .../operator/RegisteredController.java | 5 ++ .../javaoperatorsdk/operator/RuntimeInfo.java | 81 +++++++++++++++++++ .../operator/health/ControllerHealthInfo.java | 50 ++++++++++++ .../health/EventSourceHealthIndicator.java | 6 ++ .../health/InformerHealthIndicator.java | 17 ++++ ...merWrappingEventSourceHealthIndicator.java | 22 +++++ .../operator/health/Status.java | 11 +++ .../operator/processing/Controller.java | 8 ++ .../processing/event/EventSourceManager.java | 6 ++ .../processing/event/EventSources.java | 7 ++ .../processing/event/source/EventSource.java | 9 ++- .../source/informer/InformerManager.java | 25 +++--- .../source/informer/InformerWrapper.java | 28 ++++++- .../informer/ManagedInformerEventSource.java | 21 ++++- .../PerResourcePollingEventSource.java | 2 + .../source/polling/PollingEventSource.java | 23 ++++-- .../polling/PollingEventSourceTest.java | 27 ++++++- .../operator/InformerRelatedBehaviorITS.java | 45 ++++++++++- ...InformerRelatedBehaviorTestReconciler.java | 9 ++- sample-operators/webpage/k8s/operator.yaml | 12 ++- .../operator/sample/LivenessHandler.java | 29 +++++++ .../operator/sample/StartupHandler.java | 37 +++++++++ .../operator/sample/WebPageOperator.java | 16 ++-- 25 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 195c4ec55e..b4cbd1f2fd 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -699,6 +699,18 @@ leader left off should one of them become elected leader. See sample configuration in the [E2E test](https://github.com/java-operator-sdk/java-operator-sdk/blob/8865302ac0346ee31f2d7b348997ec2913d5922b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java#L21-L23) . +## Runtime Info + +[RuntimeInfo](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java#L16-L16) +is used mainly to check the actual health of event sources. Based on this information it is easy to implement custom +liveness probes. + +[stopOnInformerErrorDuringStartup](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) +setting, where this flag usually needs to be set to false, in order to control the exact liveness properties. + +See also an example implementation in the +[WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43) + ## Monitoring with Micrometer ## Automatic Generation of CRDs 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 0ac158496f..60808c899e 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 @@ -90,12 +90,11 @@ public KubernetesClient getKubernetesClient() { * where there is no obvious entrypoint to the application which can trigger the injection process * and start the cluster monitoring processes. */ - public void start() { + public synchronized void start() { try { if (started) { return; } - started = true; controllerManager.shouldStart(); final var version = ConfigurationServiceProvider.instance().getVersion(); log.info( @@ -111,6 +110,7 @@ public void start() { // the leader election would start subsequently the processor if on controllerManager.start(!leaderElectionManager.isLeaderElectionEnabled()); leaderElectionManager.start(); + started = true; } catch (Exception e) { log.error("Error starting operator", e); stop(); @@ -217,4 +217,11 @@ public int getRegisteredControllersNumber() { return controllerManager.size(); } + public RuntimeInfo getRuntimeInfo() { + return new RuntimeInfo(this); + } + + boolean isStarted() { + return started; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java index 832c2df6ee..88cd0123b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java @@ -3,7 +3,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; +import io.javaoperatorsdk.operator.health.ControllerHealthInfo; public interface RegisteredController

extends NamespaceChangeable { + ControllerConfiguration

getConfiguration(); + + ControllerHealthInfo getControllerHealthInfo(); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java new file mode 100644 index 0000000000..961e519d62 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java @@ -0,0 +1,81 @@ +package io.javaoperatorsdk.operator; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; + +/** + * RuntimeInfo in general is available when operator is fully started. You can use "isStarted" to + * check that. + */ +@SuppressWarnings("rawtypes") +public class RuntimeInfo { + + private static final Logger log = LoggerFactory.getLogger(RuntimeInfo.class); + + private final Set registeredControllers; + private final Operator operator; + + public RuntimeInfo(Operator operator) { + this.registeredControllers = operator.getRegisteredControllers(); + this.operator = operator; + } + + public boolean isStarted() { + return operator.isStarted(); + } + + public Set getRegisteredControllers() { + checkIfStarted(); + return registeredControllers; + } + + private void checkIfStarted() { + if (!isStarted()) { + log.warn( + "Operator not started yet while accessing runtime info, this might lead to an unreliable behavior"); + } + } + + public boolean allEventSourcesAreHealthy() { + checkIfStarted(); + return registeredControllers.stream() + .filter(rc -> !rc.getControllerHealthInfo().unhealthyEventSources().isEmpty()) + .findFirst().isEmpty(); + } + + /** + * @return Aggregated Map with controller related event sources. + */ + + public Map> unhealthyEventSources() { + checkIfStarted(); + Map> res = new HashMap<>(); + for (var rc : registeredControllers) { + res.put(rc.getConfiguration().getName(), + rc.getControllerHealthInfo().unhealthyEventSources()); + } + return res; + } + + /** + * @return Aggregated Map with controller related event sources that wraps an informer. Thus, + * either a + * {@link io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource} + * or an + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}. + */ + public Map> unhealthyInformerWrappingEventSourceHealthIndicator() { + checkIfStarted(); + Map> res = new HashMap<>(); + for (var rc : registeredControllers) { + res.put(rc.getConfiguration().getName(), rc.getControllerHealthInfo() + .unhealthyInformerEventSourceHealthIndicators()); + } + return res; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java new file mode 100644 index 0000000000..2adb3a8508 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.health; + +import java.util.Map; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; + +@SuppressWarnings("rawtypes") +public class ControllerHealthInfo { + + private EventSourceManager eventSourceManager; + + public ControllerHealthInfo(EventSourceManager eventSourceManager) { + this.eventSourceManager = eventSourceManager; + } + + public Map eventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map unhealthyEventSources() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue().getStatus() == Status.UNHEALTHY) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map informerEventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue() instanceof InformerWrappingEventSourceHealthIndicator) + .collect(Collectors.toMap(Map.Entry::getKey, + e -> (InformerWrappingEventSourceHealthIndicator) e.getValue())); + + } + + /** + * @return Map with event sources that wraps an informer. Thus, either a + * {@link io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource} + * or an + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}. + */ + public Map unhealthyInformerEventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue().getStatus() == Status.UNHEALTHY) + .filter(e -> e.getValue() instanceof InformerWrappingEventSourceHealthIndicator) + .collect(Collectors.toMap(Map.Entry::getKey, + e -> (InformerWrappingEventSourceHealthIndicator) e.getValue())); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java new file mode 100644 index 0000000000..e44fcb5b72 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.health; + +public interface EventSourceHealthIndicator { + + Status getStatus(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java new file mode 100644 index 0000000000..afd8b61bed --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.health; + +public interface InformerHealthIndicator extends EventSourceHealthIndicator { + + boolean hasSynced(); + + boolean isWatching(); + + boolean isRunning(); + + @Override + default Status getStatus() { + return isRunning() && hasSynced() && isWatching() ? Status.HEALTHY : Status.UNHEALTHY; + } + + String getTargetNamespace(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java new file mode 100644 index 0000000000..5a603ad321 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.health; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; + +public interface InformerWrappingEventSourceHealthIndicator + extends EventSourceHealthIndicator { + + Map informerHealthIndicators(); + + @Override + default Status getStatus() { + var nonUp = informerHealthIndicators().values().stream() + .filter(i -> i.getStatus() != Status.HEALTHY).findAny(); + + return nonUp.isPresent() ? Status.UNHEALTHY : Status.HEALTHY; + } + + ResourceConfiguration getInformerConfiguration(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java new file mode 100644 index 0000000000..d3a300b7d8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.health; + +public enum Status { + + HEALTHY, UNHEALTHY, + /** + * For event sources where it cannot be determined if it is healthy ot not. + */ + UNKNOWN + +} 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 2d9e6b4007..7446f230bf 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 @@ -39,6 +39,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; +import io.javaoperatorsdk.operator.health.ControllerHealthInfo; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; import io.javaoperatorsdk.operator.processing.event.EventProcessor; @@ -67,6 +68,7 @@ public class Controller

private final GroupVersionKind associatedGVK; private final EventProcessor

eventProcessor; + private final ControllerHealthInfo controllerHealthInfo; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -87,6 +89,7 @@ public Controller(Reconciler

reconciler, eventSourceManager = new EventSourceManager<>(this); eventProcessor = new EventProcessor<>(eventSourceManager); eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); + controllerHealthInfo = new ControllerHealthInfo(eventSourceManager); final var context = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); @@ -289,6 +292,11 @@ public ControllerConfiguration

getConfiguration() { return configuration; } + @Override + public ControllerHealthInfo getControllerHealthInfo() { + return controllerHealthInfo; + } + public KubernetesClient getClient() { return kubernetesClient; } 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 8e9c981031..6a6aae471a 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,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.*; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; @@ -198,6 +199,11 @@ public Set getRegisteredEventSources() { .collect(Collectors.toCollection(LinkedHashSet::new)); } + public Map allEventSources() { + return eventSources.allNamedEventSources().collect(Collectors.toMap(NamedEventSource::name, + NamedEventSource::original)); + } + @SuppressWarnings("unused") public Stream getNamedEventSourcesStream() { return eventSources.flatMappedSources(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java index c17843de2f..e140544586 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java @@ -49,6 +49,13 @@ public Stream additionalNamedEventSources() { flatMappedSources()); } + public Stream allNamedEventSources() { + return Stream.concat(Stream.of(namedControllerResourceEventSource(), + new NamedEventSource(retryAndRescheduleTimerEventSource, + RETRY_RESCHEDULE_TIMER_EVENT_SOURCE_NAME)), + flatMappedSources()); + } + Stream additionalEventSources() { return Stream.concat( Stream.of(retryEventSource()).filter(Objects::nonNull), 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 76c8ca164d..ec2783f797 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,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -10,7 +12,7 @@ * your reconciler implement * {@link io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer}. */ -public interface EventSource extends LifecycleAware { +public interface EventSource extends LifecycleAware, EventSourceHealthIndicator { /** * Sets the {@link EventHandler} that is linked to your reconciler when this EventSource is @@ -23,4 +25,9 @@ public interface EventSource extends LifecycleAware { default EventSourceStartPriority priority() { return EventSourceStartPriority.DEFAULT; } + + @Override + default Status getStatus() { + return Status.UNKNOWN; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index ba7aa9f67f..c996aa2c0e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -25,15 +25,17 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACES; + public class InformerManager> implements LifecycleAware, IndexerResourceCache { - private static final String ALL_NAMESPACES_MAP_KEY = "allNamespaces"; private static final Logger log = LoggerFactory.getLogger(InformerManager.class); private final Map> sources = new ConcurrentHashMap<>(); @@ -63,7 +65,7 @@ void initSources(MixedOperation, Resource> clien final var filteredBySelectorClient = client.inAnyNamespace().withLabelSelector(labelSelector); final var source = - createEventSource(filteredBySelectorClient, eventHandler, ALL_NAMESPACES_MAP_KEY); + createEventSource(filteredBySelectorClient, eventHandler, WATCH_ALL_NAMESPACES); log.debug("Registered {} -> {} for any namespace", this, source); } else { targetNamespaces.forEach( @@ -105,10 +107,11 @@ public void changeNamespaces(Set namespaces) { private InformerWrapper createEventSource( FilterWatchListDeletable, Resource> filteredBySelectorClient, - ResourceEventHandler eventHandler, String key) { - var source = new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0)); + ResourceEventHandler eventHandler, String namespaceIdentifier) { + var source = + new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0), namespaceIdentifier); source.addEventHandler(eventHandler); - sources.put(key, source); + sources.put(namespaceIdentifier, source); return source; } @@ -135,7 +138,7 @@ public Stream list(Predicate predicate) { @Override public Stream list(String namespace, Predicate predicate) { if (isWatchingAllNamespaces()) { - return getSource(ALL_NAMESPACES_MAP_KEY) + return getSource(WATCH_ALL_NAMESPACES) .map(source -> source.list(namespace, predicate)) .orElseGet(Stream::empty); } else { @@ -147,7 +150,7 @@ public Stream list(String namespace, Predicate predicate) { @Override public Optional get(ResourceID resourceID) { - return getSource(resourceID.getNamespace().orElse(ALL_NAMESPACES_MAP_KEY)) + return getSource(resourceID.getNamespace().orElse(WATCH_ALL_NAMESPACES)) .flatMap(source -> source.get(resourceID)) .map(cloner::clone); } @@ -158,11 +161,11 @@ public Stream keys() { } private boolean isWatchingAllNamespaces() { - return sources.containsKey(ALL_NAMESPACES_MAP_KEY); + return sources.containsKey(WATCH_ALL_NAMESPACES); } private Optional> getSource(String namespace) { - namespace = isWatchingAllNamespaces() || namespace == null ? ALL_NAMESPACES_MAP_KEY : namespace; + namespace = isWatchingAllNamespaces() || namespace == null ? WATCH_ALL_NAMESPACES : namespace; return Optional.ofNullable(sources.get(namespace)); } @@ -187,4 +190,8 @@ public String toString() { + configuration.getEffectiveNamespaces() + (selector != null ? " selector: " + selector : ""); } + + public Map informerHealthIndicators() { + return Collections.unmodifiableMap(sources); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index de81815a96..8809022e95 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -21,21 +21,25 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; class InformerWrapper - implements LifecycleAware, IndexerResourceCache { + implements LifecycleAware, IndexerResourceCache, InformerHealthIndicator { private static final Logger log = LoggerFactory.getLogger(InformerWrapper.class); private final SharedIndexInformer informer; private final Cache cache; + private final String namespaceIdentifier; - public InformerWrapper(SharedIndexInformer informer) { + public InformerWrapper(SharedIndexInformer informer, String namespaceIdentifier) { this.informer = informer; + this.namespaceIdentifier = namespaceIdentifier; this.cache = (Cache) informer.getStore(); + } @Override @@ -156,4 +160,24 @@ public String toString() { private String informerInfo() { return "InformerWrapper [" + versionedFullResourceName() + "]"; } + + @Override + public boolean hasSynced() { + return informer.hasSynced(); + } + + @Override + public boolean isWatching() { + return informer.isWatching(); + } + + @Override + public boolean isRunning() { + return informer.isRunning(); + } + + @Override + public String getTargetNamespace() { + return namespaceIdentifier; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 7fdf3c5e56..34e1c9f876 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -19,6 +19,9 @@ import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; +import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.Cache; @@ -29,17 +32,19 @@ public abstract class ManagedInformerEventSource implements ResourceEventHandler, Cache, IndexerResourceCache, RecentOperationCacheFiller, - NamespaceChangeable, Configurable { + NamespaceChangeable, InformerWrappingEventSourceHealthIndicator, Configurable { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); protected TemporaryResourceCache temporaryResourceCache = new TemporaryResourceCache<>(this); protected InformerManager cache = new InformerManager<>(); + protected C configuration; protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration) { super(configuration.getResourceClass()); manager().initSources(client, configuration, this); + this.configuration = configuration; } @Override @@ -136,6 +141,20 @@ public Stream list(Predicate predicate) { return cache.list(predicate); } + @Override + public Map informerHealthIndicators() { + return cache.informerHealthIndicators(); + } + + @Override + public Status getStatus() { + return InformerWrappingEventSourceHealthIndicator.super.getStatus(); + } + + @Override + public ResourceConfiguration getInformerConfiguration() { + return configuration; + } @Override public C configuration() { return manager().configuration(); 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 44bab7a624..8bf5c50fd4 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 @@ -40,6 +40,7 @@ public class PerResourcePollingEventSource private final long period; private final Set fetchedForPrimaries = ConcurrentHashMap.newKeySet(); + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, Cache

resourceCache, long period, Class resourceClass) { this(resourceFetcher, resourceCache, period, null, resourceClass, @@ -152,4 +153,5 @@ public void stop() throws OperatorException { super.stop(); timer.cancel(); } + } 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 94efbf25aa..9ef889ecb6 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 @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; @@ -45,6 +47,7 @@ public class PollingEventSource private final Timer timer = new Timer(); private final GenericResourceFetcher genericResourceFetcher; private final long period; + private final AtomicBoolean healthy = new AtomicBoolean(true); public PollingEventSource( GenericResourceFetcher supplier, @@ -73,11 +76,17 @@ public void start() throws OperatorException { new TimerTask() { @Override public void run() { - if (!isRunning()) { - log.debug("Event source not yet started. Will not run."); - return; + try { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run."); + return; + } + getStateAndFillCache(); + healthy.set(true); + } catch (RuntimeException e) { + healthy.set(false); + log.error("Error during polling.", e); } - getStateAndFillCache(); } }, period, @@ -89,7 +98,6 @@ protected synchronized void getStateAndFillCache() { handleResources(values); } - public interface GenericResourceFetcher { Map> fetchResources(); } @@ -99,4 +107,9 @@ public void stop() throws OperatorException { super.stop(); timer.cancel(); } + + @Override + public Status getStatus() { + return healthy.get() ? Status.HEALTHY : Status.UNHEALTHY; + } } 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 0a777f0cbd..605922f06b 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -8,12 +9,15 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.*; class PollingEventSourceTest @@ -21,11 +25,12 @@ class PollingEventSourceTest AbstractEventSourceTestBase, EventHandler> { public static final int DEFAULT_WAIT_PERIOD = 100; + public static final long POLL_PERIOD = 30L; private PollingEventSource.GenericResourceFetcher resourceFetcher = mock(PollingEventSource.GenericResourceFetcher.class); private final PollingEventSource pollingEventSource = - new PollingEventSource<>(resourceFetcher, 30L, SampleExternalResource.class, + new PollingEventSource<>(resourceFetcher, POLL_PERIOD, SampleExternalResource.class, (SampleExternalResource er) -> er.getName() + "#" + er.getValue()); @BeforeEach @@ -73,6 +78,26 @@ void propagatesEventOnNewResourceForPrimary() throws InterruptedException { verify(eventHandler, times(2)).handleEvent(any()); } + @Test + void updatesHealthIndicatorBasedOnExceptionsInFetcher() throws InterruptedException { + when(resourceFetcher.fetchResources()) + .thenReturn(testResponseWithOneValue()); + pollingEventSource.start(); + assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY); + + when(resourceFetcher.fetchResources()) + // 2x - to make sure to catch the health indicator change + .thenThrow(new RuntimeException("test exception")) + .thenThrow(new RuntimeException("test exception")) + .thenReturn(testResponseWithOneValue()); + + await().pollInterval(Duration.ofMillis(POLL_PERIOD)).untilAsserted( + () -> assertThat(pollingEventSource.getStatus()).isEqualTo(Status.UNHEALTHY)); + + await() + .untilAsserted(() -> assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY)); + } + private Map> testResponseWithTwoValueForSameId() { Map> res = new HashMap<>(); res.put(primaryID1(), Set.of(testResource1(), testResource2())); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java index bddfe2c428..97f5883a08 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java @@ -14,9 +14,12 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestCustomResource; import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler; +import static io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler.CONFIG_MAP_DEPENDENT_RESOURCE; +import static io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler.INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -73,21 +76,25 @@ void startsUpWhenNoPermissionToCustomResource() { adminClient.resource(testCustomResource()).createOrReplace(); setNoCustomResourceAccess(); - startOperator(false); + var operator = startOperator(false); assertNotReconciled(); + assertRuntimeInfoNoCRPermission(operator); setFullResourcesAccess(); waitForWatchReconnect(); assertReconciled(); + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isTrue(); } + @Test void startsUpWhenNoPermissionToSecondaryResource() { adminClient.resource(testCustomResource()).createOrReplace(); setNoConfigMapAccess(); - startOperator(false); + var operator = startOperator(false); assertNotReconciled(); + assertRuntimeInfoForSecondaryPermission(operator); setFullResourcesAccess(); waitForWatchReconnect(); @@ -184,6 +191,40 @@ private void assertReconciled() { }); } + + private void assertRuntimeInfoNoCRPermission(Operator operator) { + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isFalse(); + var unhealthyEventSources = + operator.getRuntimeInfo().unhealthyEventSources() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(unhealthyEventSources).isNotEmpty(); + assertThat(unhealthyEventSources.get(ControllerResourceEventSource.class.getSimpleName())) + .isNotNull(); + var informerHealthIndicators = operator.getRuntimeInfo() + .unhealthyInformerWrappingEventSourceHealthIndicator() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(informerHealthIndicators).isNotEmpty(); + assertThat(informerHealthIndicators.get(ControllerResourceEventSource.class.getSimpleName()) + .informerHealthIndicators()) + .hasSize(1); + } + + private void assertRuntimeInfoForSecondaryPermission(Operator operator) { + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isFalse(); + var unhealthyEventSources = + operator.getRuntimeInfo().unhealthyEventSources() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(unhealthyEventSources).isNotEmpty(); + assertThat(unhealthyEventSources.get(CONFIG_MAP_DEPENDENT_RESOURCE)).isNotNull(); + var informerHealthIndicators = operator.getRuntimeInfo() + .unhealthyInformerWrappingEventSourceHealthIndicator() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(informerHealthIndicators).isNotEmpty(); + assertThat( + informerHealthIndicators.get(CONFIG_MAP_DEPENDENT_RESOURCE).informerHealthIndicators()) + .hasSize(1); + } + KubernetesClient clientUsingServiceAccount() { KubernetesClient client = new KubernetesClientBuilder() .withConfig(new ConfigBuilder() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java index baeff08478..13057d547a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java @@ -10,10 +10,17 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration(dependents = @Dependent(type = ConfigMapDependentResource.class)) +@ControllerConfiguration( + name = InformerRelatedBehaviorTestReconciler.INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER, + dependents = @Dependent( + name = InformerRelatedBehaviorTestReconciler.CONFIG_MAP_DEPENDENT_RESOURCE, + type = ConfigMapDependentResource.class)) public class InformerRelatedBehaviorTestReconciler implements Reconciler, TestExecutionInfoProvider { + public static final String INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER = + "InformerRelatedBehaviorTestReconciler"; + public static final String CONFIG_MAP_DEPENDENT_RESOURCE = "ConfigMapDependentResource"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private KubernetesClient client; diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index f4b0b027ea..d8518ab21d 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -25,18 +25,22 @@ spec: imagePullPolicy: Never ports: - containerPort: 80 - readinessProbe: + startupProbe: httpGet: - path: /health + path: /startup port: 8080 initialDelaySeconds: 1 + periodSeconds: 2 timeoutSeconds: 1 + failureThreshold: 10 livenessProbe: httpGet: - path: /health + path: /healthz port: 8080 - initialDelaySeconds: 30 + initialDelaySeconds: 5 timeoutSeconds: 1 + periodSeconds: 2 + failureThreshold: 3 --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java new file mode 100644 index 0000000000..155fc13fec --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; + +import io.javaoperatorsdk.operator.Operator; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import static io.javaoperatorsdk.operator.sample.StartupHandler.sendMessage; + +public class LivenessHandler implements HttpHandler { + + private final Operator operator; + + public LivenessHandler(Operator operator) { + this.operator = operator; + } + + // custom logic can be added here based on the health of event sources + @Override + public void handle(HttpExchange httpExchange) throws IOException { + if (operator.getRuntimeInfo().allEventSourcesAreHealthy()) { + sendMessage(httpExchange, 200, "healthy"); + } else { + sendMessage(httpExchange, 400, "an event source is not healthy"); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java new file mode 100644 index 0000000000..0cbc313273 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import io.javaoperatorsdk.operator.Operator; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class StartupHandler implements HttpHandler { + + private final Operator operator; + + public StartupHandler(Operator operator) { + this.operator = operator; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + if (operator.getRuntimeInfo().isStarted()) { + sendMessage(httpExchange, 200, "started"); + } else { + sendMessage(httpExchange, 400, "not started yet"); + } + } + + public static void sendMessage(HttpExchange httpExchange, int code, String message) + throws IOException { + try (var outputStream = httpExchange.getResponseBody()) { + var bytes = message.getBytes(StandardCharsets.UTF_8); + httpExchange.sendResponseHeaders(code, bytes.length); + outputStream.write(bytes); + outputStream.flush(); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index a157df82f3..864f97ccfe 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -1,18 +1,17 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.net.InetSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.takes.facets.fork.FkRegex; -import org.takes.facets.fork.TkFork; -import org.takes.http.Exit; -import org.takes.http.FtBasic; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.javaoperatorsdk.operator.Operator; +import com.sun.net.httpserver.HttpServer; + public class WebPageOperator { public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; public static final String WEBPAGE_CLASSIC_RECONCILER_ENV_VALUE = "classic"; @@ -24,7 +23,7 @@ public static void main(String[] args) throws IOException { log.info("WebServer Operator starting!"); KubernetesClient client = new KubernetesClientBuilder().build(); - Operator operator = new Operator(client); + Operator operator = new Operator(client, o -> o.withStopOnInformerErrorDuringStartup(false)); String reconcilerEnvVar = System.getenv(WEBPAGE_RECONCILER_ENV); if (WEBPAGE_CLASSIC_RECONCILER_ENV_VALUE.equals(reconcilerEnvVar)) { operator.register(new WebPageReconciler(client)); @@ -36,6 +35,11 @@ public static void main(String[] args) throws IOException { } operator.start(); - new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + server.createContext("/startup", new StartupHandler(operator)); + // we want to restart the operator if something goes wrong with (maybe just some) event sources + server.createContext("/healthz", new LivenessHandler(operator)); + server.setExecutor(null); + server.start(); } } From 49176a8bef3a5c0598e9bb5c76026a8b6d06c0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 28 Nov 2022 14:05:53 +0100 Subject: [PATCH 03/14] chore: own version to 4.2.0-SNAPSHOT (#1633) --- micrometer-support/pom.xml | 2 +- operator-framework-bom/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- .../processing/event/source/informer/InformerManager.java | 6 +----- .../event/source/informer/ManagedInformerEventSource.java | 3 ++- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 13 files changed, 14 insertions(+), 17 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 8378720ed8..c541810f47 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 6b22c02bbb..7c435928ae 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk operator-framework-bom - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT Operator SDK - Bill of Materials pom Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 65de6f8323..e885984058 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index c996aa2c0e..4d7d547356 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -1,10 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 34e1c9f876..43173a28f7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -32,7 +32,7 @@ public abstract class ManagedInformerEventSource implements ResourceEventHandler, Cache, IndexerResourceCache, RecentOperationCacheFiller, - NamespaceChangeable, InformerWrappingEventSourceHealthIndicator, Configurable { + NamespaceChangeable, InformerWrappingEventSourceHealthIndicator, Configurable { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); @@ -155,6 +155,7 @@ public Status getStatus() { public ResourceConfiguration getInformerConfiguration() { return configuration; } + @Override public C configuration() { return manager().configuration(); diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 891aab5c96..94ac00cacb 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 27efa90b55..7b2c68639f 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index a9070ebf06..34006aeeef 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index a0f996983d..82fdd327ac 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 888a93abb5..4f98b6c1ba 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 61e2ecae15..54c3efc2da 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 1db82bf079..8a1cc0b00e 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 31b9b6a5b8..0f847b8f6f 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-webpage-operator From abf9ebdbcedbf2c58911744f81ff97126d2adc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 1 Dec 2022 11:35:11 +0100 Subject: [PATCH 04/14] feat: follow all namespace change possible (#1639) --- .../operator/processing/Controller.java | 7 +- .../source/informer/InformerManager.java | 27 +++-- .../operator/ChangeNamespaceIT.java | 110 ++++++++++++------ 3 files changed, 95 insertions(+), 49 deletions(-) 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 7446f230bf..f5f892719c 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 @@ -358,10 +358,13 @@ private void validateCRDWithLocalModelIfRequired(Class

resClass, String contr } public void changeNamespaces(Set namespaces) { - if (namespaces.contains(Constants.WATCH_ALL_NAMESPACES) - || namespaces.contains(WATCH_CURRENT_NAMESPACE)) { + if (namespaces.contains(WATCH_CURRENT_NAMESPACE)) { throw new OperatorException("Unexpected value in target namespaces: " + namespaces); } + if (namespaces.contains(Constants.WATCH_ALL_NAMESPACES) && namespaces.size() > 1) { + throw new OperatorException( + "Watching all namespaces, but additional specific namespace is present"); + } eventProcessor.stop(); eventSourceManager.changeNamespaces(namespaces); eventProcessor.start(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 4d7d547356..d51301c385 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -58,17 +58,12 @@ void initSources(MixedOperation, Resource> clien final var labelSelector = configuration.getLabelSelector(); if (ResourceConfiguration.allNamespacesWatched(targetNamespaces)) { - final var filteredBySelectorClient = - client.inAnyNamespace().withLabelSelector(labelSelector); - final var source = - createEventSource(filteredBySelectorClient, eventHandler, WATCH_ALL_NAMESPACES); + var source = createEventSourceForNamespace(WATCH_ALL_NAMESPACES); log.debug("Registered {} -> {} for any namespace", this, source); } else { targetNamespaces.forEach( ns -> { - final var source = - createEventSource(client.inNamespace(ns).withLabelSelector(labelSelector), - eventHandler, ns); + final var source = createEventSourceForNamespace(ns); log.debug("Registered {} -> {} for namespace: {}", this, source, ns); }); @@ -87,10 +82,7 @@ public void changeNamespaces(Set namespaces) { namespaces.forEach(ns -> { if (!sources.containsKey(ns)) { - final var source = - createEventSource( - client.inNamespace(ns).withLabelSelector(configuration.getLabelSelector()), - eventHandler, ns); + final InformerWrapper source = createEventSourceForNamespace(ns); source.addIndexers(this.indexers); source.start(); log.debug("Registered new {} -> {} for namespace: {}", this, source, @@ -100,6 +92,19 @@ public void changeNamespaces(Set namespaces) { } + private InformerWrapper createEventSourceForNamespace(String namespace) { + final InformerWrapper source; + if (namespace.equals(WATCH_ALL_NAMESPACES)) { + final var filteredBySelectorClient = + client.inAnyNamespace().withLabelSelector(configuration.getLabelSelector()); + source = createEventSource(filteredBySelectorClient, eventHandler, WATCH_ALL_NAMESPACES); + } else { + source = createEventSource( + client.inNamespace(namespace).withLabelSelector(configuration.getLabelSelector()), + eventHandler, namespace); + } + return source; + } private InformerWrapper createEventSource( FilterWatchListDeletable, Resource> filteredBySelectorClient, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java index 56eda890a1..c7d3b04c4a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java @@ -4,6 +4,8 @@ import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,6 +14,7 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.sample.changenamespace.ChangeNamespaceTestCustomResource; import io.javaoperatorsdk.operator.sample.changenamespace.ChangeNamespaceTestReconciler; @@ -25,63 +28,98 @@ class ChangeNamespaceIT { public static final String TEST_RESOURCE_NAME_2 = "test2"; public static final String TEST_RESOURCE_NAME_3 = "test3"; public static final String ADDITIONAL_TEST_NAMESPACE = "additional-test-namespace"; + @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder().withReconciler(new ChangeNamespaceTestReconciler()) .build(); + @BeforeEach + void setup() { + client().namespaces().resource(additionalTestNamespace()).create(); + } + + @AfterEach + void cleanup() { + client().namespaces().resource(additionalTestNamespace()).delete(); + } + @SuppressWarnings("rawtypes") @Test void addNewAndRemoveOldNamespaceTest() { - try { - var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); - var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); + var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + + assertReconciled(reconciler, defaultNamespaceResource); + var resourceInAdditionalTestNamespace = createResourceInAdditionalNamespace(); + + assertNotReconciled(reconciler, resourceInAdditionalTestNamespace); + // adding additional namespace + RegisteredController registeredController = + operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); + registeredController + .changeNamespaces(Set.of(operator.getNamespace(), ADDITIONAL_TEST_NAMESPACE)); + + assertReconciled(reconciler, resourceInAdditionalTestNamespace); - await().pollDelay(Duration.ofMillis(100)).untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); + // removing a namespace + registeredController.changeNamespaces(Set.of(ADDITIONAL_TEST_NAMESPACE)); - client().namespaces().create(additionalTestNamespace()); - var resourceInAdditionalTestNamespace = createResourceInTestNamespace(); - await().pollDelay(Duration.ofMillis(200)).untilAsserted( - () -> assertThat( - reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) - .isZero()); + var newResourceInDefaultNamespace = operator.create(customResource(TEST_RESOURCE_NAME_3)); + assertNotReconciled(reconciler, newResourceInDefaultNamespace); - // adding additional namespace - RegisteredController registeredController = - operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); - registeredController - .changeNamespaces(Set.of(operator.getNamespace(), ADDITIONAL_TEST_NAMESPACE)); + ConfigMap firstMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME_1); + firstMap.setData(Map.of("data", "newdata")); + operator.replace(firstMap); + assertReconciled(reconciler, defaultNamespaceResource); + } + + @Test + void changeToWatchAllNamespaces() { + var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); + var resourceInAdditionalTestNamespace = createResourceInAdditionalNamespace(); + + assertNotReconciled(reconciler, resourceInAdditionalTestNamespace); - await().untilAsserted( - () -> assertThat( - reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) - .isEqualTo(2)); + var registeredController = + operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); - // removing a namespace - registeredController.changeNamespaces(Set.of(ADDITIONAL_TEST_NAMESPACE)); + registeredController + .changeNamespaces(Set.of(Constants.WATCH_ALL_NAMESPACES)); - var newResourceInDefaultNamespace = operator.create(customResource(TEST_RESOURCE_NAME_3)); - await().pollDelay(Duration.ofMillis(200)) - .untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(newResourceInDefaultNamespace)).isZero()); + assertReconciled(reconciler, resourceInAdditionalTestNamespace); + registeredController.changeNamespaces(Set.of(operator.getNamespace())); - ConfigMap firstMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME_1); - firstMap.setData(Map.of("data", "newdata")); - operator.replace(firstMap); + var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + var resource2InAdditionalResource = createResourceInAdditionalNamespace(TEST_RESOURCE_NAME_3); + assertReconciled(reconciler, defaultNamespaceResource); + assertNotReconciled(reconciler, resource2InAdditionalResource); + } + + private static void assertReconciled(ChangeNamespaceTestReconciler reconciler, + ChangeNamespaceTestCustomResource resourceInAdditionalTestNamespace) { + await().untilAsserted( + () -> assertThat( + reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) + .isEqualTo(2)); + } - await().untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); + private static void assertNotReconciled(ChangeNamespaceTestReconciler reconciler, + ChangeNamespaceTestCustomResource resourceInAdditionalTestNamespace) { + await().pollDelay(Duration.ofMillis(200)).untilAsserted( + () -> assertThat( + reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) + .isZero()); + } - } finally { - client().namespaces().delete(additionalTestNamespace()); - } + private ChangeNamespaceTestCustomResource createResourceInAdditionalNamespace() { + return createResourceInAdditionalNamespace(TEST_RESOURCE_NAME_2); } - private ChangeNamespaceTestCustomResource createResourceInTestNamespace() { - var res = customResource(TEST_RESOURCE_NAME_2); + private ChangeNamespaceTestCustomResource createResourceInAdditionalNamespace(String name) { + var res = customResource(name); return client().resources(ChangeNamespaceTestCustomResource.class) .inNamespace(ADDITIONAL_TEST_NAMESPACE) .create(res); From 8cd40dd07e6d89dd79119db39555550c2d6879a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 5 Dec 2022 08:46:19 +0100 Subject: [PATCH 05/14] feat: metrics contains the whole resource (#1645) --- .../micrometer/MicrometerMetrics.java | 14 +++-- .../operator/api/monitoring/Metrics.java | 58 ++++++++----------- .../processing/event/EventProcessor.java | 54 ++++++++--------- .../processing/event/EventProcessorTest.java | 2 +- 4 files changed, 59 insertions(+), 69 deletions(-) diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index fff9cf427f..fb7d064c26 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Optional; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -84,10 +85,10 @@ public void cleanupDoneFor(ResourceID resourceID, Map metadata) } @Override - public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable, + public void reconcileCustomResource(HasMetadata resource, RetryInfo retryInfoNullable, Map metadata) { Optional retryInfo = Optional.ofNullable(retryInfoNullable); - incrementCounter(resourceID, RECONCILIATIONS + "started", + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "started", metadata, RECONCILIATIONS + "retries.number", "" + retryInfo.map(RetryInfo::getAttemptCount).orElse(0), @@ -96,11 +97,11 @@ public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNu } @Override - public void finishedReconciliation(ResourceID resourceID, Map metadata) { - incrementCounter(resourceID, RECONCILIATIONS + "success", metadata); + public void finishedReconciliation(HasMetadata resource, Map metadata) { + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "success", metadata); } - public void failedReconciliation(ResourceID resourceID, Exception exception, + public void failedReconciliation(HasMetadata resource, Exception exception, Map metadata) { var cause = exception.getCause(); if (cause == null) { @@ -108,7 +109,8 @@ public void failedReconciliation(ResourceID resourceID, Exception exception, } else if (cause instanceof RuntimeException) { cause = cause.getCause() != null ? cause.getCause() : cause; } - incrementCounter(resourceID, RECONCILIATIONS + "failed", metadata, "exception", + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "failed", metadata, + "exception", cause.getClass().getSimpleName()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java index be608b098e..31ffda96f8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java @@ -29,56 +29,39 @@ public interface Metrics { */ default void receivedEvent(Event event, Map metadata) {} - /** - * - * @deprecated Use (and implement) {@link #receivedEvent(Event, Map)} instead - */ - @Deprecated - default void receivedEvent(Event event) { - receivedEvent(event, Collections.emptyMap()); - } - - /** - * - * @deprecated Use (and implement) {@link #reconcileCustomResource(ResourceID, RetryInfo, Map)} - * instead - */ - @Deprecated - default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) { - reconcileCustomResource(resourceID, retryInfo, Collections.emptyMap()); - } + @Deprecated(forRemoval = true) + default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo, + Map metadata) {} /** * Called right before a resource is dispatched to the ExecutorService for reconciliation. * - * @param resourceID the {@link ResourceID} associated with the resource + * @param resource the associated with the resource * @param retryInfo the current retry state information for the reconciliation request * @param metadata metadata associated with the resource being processed */ - default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo, - Map metadata) {} - - /** - * - * @deprecated Use (and implement) {@link #failedReconciliation(ResourceID, Exception, Map)} - * instead - */ - @Deprecated - default void failedReconciliation(ResourceID resourceID, Exception exception) { - failedReconciliation(resourceID, exception, Collections.emptyMap()); + default void reconcileCustomResource(HasMetadata resource, RetryInfo retryInfo, + Map metadata) { + reconcileCustomResource(ResourceID.fromResource(resource), retryInfo, metadata); } + @Deprecated(forRemoval = true) + default void failedReconciliation(ResourceID resourceID, Exception exception, + Map metadata) {} + /** * Called when a precedent reconciliation for the resource associated with the specified * {@link ResourceID} resulted in the provided exception, resulting in a retry of the * reconciliation. * - * @param resourceID the {@link ResourceID} associated with the resource being processed + * @param resource the {@link ResourceID} associated with the resource being processed * @param exception the exception that caused the failed reconciliation resulting in a retry * @param metadata metadata associated with the resource being processed */ - default void failedReconciliation(ResourceID resourceID, Exception exception, - Map metadata) {} + default void failedReconciliation(HasMetadata resource, Exception exception, + Map metadata) { + failedReconciliation(ResourceID.fromResource(resource), exception, metadata); + } /** * @@ -107,16 +90,21 @@ default void finishedReconciliation(ResourceID resourceID) { finishedReconciliation(resourceID, Collections.emptyMap()); } + @Deprecated(forRemoval = true) + default void finishedReconciliation(ResourceID resourceID, Map metadata) {} + /** * Called when the * {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)} * method of the Reconciler associated with the resource associated with the specified * {@link ResourceID} has sucessfully finished. * - * @param resourceID the {@link ResourceID} associated with the resource being processed + * @param resource the {@link ResourceID} associated with the resource being processed * @param metadata metadata associated with the resource being processed */ - default void finishedReconciliation(ResourceID resourceID, Map metadata) {} + default void finishedReconciliation(HasMetadata resource, Map metadata) { + finishedReconciliation(ResourceID.fromResource(resource), metadata); + } /** * Encapsulates the information about a controller execution i.e. a call to either 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 e7a600cf2c..57679f48ca 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 @@ -32,25 +32,25 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -public class EventProcessor implements EventHandler, LifecycleAware { +public class EventProcessor

implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); private static final long MINIMAL_RATE_LIMIT_RESCHEDULE_DURATION = 50; private volatile boolean running; private final ControllerConfiguration controllerConfiguration; - private final ReconciliationDispatcher reconciliationDispatcher; + private final ReconciliationDispatcher

reconciliationDispatcher; private final Retry retry; private final ExecutorService executor; private final Metrics metrics; - private final Cache cache; - private final EventSourceManager eventSourceManager; + private final Cache

cache; + private final EventSourceManager

eventSourceManager; private final RateLimiter rateLimiter; private final ResourceStateManager resourceStateManager = new ResourceStateManager(); private final Map metricsMetadata; - public EventProcessor(EventSourceManager eventSourceManager) { + public EventProcessor(EventSourceManager

eventSourceManager) { this( eventSourceManager.getController().getConfiguration(), eventSourceManager.getControllerResourceEventSource(), @@ -63,8 +63,8 @@ public EventProcessor(EventSourceManager eventSourceManager) { @SuppressWarnings("rawtypes") EventProcessor( ControllerConfiguration controllerConfiguration, - ReconciliationDispatcher reconciliationDispatcher, - EventSourceManager eventSourceManager, + ReconciliationDispatcher

reconciliationDispatcher, + EventSourceManager

eventSourceManager, Metrics metrics) { this( controllerConfiguration, @@ -78,11 +78,11 @@ public EventProcessor(EventSourceManager eventSourceManager) { @SuppressWarnings({"rawtypes", "unchecked"}) private EventProcessor( ControllerConfiguration controllerConfiguration, - Cache cache, + Cache

cache, ExecutorService executor, - ReconciliationDispatcher reconciliationDispatcher, + ReconciliationDispatcher

reconciliationDispatcher, Metrics metrics, - EventSourceManager eventSourceManager) { + EventSourceManager

eventSourceManager) { this.controllerConfiguration = controllerConfiguration; this.running = false; this.executor = @@ -136,7 +136,7 @@ private void submitReconciliationExecution(ResourceState state) { try { boolean controllerUnderExecution = isControllerUnderExecution(state); final var resourceID = state.getId(); - Optional maybeLatest = cache.get(resourceID); + Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution && maybeLatest.isPresent()) { var rateLimit = state.getRateLimit(); @@ -151,9 +151,9 @@ private void submitReconciliationExecution(ResourceState state) { } state.setUnderProcessing(true); final var latest = maybeLatest.get(); - ExecutionScope executionScope = new ExecutionScope<>(latest, state.getRetry()); + ExecutionScope

executionScope = new ExecutionScope<>(latest, state.getRetry()); state.unMarkEventReceived(); - metrics.reconcileCustomResource(resourceID, state.getRetry(), metricsMetadata); + metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ReconcilerExecutor(executionScope)); } else { @@ -221,7 +221,7 @@ private void handleRateLimitedSubmission(ResourceID resourceID, Duration minimal } synchronized void eventProcessingFinished( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope

executionScope, PostExecutionControl

postExecutionControl) { if (!running) { return; } @@ -244,7 +244,7 @@ synchronized void eventProcessingFinished( return; } cleanupOnSuccessfulExecution(executionScope); - metrics.finishedReconciliation(resourceID, metricsMetadata); + metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); if (state.deleteEventPresent()) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { @@ -253,12 +253,12 @@ synchronized void eventProcessingFinished( postExecutionControl .getUpdatedCustomResource() .ifPresent( - r -> { + p -> { if (!postExecutionControl.updateIsStatusPatch()) { eventSourceManager .getControllerResourceEventSource() .handleRecentResourceUpdate( - ResourceID.fromResource(r), r, executionScope.getResource()); + ResourceID.fromResource(p), p, executionScope.getResource()); } }); if (state.eventPresent()) { @@ -270,7 +270,7 @@ synchronized void eventProcessingFinished( } private void reScheduleExecutionIfInstructed( - PostExecutionControl postExecutionControl, R customResource) { + PostExecutionControl

postExecutionControl, P customResource) { postExecutionControl .getReScheduleDelay() @@ -281,7 +281,7 @@ private void reScheduleExecutionIfInstructed( }, () -> scheduleExecutionForMaxReconciliationInterval(customResource)); } - private void scheduleExecutionForMaxReconciliationInterval(R customResource) { + private void scheduleExecutionForMaxReconciliationInterval(P customResource) { this.controllerConfiguration .maxReconciliationInterval() .ifPresent(m -> { @@ -294,7 +294,7 @@ private void scheduleExecutionForMaxReconciliationInterval(R customResource) { }); } - TimerEventSource retryEventSource() { + TimerEventSource

retryEventSource() { return eventSourceManager.retryEventSource(); } @@ -304,7 +304,7 @@ TimerEventSource retryEventSource() { * according to the retry timing if there was an exception. */ private void handleRetryOnException( - ExecutionScope executionScope, Exception exception) { + ExecutionScope

executionScope, Exception exception) { final var state = getOrInitRetryExecution(executionScope); var resourceID = state.getId(); boolean eventPresent = state.eventPresent(); @@ -323,7 +323,7 @@ private void handleRetryOnException( "Scheduling timer event for retry with delay:{} for resource: {}", delay, resourceID); - metrics.failedReconciliation(resourceID, exception, metricsMetadata); + metrics.failedReconciliation(executionScope.getResource(), exception, metricsMetadata); retryEventSource().scheduleOnce(resourceID, delay); }, () -> { @@ -332,7 +332,7 @@ private void handleRetryOnException( }); } - private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { + private void cleanupOnSuccessfulExecution(ExecutionScope

executionScope) { log.debug( "Cleanup for successful execution for resource: {}", getName(executionScope.getResource())); if (isRetryConfigured()) { @@ -341,7 +341,7 @@ private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { retryEventSource().cancelOnceSchedule(executionScope.getResourceID()); } - private ResourceState getOrInitRetryExecution(ExecutionScope executionScope) { + private ResourceState getOrInitRetryExecution(ExecutionScope

executionScope) { final var state = resourceStateManager.getOrCreate(executionScope.getResourceID()); RetryExecution retryExecution = state.getRetry(); if (retryExecution == null) { @@ -387,9 +387,9 @@ private void handleAlreadyMarkedEvents() { } private class ReconcilerExecutor implements Runnable { - private final ExecutionScope executionScope; + private final ExecutionScope

executionScope; - private ReconcilerExecutor(ExecutionScope executionScope) { + private ReconcilerExecutor(ExecutionScope

executionScope) { this.executionScope = executionScope; } @@ -401,7 +401,7 @@ public void run() { try { MDCUtils.addResourceInfo(executionScope.getResource()); thread.setName("ReconcilerExecutor-" + controllerName() + "-" + thread.getId()); - PostExecutionControl postExecutionControl = + PostExecutionControl

postExecutionControl = reconciliationDispatcher.handleExecution(executionScope); eventProcessingFinished(executionScope, postExecutionControl); } finally { 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 23d5961015..23b3f5c51c 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 @@ -277,7 +277,7 @@ void startProcessedMarkedEventReceivedBefore() { eventProcessor.start(); verify(reconciliationDispatcherMock, timeout(100).times(1)).handleExecution(any()); - verify(metricsMock, times(1)).reconcileCustomResource(any(), isNull(), any()); + verify(metricsMock, times(1)).reconcileCustomResource(any(HasMetadata.class), isNull(), any()); } @Test From 9598629d690788f2bd42ed56460a6294dd78f789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 9 Dec 2022 09:51:37 +0100 Subject: [PATCH 06/14] feat: reading cache just on reconciliation dispatching (#1640) --- .../processing/event/EventProcessor.java | 15 ++++++++--- .../processing/event/ExecutionScope.java | 10 ++++--- .../processing/event/EventProcessorTest.java | 27 ++++++++++++------- .../event/ReconciliationDispatcherTest.java | 23 ++++++---------- 4 files changed, 45 insertions(+), 30 deletions(-) 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 57679f48ca..4ac69a199d 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 @@ -151,11 +151,11 @@ private void submitReconciliationExecution(ResourceState state) { } state.setUnderProcessing(true); final var latest = maybeLatest.get(); - ExecutionScope

executionScope = new ExecutionScope<>(latest, state.getRetry()); + ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); state.unMarkEventReceived(); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); - executor.execute(new ReconcilerExecutor(executionScope)); + executor.execute(new ReconcilerExecutor(resourceID, executionScope)); } else { log.debug( "Skipping executing controller for resource id: {}. Controller in execution: {}. Latest Resource present: {}", @@ -388,9 +388,11 @@ private void handleAlreadyMarkedEvents() { private class ReconcilerExecutor implements Runnable { private final ExecutionScope

executionScope; + private final ResourceID resourceID; - private ReconcilerExecutor(ExecutionScope

executionScope) { + private ReconcilerExecutor(ResourceID resourceID, ExecutionScope

executionScope) { this.executionScope = executionScope; + this.resourceID = resourceID; } @Override @@ -399,6 +401,13 @@ public void run() { final var thread = Thread.currentThread(); final var name = thread.getName(); try { + var actualResource = cache.get(resourceID); + if (actualResource.isEmpty()) { + log.debug("Skipping execution; primary resource missing from cache: {}", + resourceID); + return; + } + actualResource.ifPresent(executionScope::setResource); MDCUtils.addResourceInfo(executionScope.getResource()); thread.setName("ReconcilerExecutor-" + controllerName() + "-" + thread.getId()); PostExecutionControl

postExecutionControl = diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java index e750c931c3..9621ea915b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java @@ -6,14 +6,18 @@ class ExecutionScope { // the latest custom resource from cache - private final R resource; + private R resource; private final RetryInfo retryInfo; - ExecutionScope(R resource, RetryInfo retryInfo) { - this.resource = resource; + ExecutionScope(RetryInfo retryInfo) { this.retryInfo = retryInfo; } + public ExecutionScope setResource(R resource) { + this.resource = resource; + return this; + } + public R getResource() { return resource; } 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 23b3f5c51c..583e7a7289 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 @@ -120,7 +120,9 @@ void ifExecutionInProgressWaitsUntilItsFinished() { void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null); + executionScope.setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -254,7 +256,7 @@ void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(null).setResource(cr), PostExecutionControl.defaultDispatch()); verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); @@ -283,7 +285,8 @@ void startProcessedMarkedEventReceivedBefore() { @Test void updatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceUpdated(customResource); @@ -297,7 +300,8 @@ void updatesEventSourceHandlerIfResourceUpdated() { @Test void notUpdatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceStatusPatched(customResource); @@ -311,7 +315,8 @@ void notUpdatesEventSourceHandlerIfResourceUpdated() { void notReschedulesAfterTheFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -324,7 +329,8 @@ void notReschedulesAfterTheFinalizerRemoveProcessed() { void skipEventProcessingIfFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -341,7 +347,8 @@ void skipEventProcessingIfFinalizerRemoveProcessed() { void newResourceAfterMissedDeleteEvent() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); var newResource = testCustomResource(); @@ -377,7 +384,8 @@ void rateLimitsReconciliationSubmission() { @Test void schedulesRetryForMarReconciliationInterval() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); @@ -398,7 +406,8 @@ void schedulesRetryForMarReconciliationIntervalIfRetryExhausted() { eventSourceManagerMock, metricsMock)); eventProcessorWithRetry.start(); - ExecutionScope executionScope = new ExecutionScope(testCustomResource(), null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(testCustomResource()); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException()); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); 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 892fffcdbb..19bb3fd0a6 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 @@ -374,7 +374,6 @@ void propagatesRetryInfoToContextIfFinalizerSet() { reconciliationDispatcher.handleExecution( new ExecutionScope( - testCustomResource, new RetryInfo() { @Override public int getAttemptCount() { @@ -385,7 +384,7 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - })); + }).setResource(testCustomResource)); ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); @@ -504,7 +503,6 @@ void callErrorStatusHandlerIfImplemented() { reconciliationDispatcher.handleExecution( new ExecutionScope( - testCustomResource, new RetryInfo() { @Override public int getAttemptCount() { @@ -515,7 +513,7 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - })); + }).setResource(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), @@ -535,8 +533,7 @@ void callErrorStatusHandlerEvenOnFirstError() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -555,8 +552,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -576,8 +572,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -596,8 +591,7 @@ void errorStatusHandlerCanPatchResource() { reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), @@ -622,8 +616,7 @@ void ifRetryLimitedToZeroMaxAttemptsErrorHandlerGetsCorrectLastAttempt() { reconciler.errorHandler = mockErrorHandler; reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(mockErrorHandler, times(1)).updateErrorStatus(any(), ArgumentMatchers.argThat((ArgumentMatcher>) context -> { @@ -667,7 +660,7 @@ private void removeFinalizers(CustomResource customResource) { } public ExecutionScope executionScopeWithCREvent(T resource) { - return new ExecutionScope<>(resource, null); + return (ExecutionScope) new ExecutionScope<>(null).setResource(resource); } private class TestReconciler From b369781fbc4320ba19c118bfd1d7b92a6993f076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 9 Dec 2022 13:42:25 +0100 Subject: [PATCH 07/14] feat: cache object pruning (#1630) Co-authored-by: Chris Laprun --- docs/documentation/features.md | 24 +++- .../AnnotationControllerConfiguration.java | 9 ++ .../ControllerConfigurationOverrider.java | 11 +- .../DefaultControllerConfiguration.java | 8 +- .../config/DefaultResourceConfiguration.java | 16 ++- .../api/config/ResourceConfiguration.java | 9 ++ .../informer/InformerConfiguration.java | 23 +++- .../reconciler/ControllerConfiguration.java | 22 ++++ .../event/ReconciliationDispatcher.java | 46 +++++--- .../source/informer/InformerManager.java | 5 +- .../source/informer/InformerWrapper.java | 1 - .../informer/ManagedInformerEventSource.java | 4 +- .../informer/TemporaryResourceCache.java | 19 +++- .../informer/TransformingItemStore.java | 76 +++++++++++++ .../operator/ControllerManagerTest.java | 2 +- .../event/ReconciliationDispatcherTest.java | 50 +++++--- .../event/source/ResourceEventFilterTest.java | 2 +- .../ControllerResourceEventSourceTest.java | 2 +- .../informer/TemporaryResourceCacheTest.java | 32 +++++- .../informer/TransformingItemStoreTest.java | 59 ++++++++++ .../operator/CachePruneIT.java | 77 +++++++++++++ .../cacheprune/CachePruneCustomResource.java | 15 +++ .../cacheprune/CachePruneReconciler.java | 107 ++++++++++++++++++ .../sample/cacheprune/CachePruneSpec.java | 15 +++ .../sample/cacheprune/CachePruneStatus.java | 15 +++ .../LabelRemovingPruneFunction.java | 13 +++ .../webpage/src/main/resources/log4j2.xml | 2 +- 27 files changed, 611 insertions(+), 53 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index b4cbd1f2fd..d8e1cfa642 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -711,7 +711,29 @@ setting, where this flag usually needs to be set to false, in order to control t See also an example implementation in the [WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43) -## Monitoring with Micrometer +## Optimization of Caches + +** Cache pruning is an experimental feature. Might a subject of change or even removal in the future. ** + +Operators using informers will initially cache the data for all known resources when starting up +so that access to resources can be performed quickly. Consequently, the memory required for the +operator to run and startup time will both increase quite dramatically when dealing with large +clusters with numerous resources. + +It is thus possible to configure the operator to cache only pruned versions of the resources to +alleviate the memory usage of the primary and secondary caches. This setup, however, has +implications on how reconcilers deal with resources since they will only work with partial +objects. As a consequence, resources need to be updated using PATCH operations only, sending +only required changes. + +To see how to use, and how to handle related caveats regarding how to deal with pruned objects +that leverage +[server side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) patches, +please check the provided +[integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/c688524e64205690ba15587e7ed96a64dc231430/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java) +and associates reconciler. + +Pruned caches are currently not supported with the Dependent Resources feature. ## Automatic Generation of CRDs diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 43c61319ac..b996eeead9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -8,6 +8,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -83,6 +84,14 @@ public Set getNamespaces() { DEFAULT_NAMESPACES_SET.toArray(String[]::new))); } + @Override + @SuppressWarnings("unchecked") + public Optional> cachePruneFunction() { + return Optional.ofNullable( + Utils.instantiate(annotation.cachePruneFunction(), UnaryOperator.class, + Utils.contextFor(this, null, null))); + } + @Override @SuppressWarnings("unchecked") public Class

getResourceClass() { 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 c36aa51d2e..1f5494050e 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 @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -38,6 +39,7 @@ public class ControllerConfigurationOverrider { private OnUpdateFilter onUpdateFilter; private GenericFilter genericFilter; private RateLimiter rateLimiter; + private UnaryOperator cachePruneFunction; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizerName(); @@ -56,6 +58,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { dependentResources.forEach(drs -> namedDependentResourceSpecs.put(drs.getName(), drs)); this.original = original; this.rateLimiter = original.getRateLimiter(); + this.cachePruneFunction = original.cachePruneFunction().orElse(null); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -158,6 +161,12 @@ public ControllerConfigurationOverrider withGenericFilter(GenericFilter ge return this; } + public ControllerConfigurationOverrider withCachePruneFunction( + UnaryOperator cachePruneFunction) { + this.cachePruneFunction = cachePruneFunction; + return this; + } + @SuppressWarnings("unchecked") public ControllerConfigurationOverrider replacingNamedDependentResourceConfig(String name, Object dependentResourceConfig) { @@ -208,7 +217,7 @@ public ControllerConfiguration build() { onUpdateFilter, genericFilter, rateLimiter, - newDependentSpecs); + newDependentSpecs, cachePruneFunction); } public static ControllerConfigurationOverrider override( 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 3f4d952133..f6277c4139 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 @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -49,8 +50,10 @@ public DefaultControllerConfiguration( OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, RateLimiter rateLimiter, - List dependents) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces); + List dependents, + UnaryOperator cachePruneFunction) { + super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces, + cachePruneFunction); this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; @@ -116,4 +119,5 @@ public Optional maxReconciliationInterval() { public RateLimiter getRateLimiter() { return rateLimiter; } + } 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 index 9bc6ce5dba..8e42284a25 100644 --- 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 @@ -2,6 +2,7 @@ import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -19,18 +20,23 @@ public class DefaultResourceConfiguration private final OnAddFilter onAddFilter; private final OnUpdateFilter onUpdateFilter; private final GenericFilter genericFilter; + private final UnaryOperator cachePruneFunction; public DefaultResourceConfiguration(String labelSelector, Class resourceClass, OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, String... namespaces) { this(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces == null || namespaces.length == 0 ? DEFAULT_NAMESPACES_SET - : Set.of(namespaces)); + : Set.of(namespaces), + null); } public DefaultResourceConfiguration(String labelSelector, Class resourceClass, OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, Set namespaces) { + OnUpdateFilter onUpdateFilter, + GenericFilter genericFilter, + Set namespaces, + UnaryOperator cachePruneFunction) { this.labelSelector = labelSelector; this.resourceClass = resourceClass; this.onAddFilter = onAddFilter; @@ -39,6 +45,7 @@ public DefaultResourceConfiguration(String labelSelector, Class resourceClass this.namespaces = namespaces == null || namespaces.isEmpty() ? DEFAULT_NAMESPACES_SET : namespaces; + this.cachePruneFunction = cachePruneFunction; } @Override @@ -56,6 +63,11 @@ public Set getNamespaces() { return namespaces; } + @Override + public Optional> cachePruneFunction() { + return Optional.ofNullable(this.cachePruneFunction); + } + @Override public Class getResourceClass() { return resourceClass; 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 90e18f3e52..6a6574182a 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 @@ -3,11 +3,13 @@ import java.util.Collections; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; @@ -108,4 +110,11 @@ default Set getEffectiveNamespaces() { } return targetNamespaces; } + + /** + * See {@link ControllerConfiguration#cachePruneFunction()} for details. + */ + default Optional> cachePruneFunction() { + return Optional.empty(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 52f71501a9..03e37863df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -3,6 +3,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; @@ -29,6 +30,7 @@ class DefaultInformerConfiguration extends private final SecondaryToPrimaryMapper secondaryToPrimaryMapper; private final boolean followControllerNamespaceChanges; private final OnDeleteFilter onDeleteFilter; + private final UnaryOperator cachePruneFunction; protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, @@ -38,8 +40,10 @@ protected DefaultInformerConfiguration(String labelSelector, OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, OnDeleteFilter onDeleteFilter, - GenericFilter genericFilter) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces); + GenericFilter genericFilter, + UnaryOperator cachePruneFunction) { + super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces, + cachePruneFunction); this.followControllerNamespaceChanges = followControllerNamespaceChanges; this.primaryToSecondaryMapper = primaryToSecondaryMapper; @@ -47,6 +51,7 @@ protected DefaultInformerConfiguration(String labelSelector, Objects.requireNonNullElse(secondaryToPrimaryMapper, Mappers.fromOwnerReference()); this.onDeleteFilter = onDeleteFilter; + this.cachePruneFunction = cachePruneFunction; } @Override @@ -67,6 +72,11 @@ public Optional> onDeleteFilter() { public

PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper() { return (PrimaryToSecondaryMapper

) primaryToSecondaryMapper; } + + @Override + public Optional> cachePruneFunction() { + return Optional.ofNullable(this.cachePruneFunction); + } } /** @@ -102,6 +112,7 @@ class InformerConfigurationBuilder { private OnDeleteFilter onDeleteFilter; private GenericFilter genericFilter; private boolean inheritControllerNamespacesOnChange = false; + private UnaryOperator cachePruneFunction; private InformerConfigurationBuilder(Class resourceClass) { this.resourceClass = resourceClass; @@ -202,12 +213,18 @@ public InformerConfigurationBuilder withGenericFilter(GenericFilter generi return this; } + public InformerConfigurationBuilder withCachePruneFunction( + UnaryOperator cachePruneFunction) { + this.cachePruneFunction = cachePruneFunction; + return this; + } + public InformerConfiguration build() { return new DefaultInformerConfiguration<>(labelSelector, resourceClass, primaryToSecondaryMapper, secondaryToPrimaryMapper, namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter, - onDeleteFilter, genericFilter); + onDeleteFilter, genericFilter, cachePruneFunction); } } 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 ec76adf89d..df3efa7d40 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,6 +5,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.UnaryOperator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; @@ -118,4 +119,25 @@ MaxReconciliationInterval maxReconciliationInterval() default @MaxReconciliation * accessible no-arg constructor. */ Class rateLimiter() default LinearRateLimiter.class; + + /** + *

+ * This is an experimental feature, might be a subject of change and even removal in the + * future. + *

+ *

+ * In order to optimize cache, thus set null on some attributes, this function can be set. Note + * that this has subtle implications how updates on the resources should be handled. Notably only + * patching of the resource can be used from that point, since update would remove not cached + * parts of the resource. + *

+ *

+ * Note that this feature does not work with Dependent Resources. + *

+ * + * + * + * @return function to remove parts of the resource. + */ + Class cachePruneFunction() default UnaryOperator.class; } 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 015a2f5c31..4a7ac68082 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 @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.function.Function; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +12,8 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; @@ -82,7 +86,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) Context

context = new DefaultContext<>(executionScope.getRetryInfo(), controller, originalResource); if (markedForDeletion) { - return handleCleanup(resourceForExecution, context); + return handleCleanup(originalResource, resourceForExecution, context); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); } @@ -109,7 +113,8 @@ private PostExecutionControl

handleReconcile( * finalizer add. This will make sure that the resources are not created before there is a * finalizer. */ - var updatedResource = updateCustomResourceWithFinalizer(originalResource); + var updatedResource = + updateCustomResourceWithFinalizer(resourceForExecution, originalResource); return PostExecutionControl.onlyFinalizerAdded(updatedResource); } else { try { @@ -276,7 +281,8 @@ private void updatePostExecutionControlWithReschedule( } - private PostExecutionControl

handleCleanup(P resource, Context

context) { + private PostExecutionControl

handleCleanup(P originalResource, P resource, + Context

context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), @@ -289,7 +295,8 @@ private PostExecutionControl

handleCleanup(P resource, Context

context) { // cleanup is finished, nothing left to done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) { - P customResource = removeFinalizer(resource, finalizerName); + P customResource = conflictRetryingPatch(resource, originalResource, + r -> r.removeFinalizer(finalizerName)); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -304,11 +311,13 @@ private PostExecutionControl

handleCleanup(P resource, Context

context) { return postExecutionControl; } - private P updateCustomResourceWithFinalizer(P resource) { + private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { log.debug( - "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); - resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.updateResource(resource); + "Adding finalizer for resource: {} version: {}", getUID(originalResource), + getVersion(originalResource)); + + return conflictRetryingPatch(resourceForExecution, originalResource, + r -> r.addFinalizer(configuration().getFinalizerName())); } private P updateCustomResource(P resource) { @@ -321,20 +330,21 @@ ControllerConfiguration

configuration() { return controller.getConfiguration(); } - public P removeFinalizer(P resource, String finalizer) { + public P conflictRetryingPatch(P resource, P originalResource, + Function modificationFunction) { if (log.isDebugEnabled()) { log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource)); } int retryIndex = 0; while (true) { try { - var removed = resource.removeFinalizer(finalizer); - if (!removed) { + var modified = modificationFunction.apply(resource); + if (Boolean.FALSE.equals(modified)) { return resource; } - return customResourceFacade.updateResource(resource); + return customResourceFacade.serverSideApplyLockResource(resource, originalResource); } catch (KubernetesClientException e) { - log.trace("Exception during finalizer removal for resource: {}", resource); + log.trace("Exception during patch for resource: {}", resource); retryIndex++; // only retry on conflict (HTTP 409), otherwise fail if (e.getCode() != 409) { @@ -343,7 +353,7 @@ public P removeFinalizer(P resource, String finalizer) { if (retryIndex >= MAX_FINALIZER_REMOVAL_RETRY) { throw new OperatorException( "Exceeded maximum (" + MAX_FINALIZER_REMOVAL_RETRY - + ") retry attempts to remove finalizer '" + finalizer + "' for resource " + + ") retry attempts to patch resource: " + ResourceID.fromResource(resource)); } resource = customResourceFacade.getResource(resource.getMetadata().getNamespace(), @@ -370,12 +380,18 @@ public R getResource(String namespace, String name) { } } + public R serverSideApplyLockResource(R resource, R originalResource) { + var patchContext = PatchContext.of(PatchType.SERVER_SIDE_APPLY); + patchContext.setForce(true); + return resource(originalResource).patch(patchContext, + resource); + } + public R updateResource(R resource) { log.debug( "Trying to replace resource {}, version: {}", getName(resource), resource.getMetadata().getResourceVersion()); - return resource(resource).lockResourceVersion(resource.getMetadata().getResourceVersion()) .replace(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index d51301c385..94788e6706 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -109,8 +109,11 @@ private InformerWrapper createEventSourceForNamespace(String namespace) { private InformerWrapper createEventSource( FilterWatchListDeletable, Resource> filteredBySelectorClient, ResourceEventHandler eventHandler, String namespaceIdentifier) { + var informer = filteredBySelectorClient.runnableInformer(0); + configuration.cachePruneFunction() + .ifPresent(f -> informer.itemStore(new TransformingItemStore<>(f))); var source = - new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0), namespaceIdentifier); + new InformerWrapper<>(informer, namespaceIdentifier); source.addEventHandler(eventHandler); sources.put(namespaceIdentifier, source); return source; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 8809022e95..17dc5cc969 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -39,7 +39,6 @@ public InformerWrapper(SharedIndexInformer informer, String namespaceIdentifi this.informer = informer; this.namespaceIdentifier = namespaceIdentifier; this.cache = (Cache) informer.getStore(); - } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 43173a28f7..4286335644 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -36,13 +36,15 @@ public abstract class ManagedInformerEventSource temporaryResourceCache = new TemporaryResourceCache<>(this); + protected TemporaryResourceCache temporaryResourceCache; protected InformerManager cache = new InformerManager<>(); protected C configuration; protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration) { super(configuration.getResourceClass()); + temporaryResourceCache = new TemporaryResourceCache<>(this, + configuration.cachePruneFunction().orElse(null)); manager().initSources(client, configuration, this); this.configuration = configuration; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index c0c041f3fb..0af2ec0b2f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.UnaryOperator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,11 +35,14 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + private UnaryOperator cachePruneFunction; private final Map cache = new ConcurrentHashMap<>(); private final ManagedInformerEventSource managedInformerEventSource; - public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { + public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource, + UnaryOperator cachePruneFunction) { this.managedInformerEventSource = managedInformerEventSource; + this.cachePruneFunction = cachePruneFunction; } public synchronized void removeResourceFromCache(T resource) { @@ -46,14 +50,14 @@ public synchronized void removeResourceFromCache(T resource) { } public synchronized void unconditionallyCacheResource(T newResource) { - cache.put(ResourceID.fromResource(newResource), newResource); + putToCache(newResource, null); } public synchronized void putAddedResource(T newResource) { ResourceID resourceID = ResourceID.fromResource(newResource); if (managedInformerEventSource.get(resourceID).isEmpty()) { log.debug("Putting resource to cache with ID: {}", resourceID); - cache.put(resourceID, newResource); + putToCache(newResource, resourceID); } else { log.debug("Won't put resource into cache found already informer cache: {}", resourceID); } @@ -70,7 +74,7 @@ public synchronized void putUpdatedResource(T newResource, String previousResour if (informerCacheResource.get().getMetadata().getResourceVersion() .equals(previousResourceVersion)) { log.debug("Putting resource to temporal cache with id: {}", resourceId); - cache.put(resourceId, newResource); + putToCache(newResource, resourceId); } else { // if something is in cache it's surely obsolete now log.debug("Trying to remove an obsolete resource from cache for id: {}", resourceId); @@ -78,6 +82,13 @@ public synchronized void putUpdatedResource(T newResource, String previousResour } } + private void putToCache(T resource, ResourceID resourceID) { + if (cachePruneFunction != null) { + resource = cachePruneFunction.apply(resource); + } + cache.put(resourceID == null ? ResourceID.fromResource(resource) : resourceID, resource); + } + public synchronized Optional getResourceFromCache(ResourceID resourceID) { return Optional.ofNullable(cache.get(resourceID)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java new file mode 100644 index 0000000000..60fdd32005 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.fabric8.kubernetes.client.informers.cache.ItemStore; + +public class TransformingItemStore implements ItemStore { + + private Function keyFunction; + private UnaryOperator transformationFunction; + private ConcurrentHashMap store = new ConcurrentHashMap<>(); + + public TransformingItemStore(UnaryOperator transformationFunction) { + this(Cache::metaNamespaceKeyFunc, transformationFunction); + } + + public TransformingItemStore(Function keyFunction, + UnaryOperator transformationFunction) { + this.keyFunction = keyFunction; + this.transformationFunction = transformationFunction; + } + + @Override + public String getKey(R obj) { + return keyFunction.apply(obj); + } + + @Override + public R put(String key, R obj) { + var originalName = obj.getMetadata().getName(); + var originalNamespace = obj.getMetadata().getNamespace(); + var originalResourceVersion = obj.getMetadata().getResourceVersion(); + + var transformed = transformationFunction.apply(obj); + + transformed.getMetadata().setName(originalName); + transformed.getMetadata().setNamespace(originalNamespace); + transformed.getMetadata().setResourceVersion(originalResourceVersion); + return store.put(key, transformed); + } + + @Override + public R remove(String key) { + return store.remove(key); + } + + @Override + public Stream keySet() { + return store.keySet().stream(); + } + + @Override + public Stream values() { + return store.values().stream(); + } + + @Override + public R get(String key) { + return store.get(key); + } + + @Override + public int size() { + return store.size(); + } + + @Override + public boolean isFullState() { + return false; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index d788f61e4a..94af637b38 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -58,7 +58,7 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, - null, null, null, null, null, null); + null, null, null, null, null, null, null); this.controller = controller; } 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 19bb3fd0a6..ef7a27677b 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 @@ -22,10 +22,7 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.*; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -137,8 +134,9 @@ void addFinalizerOnNewResource() { verify(reconciler, never()) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) - .updateResource( - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); + .serverSideApplyLockResource( + argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)), + any()); assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); } @@ -218,7 +216,8 @@ void removesDefaultFinalizerOnDeleteIfSet() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(1)).updateResource(testCustomResource); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(testCustomResource, + testCustomResource); } @Test @@ -227,7 +226,7 @@ void retriesFinalizerRemovalWithFreshResource() { markForDeletion(testCustomResource); var resourceWithFinalizer = TestUtils.testCustomResource(); resourceWithFinalizer.addFinalizer(DEFAULT_FINALIZER); - when(customResourceFacade.updateResource(testCustomResource)) + when(customResourceFacade.serverSideApplyLockResource(testCustomResource, testCustomResource)) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())).thenReturn(resourceWithFinalizer); @@ -236,7 +235,7 @@ void retriesFinalizerRemovalWithFreshResource() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(2)).updateResource(any()); + verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any()); verify(customResourceFacade, times(1)).getResource(any(), any()); } @@ -244,7 +243,7 @@ void retriesFinalizerRemovalWithFreshResource() { void throwsExceptionIfFinalizerRemovalRetryExceeded() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.updateResource(any())) + when(customResourceFacade.serverSideApplyLockResource(any(), any())) .thenThrow(new KubernetesClientException(null, 409, null)); when(customResourceFacade.getResource(any(), any())) .thenAnswer((Answer) invocationOnMock -> createResourceWithFinalizer()); @@ -256,7 +255,9 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { assertThat(postExecControl.getRuntimeException()).isPresent(); assertThat(postExecControl.getRuntimeException().get()) .isInstanceOf(OperatorException.class); - verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).updateResource(any()); + verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).serverSideApplyLockResource( + any(), + any()); verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY - 1)).getResource(any(), any()); } @@ -265,7 +266,7 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.updateResource(any())) + when(customResourceFacade.serverSideApplyLockResource(any(), any())) .thenThrow(new KubernetesClientException(null, 400, null)); var res = @@ -273,7 +274,7 @@ void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { assertThat(res.getRuntimeException()).isPresent(); assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class); - verify(customResourceFacade, times(1)).updateResource(any()); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any()); verify(customResourceFacade, never()).getResource(any(), any()); } @@ -337,13 +338,14 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.updateResource(any())).thenReturn(testCustomResource); + when(customResourceFacade.serverSideApplyLockResource(any(), any())) + .thenReturn(testCustomResource); var postExecControl = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, times(1)).updateResource(any()); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any()); assertThat(postExecControl.updateIsStatusPatch()).isFalse(); assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); } @@ -639,6 +641,24 @@ void canSkipSchedulingMaxDelayIf() { assertThat(control.getReScheduleDelay()).isNotPresent(); } + @Test + void retriesAddingFinalizer() { + removeFinalizers(testCustomResource); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + when(customResourceFacade.serverSideApplyLockResource(any(), any())) + .thenThrow(new KubernetesClientException(null, 409, null)) + .thenReturn(testCustomResource); + when(customResourceFacade.getResource(any(), any())) + .then((Answer) invocationOnMock -> { + testCustomResource.getFinalizers().clear(); + return testCustomResource; + }); + + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any()); + } + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); 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 7cc5a20781..55ab40c173 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 @@ -145,7 +145,7 @@ public ControllerConfig(String finalizer, boolean generationAware, eventFilter, customResourceClass, null, - null, null, null, null, null); + null, null, null, null, null, null); } } 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 00980743e0..d30a69d694 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 @@ -188,7 +188,7 @@ public TestConfiguration(boolean generationAware, OnAddFilter informerEventSource = mock(InformerEventSource.class); private TemporaryResourceCache temporaryResourceCache = - new TemporaryResourceCache<>(informerEventSource); + new TemporaryResourceCache<>(informerEventSource, null); @Test @@ -79,6 +80,29 @@ void removesResourceFromCache() { .isNotPresent(); } + @Test + void objectIsTransformedBeforePutIntoCache() { + temporaryResourceCache = + new TemporaryResourceCache<>(informerEventSource, r -> { + r.getMetadata().setLabels(null); + return r; + }); + + temporaryResourceCache.putAddedResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + + temporaryResourceCache.unconditionallyCacheResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + + temporaryResourceCache.unconditionallyCacheResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + } + + private void assertLabelsIsEmpty(TemporaryResourceCache temporaryResourceCache) { + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource())) + .orElseThrow().getMetadata().getLabels()).isNull(); + } + private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); when(informerEventSource.get(any())).thenReturn(Optional.empty()); @@ -90,7 +114,9 @@ private ConfigMap propagateTestResourceToCache() { ConfigMap testResource() { ConfigMap configMap = new ConfigMap(); - configMap.setMetadata(new ObjectMeta()); + configMap.setMetadata(new ObjectMetaBuilder() + .withLabels(Map.of("k", "v")) + .build()); configMap.getMetadata().setName("test"); configMap.getMetadata().setNamespace("default"); configMap.getMetadata().setResourceVersion(RESOURCE_VERSION); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java new file mode 100644 index 0000000000..3bebc79094 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java @@ -0,0 +1,59 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; + +import static io.fabric8.kubernetes.client.informers.cache.Cache.metaNamespaceKeyFunc; +import static org.assertj.core.api.Assertions.assertThat; + +class TransformingItemStoreTest { + + @Test + void cachedObjectTransformed() { + TransformingItemStore transformingItemStore = new TransformingItemStore<>(r -> { + r.getMetadata().setLabels(null); + return r; + }); + + var cm = configMap(); + cm.getMetadata().setLabels(Map.of("k", "v")); + transformingItemStore.put(metaNamespaceKeyFunc(cm), cm); + + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getLabels()) + .isNull(); + } + + @Test + void preservesSelectedAttributes() { + TransformingItemStore transformingItemStore = new TransformingItemStore<>(r -> { + r.getMetadata().setName(null); + r.getMetadata().setNamespace(null); + r.getMetadata().setResourceVersion(null); + return r; + }); + var cm = configMap(); + transformingItemStore.put(metaNamespaceKeyFunc(cm), cm); + + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getName()) + .isNotNull(); + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getNamespace()) + .isNotNull(); + assertThat( + transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getResourceVersion()) + .isNotNull(); + } + + ConfigMap configMap() { + var cm = new ConfigMap(); + cm.setMetadata(new ObjectMetaBuilder() + .withName("test1") + .withNamespace("default").withResourceVersion("1") + .build()); + return cm; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java new file mode 100644 index 0000000000..9bf57ab372 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneCustomResource; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneSpec; + +import static io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class CachePruneIT { + + public static final String DEFAULT_DATA = "default_data"; + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String UPDATED_DATA = "updated_data"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new CachePruneReconciler()).build(); + + @Test + void pruningRelatedBehavior() { + var res = operator.create(testResource()); + await().untilAsserted(() -> { + assertState(DEFAULT_DATA); + }); + + res.getSpec().setData(UPDATED_DATA); + var updated = operator.replace(res); + + await().untilAsserted(() -> { + assertState(UPDATED_DATA); + }); + + operator.delete(updated); + + await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> { + var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME); + var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(configMap).isNull(); + assertThat(actual).isNull(); + }); + } + + void assertState(String expectedData) { + var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME); + assertThat(actual.getMetadata()).isNotNull(); + assertThat(actual.getMetadata().getFinalizers()).isNotEmpty(); + assertThat(actual.getStatus().getCreated()).isTrue(); + assertThat(actual.getMetadata().getLabels()).isNotEmpty(); + var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(configMap.getData()).containsEntry(DATA_KEY, expectedData); + assertThat(configMap.getMetadata().getLabels()).isNotEmpty(); + } + + CachePruneCustomResource testResource() { + var res = new CachePruneCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withLabels(Map.of("sampleLabel", "val")) + .build()); + res.setSpec(new CachePruneSpec()); + res.getSpec().setData(DEFAULT_DATA); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java new file mode 100644 index 0000000000..60431588fd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("cpr") +public class CachePruneCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java new file mode 100644 index 0000000000..236b205c2c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java @@ -0,0 +1,107 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +@ControllerConfiguration(cachePruneFunction = LabelRemovingPruneFunction.class) +public class CachePruneReconciler + implements Reconciler, + EventSourceInitializer, + Cleaner, KubernetesClientAware { + + public static final String DATA_KEY = "data"; + public static final String FIELD_MANAGER = "controller"; + public static final String SECONDARY_CREATE_FIELD_MANAGER = "creator"; + private KubernetesClient client; + + @Override + public UpdateControl reconcile( + CachePruneCustomResource resource, + Context context) { + var configMap = context.getSecondaryResource(ConfigMap.class); + configMap.ifPresentOrElse(cm -> { + if (!cm.getMetadata().getLabels().isEmpty()) { + throw new AssertionError("Labels should be null"); + } + if (!cm.getData().get(DATA_KEY) + .equals(resource.getSpec().getData())) { + var cloned = ConfigurationServiceProvider.instance().getResourceCloner().clone(cm); + cloned.getData().put(DATA_KEY, resource.getSpec().getData()); + var patchContext = patchContextWithFieldManager(FIELD_MANAGER); + // setting new field manager since we don't control label anymore: + // since not the whole object is present in cache SSA would remove labels if the controller + // is not the manager. + // Note that JSON Merge Patch (or others would also work here, without this "hack". + patchContext.setForce(true); + patchContext.setFieldManager(FIELD_MANAGER); + client.configMaps().resource(cm) + .patch(patchContext, cloned); + } + }, () -> client.configMaps().resource(configMap(resource)) + .patch(patchContextWithFieldManager(SECONDARY_CREATE_FIELD_MANAGER))); + + resource.setStatus(new CachePruneStatus()); + resource.getStatus().setCreated(true); + return UpdateControl.patchStatus(resource); + } + + private PatchContext patchContextWithFieldManager(String fieldManager) { + PatchContext patchContext = new PatchContext(); + // using server side apply + patchContext.setPatchType(PatchType.SERVER_SIDE_APPLY); + patchContext.setFieldManager(fieldManager); + return patchContext; + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + InformerEventSource configMapEventSource = + new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class, context) + .withCachePruneFunction(new LabelRemovingPruneFunction<>()) + .build(), + context); + return EventSourceInitializer.nameEventSources(configMapEventSource); + } + + ConfigMap configMap(CachePruneCustomResource resource) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(resource.getMetadata().getName()); + configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace()); + configMap.setData(Map.of(DATA_KEY, resource.getSpec().getData())); + HashMap labels = new HashMap<>(); + labels.put("mylabel", "val"); + configMap.getMetadata().setLabels(labels); + configMap.addOwnerReference(resource); + return configMap; + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; + } + + @Override + public DeleteControl cleanup(CachePruneCustomResource resource, + Context context) { + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java new file mode 100644 index 0000000000..2d58a70d3a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +public class CachePruneSpec { + + private String data; + + public String getData() { + return data; + } + + public CachePruneSpec setData(String data) { + this.data = data; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java new file mode 100644 index 0000000000..a074c0e011 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +public class CachePruneStatus { + + private Boolean created; + + public Boolean getCreated() { + return created; + } + + public CachePruneStatus setCreated(Boolean created) { + this.created = created; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java new file mode 100644 index 0000000000..a495803628 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class LabelRemovingPruneFunction implements UnaryOperator { + @Override + public R apply(R r) { + r.getMetadata().setLabels(null); + return r; + } +} diff --git a/sample-operators/webpage/src/main/resources/log4j2.xml b/sample-operators/webpage/src/main/resources/log4j2.xml index 5b794e7de3..3e92919d3b 100644 --- a/sample-operators/webpage/src/main/resources/log4j2.xml +++ b/sample-operators/webpage/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + From 5f69aa23a47e979528b050a8b35a702fed1171f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 9 Dec 2022 13:55:33 +0100 Subject: [PATCH 08/14] feat: feat controller queue size, execution thread count (#1649) --- .../micrometer/MicrometerMetrics.java | 56 +++++++++++++++++++ .../operator/api/monitoring/Metrics.java | 12 ++++ .../operator/api/reconciler/Constants.java | 1 + .../operator/processing/Controller.java | 2 +- .../processing/event/EventProcessor.java | 12 ++-- .../config/MockControllerConfiguration.java | 1 + .../event/source/ResourceEventFilterTest.java | 2 +- .../ControllerResourceEventSourceTest.java | 2 +- 8 files changed, 80 insertions(+), 8 deletions(-) diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index fb7d064c26..6bc4f22fdc 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -5,28 +5,56 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.GroupVersionKind; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.CONTROLLER_NAME; + public class MicrometerMetrics implements Metrics { private static final String PREFIX = "operator.sdk."; private static final String RECONCILIATIONS = "reconciliations."; + private static final String RECONCILIATIONS_EXECUTIONS = PREFIX + RECONCILIATIONS + "executions."; + private static final String RECONCILIATIONS_QUEUE_SIZE = PREFIX + RECONCILIATIONS + "queue.size."; private final MeterRegistry registry; + private final Map gauges = new ConcurrentHashMap<>(); public MicrometerMetrics(MeterRegistry registry) { this.registry = registry; } + @Override + public void controllerRegistered(Controller controller) { + String executingThreadsName = + RECONCILIATIONS_EXECUTIONS + controller.getConfiguration().getName(); + AtomicInteger executingThreads = + registry.gauge(executingThreadsName, + gvkTags(controller.getConfiguration().getResourceClass()), + new AtomicInteger(0)); + gauges.put(executingThreadsName, executingThreads); + + String controllerQueueName = + RECONCILIATIONS_QUEUE_SIZE + controller.getConfiguration().getName(); + AtomicInteger controllerQueueSize = + registry.gauge(controllerQueueName, + gvkTags(controller.getConfiguration().getResourceClass()), + new AtomicInteger(0)); + gauges.put(controllerQueueName, controllerQueueSize); + } + public T timeControllerExecution(ControllerExecution execution) { final var name = execution.controllerName(); final var execName = PREFIX + "controllers.execution." + execution.name(); @@ -94,6 +122,10 @@ public void reconcileCustomResource(HasMetadata resource, RetryInfo retryInfoNul "" + retryInfo.map(RetryInfo::getAttemptCount).orElse(0), RECONCILIATIONS + "retries.last", "" + retryInfo.map(RetryInfo::isLastAttempt).orElse(true)); + + AtomicInteger controllerQueueSize = + gauges.get(RECONCILIATIONS_QUEUE_SIZE + metadata.get(CONTROLLER_NAME)); + controllerQueueSize.incrementAndGet(); } @Override @@ -101,6 +133,24 @@ public void finishedReconciliation(HasMetadata resource, Map met incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "success", metadata); } + @Override + public void reconciliationExecutionStarted(HasMetadata resource, Map metadata) { + AtomicInteger reconcilerExecutions = + gauges.get(RECONCILIATIONS_EXECUTIONS + metadata.get(CONTROLLER_NAME)); + reconcilerExecutions.incrementAndGet(); + } + + @Override + public void reconciliationExecutionFinished(HasMetadata resource, Map metadata) { + AtomicInteger reconcilerExecutions = + gauges.get(RECONCILIATIONS_EXECUTIONS + metadata.get(CONTROLLER_NAME)); + reconcilerExecutions.decrementAndGet(); + + AtomicInteger controllerQueueSize = + gauges.get(RECONCILIATIONS_QUEUE_SIZE + metadata.get(CONTROLLER_NAME)); + controllerQueueSize.decrementAndGet(); + } + public void failedReconciliation(HasMetadata resource, Exception exception, Map metadata) { var cause = exception.getCause(); @@ -118,6 +168,12 @@ public void failedReconciliation(HasMetadata resource, Exception exception, return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); } + public static List gvkTags(Class resourceClass) { + final var gvk = GroupVersionKind.gvkFor(resourceClass); + return List.of(Tag.of("group", gvk.group), Tag.of("version", gvk.version), + Tag.of("kind", gvk.kind)); + } + private void incrementCounter(ResourceID id, String counterName, Map metadata, String... additionalTags) { final var additionalTagsNb = diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java index 31ffda96f8..c134a5522f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -20,6 +21,11 @@ public interface Metrics { */ Metrics NOOP = new Metrics() {}; + /** + * Do initialization if necessary; + */ + default void controllerRegistered(Controller controller) {} + /** * Called when an event has been accepted by the SDK from an event source, which would result in * potentially triggering the associated Reconciler. @@ -63,6 +69,12 @@ default void failedReconciliation(HasMetadata resource, Exception exception, failedReconciliation(ResourceID.fromResource(resource), exception, metadata); } + + default void reconciliationExecutionStarted(HasMetadata resource, Map metadata) {} + + default void reconciliationExecutionFinished(HasMetadata resource, + Map metadata) {} + /** * * @deprecated Use (and implement) {@link #cleanupDoneFor(ResourceID, Map)} instead 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 index 55a9cbdbed..08aeb16f90 100644 --- 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 @@ -21,6 +21,7 @@ public final class Constants { public static final String SAME_AS_CONTROLLER = "JOSDK_SAME_AS_CONTROLLER"; public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk"; + public static final String CONTROLLER_NAME = "controller.name"; private Constants() {} } 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 f5f892719c..92c8de675d 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 @@ -90,10 +90,10 @@ public Controller(Reconciler

reconciler, eventProcessor = new EventProcessor<>(eventSourceManager); eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); controllerHealthInfo = new ControllerHealthInfo(eventSourceManager); - final var context = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); initAndRegisterEventSources(context); + ConfigurationServiceProvider.instance().getMetrics().controllerRegistered(this); } @Override 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 4ac69a199d..59962e39f1 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 @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.time.Duration; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -18,7 +18,6 @@ import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -98,9 +97,10 @@ private EventProcessor( this.rateLimiter = controllerConfiguration.getRateLimiter(); metricsMetadata = Optional.ofNullable(eventSourceManager.getController()) - .map(Controller::getAssociatedGroupVersionKind) - .map(gvk -> Map.of(Constants.RESOURCE_GVK_KEY, (Object) gvk)) - .orElse(Collections.emptyMap()); + .map(c -> Map.of( + Constants.RESOURCE_GVK_KEY, c.getAssociatedGroupVersionKind(), + Constants.CONTROLLER_NAME, controllerConfiguration.getName())) + .orElse(new HashMap<>()); } @Override @@ -409,11 +409,13 @@ public void run() { } actualResource.ifPresent(executionScope::setResource); MDCUtils.addResourceInfo(executionScope.getResource()); + metrics.reconciliationExecutionStarted(executionScope.getResource(), metricsMetadata); thread.setName("ReconcilerExecutor-" + controllerName() + "-" + thread.getId()); PostExecutionControl

postExecutionControl = reconciliationDispatcher.handleExecution(executionScope); eventProcessingFinished(executionScope, postExecutionControl); } finally { + metrics.reconciliationExecutionFinished(executionScope.getResource(), metricsMetadata); // restore original name thread.setName(name); MDCUtils.removeResourceInfo(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java index 0e3fc5df22..baf680b677 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java @@ -14,6 +14,7 @@ public static ControllerConfiguration forResource( when(configuration.getResourceClass()).thenReturn(resourceType); when(configuration.getNamespaces()).thenReturn(DEFAULT_NAMESPACES_SET); when(configuration.getEffectiveNamespaces()).thenCallRealMethod(); + when(configuration.getName()).thenReturn(resourceType.getSimpleName()); return configuration; } } 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 55ab40c173..df73d79164 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 @@ -135,7 +135,7 @@ public ControllerConfig(String finalizer, boolean generationAware, ResourceEventFilter eventFilter, Class customResourceClass) { super( null, - null, + "testController", null, finalizer, generationAware, 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 d30a69d694..27b43656be 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 @@ -178,7 +178,7 @@ public TestConfiguration(boolean generationAware, OnAddFilter genericFilter) { super( null, - null, + "testController", null, FINALIZER, generationAware, From 47ef0760a5e1154221779571531dfd82d4135ca2 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 9 Dec 2022 14:22:42 +0100 Subject: [PATCH 09/14] feat: compute cleaner status based on specs, resolve from config (#1648) --- .../operator/processing/Controller.java | 2 +- .../dependent/AbstractDependentResource.java | 9 ++- .../workflow/DefaultManagedWorkflow.java | 6 +- .../workflow/DependentResourceNode.java | 4 +- .../dependent/workflow/ManagedWorkflow.java | 8 +- .../workflow/ManagedWorkflowSupport.java | 9 ++- .../workflow/SpecDependentResourceNode.java | 7 +- .../dependent/workflow/Workflow.java | 76 ++++++++++--------- .../workflow/ManagedWorkflowSupportTest.java | 4 - .../workflow/ManagedWorkflowTest.java | 21 +++-- .../workflow/ManagedWorkflowTestUtils.java | 10 +-- .../dependent/workflow/WorkflowTest.java | 25 ++++++ 12 files changed, 113 insertions(+), 68 deletions(-) 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 92c8de675d..0fa167e3c2 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 @@ -84,7 +84,7 @@ public Controller(Reconciler

reconciler, contextInitializer = reconciler instanceof ContextInitializer; isCleaner = reconciler instanceof Cleaner; managedWorkflow = configurationService.getWorkflowFactory().workflowFor(configuration); - managedWorkflow.resolve(kubernetesClient, configuration.getDependentResources()); + managedWorkflow.resolve(kubernetesClient, configuration); eventSourceManager = new EventSourceManager<>(this); eventProcessor = new EventProcessor<>(eventSourceManager); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index dbfc34fe42..7a32938403 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.processing.dependent; -import java.util.*; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; @@ -21,6 +22,7 @@ public abstract class AbstractDependentResource private final boolean creatable = this instanceof Creator; private final boolean updatable = this instanceof Updater; + private final boolean deletable = this instanceof Deleter; protected Creator creator; protected Updater updater; @@ -172,4 +174,9 @@ protected boolean isCreatable() { protected boolean isUpdatable() { return updatable; } + + @Override + public boolean isDeletable() { + return deletable; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java index 3096f1d4fb..b9bc29a32f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -49,9 +50,10 @@ public Map getDependentResourcesByName() { } @Override - public ManagedWorkflow

resolve(KubernetesClient client, List specs) { + public ManagedWorkflow

resolve(KubernetesClient client, + ControllerConfiguration

configuration) { if (!resolved) { - workflow.resolve(client, specs); + workflow.resolve(client, configuration); resolved = true; } return this; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java index 2c069bf999..11700c8d25 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @SuppressWarnings("rawtypes") public interface DependentResourceNode { @@ -26,5 +26,5 @@ public interface DependentResourceNode { String getName(); - default void resolve(KubernetesClient client, List dependentResources) {} + default void resolve(KubernetesClient client, ControllerConfiguration

configuration) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java index de2b634b5a..518ab69289 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java @@ -1,12 +1,11 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; import java.util.Collections; -import java.util.List; import java.util.Map; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -40,7 +39,7 @@ public Map getDependentResourcesByName() { } @Override - public ManagedWorkflow resolve(KubernetesClient client, List dependentResources) { + public ManagedWorkflow resolve(KubernetesClient client, ControllerConfiguration configuration) { return this; } }; @@ -55,6 +54,5 @@ public ManagedWorkflow resolve(KubernetesClient client, List dependentResources) Map getDependentResourcesByName(); - ManagedWorkflow

resolve(KubernetesClient client, - List dependentResources); + ManagedWorkflow

resolve(KubernetesClient client, ControllerConfiguration

configuration); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java index 9b11784443..24525357cf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java @@ -50,16 +50,19 @@ public

Workflow

createWorkflow( List dependentResourceSpecs) { var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs); final var alreadyCreated = new ArrayList(orderedResourceSpecs.size()); + final boolean[] cleanerHolder = {false}; final var nodes = orderedResourceSpecs.stream() - .map(spec -> createFrom(spec, alreadyCreated)) + .map(spec -> createFrom(spec, alreadyCreated, cleanerHolder)) .collect(Collectors.toSet()); - return new Workflow<>(nodes); + return new Workflow<>(nodes, cleanerHolder[0]); } private DependentResourceNode createFrom(DependentResourceSpec spec, - List alreadyCreated) { + List alreadyCreated, boolean[] cleanerHolder) { final var node = new SpecDependentResourceNode<>(spec); alreadyCreated.add(node); + // if any previously checked dependent was a cleaner, no need to check further + cleanerHolder[0] = cleanerHolder[0] || Workflow.isDeletable(spec.getDependentResourceClass()); spec.getDependsOn().forEach(depend -> { final DependentResourceNode dependsOn = alreadyCreated.stream() .filter(drn -> depend.equals(drn.getName())).findFirst() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java index ae8ed282c6..fc6e77db6d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java @@ -1,9 +1,8 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; -import java.util.List; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; @@ -21,8 +20,8 @@ public SpecDependentResourceNode(DependentResourceSpec spec) { @Override @SuppressWarnings({"rawtypes", "unchecked"}) - public void resolve(KubernetesClient client, List dependentResources) { - final var spec = dependentResources.stream() + public void resolve(KubernetesClient client, ControllerConfiguration

configuration) { + final var spec = configuration.getDependentResources().stream() .filter(drs -> drs.getName().equals(getName())) .findFirst().orElseThrow(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java index 07163ab8c5..99e2fa94e1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java @@ -11,10 +11,13 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; /** * Dependents definition: so if B depends on A, the B is dependent of A. @@ -34,23 +37,40 @@ public class Workflow

{ // it's "global" executor service shared between multiple reconciliations running parallel private final ExecutorService executorService; private boolean resolved; - private boolean hasCleaner; + private final boolean hasCleaner; - Workflow(Set dependentResourceNodes) { + Workflow(Set dependentResourceNodes, boolean hasCleaner) { this(dependentResourceNodes, ExecutorServiceManager.instance().workflowExecutorService(), - THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false, false); + THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false, hasCleaner); } Workflow(Set dependentResourceNodes, ExecutorService executorService, boolean throwExceptionAutomatically, boolean resolved, boolean hasCleaner) { this.executorService = executorService; - this.dependentResourceNodes = dependentResourceNodes.stream() - .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); + this.dependentResourceNodes = toMap(dependentResourceNodes); this.throwExceptionAutomatically = throwExceptionAutomatically; this.resolved = resolved; this.hasCleaner = hasCleaner; - preprocessForReconcile(); + } + + private Map toMap( + Set dependentResourceNodes) { + final var nodes = new ArrayList<>(dependentResourceNodes); + bottomLevelResource.addAll(nodes); + return dependentResourceNodes.stream() + .peek(drn -> { + // add cycle detection? + if (drn.getDependsOn().isEmpty()) { + topLevelResources.add(drn); + } else { + for (DependentResourceNode dependsOn : (List) drn + .getDependsOn()) { + bottomLevelResource.remove(dependsOn); + } + } + }) + .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); } public DependentResource getDependentResourceFor(DependentResourceNode node) { @@ -92,22 +112,6 @@ public WorkflowCleanupResult cleanup(P primary, Context

context) { return result; } - // add cycle detection? - @SuppressWarnings("unchecked") - private void preprocessForReconcile() { - final var nodes = new ArrayList<>(dependentResourceNodes.values()); - bottomLevelResource.addAll(nodes); - for (DependentResourceNode node : nodes) { - if (node.getDependsOn().isEmpty()) { - topLevelResources.add(node); - } else { - for (DependentResourceNode dependsOn : node.getDependsOn()) { - bottomLevelResource.remove(dependsOn); - } - } - } - } - Set getTopLevelDependentResources() { return topLevelResources; } @@ -125,24 +129,26 @@ Map nodes() { } @SuppressWarnings("unchecked") - void resolve(KubernetesClient client, List dependentResources) { + void resolve(KubernetesClient client, ControllerConfiguration

configuration) { if (!resolved) { - final boolean[] cleanerHolder = {false}; - dependentResourceNodes.values() - .forEach(drn -> { - drn.resolve(client, dependentResources); - final var dr = dependentResource(drn); - if (dr.isDeletable()) { - cleanerHolder[0] = true; - } - }); + dependentResourceNodes.values().forEach(drn -> drn.resolve(client, configuration)); resolved = true; - hasCleaner = cleanerHolder[0]; } } boolean hasCleaner() { - throwIfUnresolved(); return hasCleaner; } + + static boolean isDeletable(Class drClass) { + final var isDeleter = Deleter.class.isAssignableFrom(drClass); + if (!isDeleter) { + return false; + } + + if (KubernetesDependentResource.class.isAssignableFrom(drClass)) { + return !GarbageCollected.class.isAssignableFrom(drClass); + } + return true; + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java index f4ef13d122..3d5bde29db 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java @@ -8,13 +8,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowTestUtils.createDRS; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; class ManagedWorkflowSupportTest { @@ -140,9 +138,7 @@ void createsWorkflow() { createDRS(NAME_3, NAME_1), createDRS(NAME_4, NAME_3, NAME_2)); - final var client = mock(KubernetesClient.class); var workflow = managedWorkflowSupport.createWorkflow(specs); - workflow.resolve(client, specs); assertThat(workflow.nodes().values()).map(DependentResourceNode::getName) .containsExactlyInAnyOrder(NAME_1, NAME_2, NAME_3, NAME_4); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java index 96a55aa85f..d8771af5e5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -17,7 +16,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SuppressWarnings({"rawtypes", "unchecked"}) +@SuppressWarnings({"rawtypes"}) class ManagedWorkflowTest { public static final String NAME = "name"; @@ -39,6 +38,19 @@ void isNotCleanerIfGarbageCollected() { .isCleaner()).isFalse(); } + @Test + void isCleanerShouldWork() { + assertThat(managedWorkflow( + createDRSWithTraits(NAME, GarbageCollected.class), + createDRSWithTraits("foo", Deleter.class)) + .isCleaner()).isTrue(); + + assertThat(managedWorkflow( + createDRSWithTraits("foo", Deleter.class), + createDRSWithTraits(NAME, GarbageCollected.class)) + .isCleaner()).isTrue(); + } + @Test void isCleanerIfHasDeleter() { var spec = createDRSWithTraits(NAME, Deleter.class); @@ -49,12 +61,9 @@ ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) { final var configuration = mock(ControllerConfiguration.class); final var specList = List.of(specs); - KubernetesClient kubernetesClientMock = mock(KubernetesClient.class); - when(configuration.getDependentResources()).thenReturn(specList); return ConfigurationServiceProvider.instance().getWorkflowFactory() - .workflowFor(configuration) - .resolve(kubernetesClientMock, specList); + .workflowFor(configuration); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java index 52c6840b1c..9b97963366 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -31,13 +32,12 @@ public static DependentResourceSpec createDRSWithTraits(String name, Class toMock = DependentResource.class; final var garbageCollected = dependentResourceTraits != null && Arrays.asList(dependentResourceTraits).contains(GarbageCollected.class); + if (garbageCollected) { + toMock = KubernetesDependentResource.class; + } final var dr = mock(toMock, withSettings().extraInterfaces(dependentResourceTraits)); - // it would be better to call the real method here but it doesn't work because - // KubernetesDependentResource checks for GarbageCollected trait when instantiated which doesn't - // happen when using mocks - when(dr.isDeletable()).thenReturn(!garbageCollected); - when(spy.getDependentResource()).thenReturn(dr); + when(spy.getDependentResourceClass()).thenReturn(dr.getClass()); return spy; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java index 7da98f66b2..382022782e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java @@ -5,11 +5,17 @@ import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; @SuppressWarnings("rawtypes") class WorkflowTest { @@ -54,4 +60,23 @@ void calculatesBottomLevelResources() { assertThat(bottomResources).containsExactlyInAnyOrder(dr2, independentDR); } + + @Test + void isDeletableShouldWork() { + var dr = mock(DependentResource.class); + assertFalse(Workflow.isDeletable(dr.getClass())); + + dr = mock(DependentResource.class, withSettings().extraInterfaces(Deleter.class)); + assertTrue(Workflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class); + assertFalse(Workflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class)); + assertTrue(Workflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class, + GarbageCollected.class)); + assertFalse(Workflow.isDeletable(dr.getClass())); + } } From f159a29343751a2ae44cfd4e3e3e7dca31cc4086 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 9 Dec 2022 15:00:10 +0100 Subject: [PATCH 10/14] feat: compute managed workflow graphs w/o requiring dependent resources (#1647) This allows for workflows to be computed at build time, only to be resolved (i.e. associating workflow nodes to actual dependent resources) when needed. In the process, the configuration mechanism of managed dependent resource has been improved to make it possible to resolve the configuration solely from the dependent resource class instead of previously requiring the dependent to be instantiated. This is now done viw the DependentResourceConfigurationResolver class. --- .../AnnotationControllerConfiguration.java | 46 +--- .../api/config/ConfigurationService.java | 4 +- .../ControllerConfigurationOverrider.java | 48 ++-- .../DefaultControllerConfiguration.java | 35 ++- .../operator/api/config/Utils.java | 1 - .../dependent/ConfigurationConverter.java | 12 + .../api/config/dependent/Configured.java | 16 ++ ...ependentResourceConfigurationProvider.java | 6 + ...ependentResourceConfigurationResolver.java | 181 +++++++++++++++ .../dependent/DependentResourceSpec.java | 39 +--- .../dependent/DependentResourceFactory.java | 19 +- ...notationDependentResourceConfigurator.java | 11 - .../operator/processing/Controller.java | 23 +- .../KubernetesDependentConverter.java | 61 +++++ .../KubernetesDependentResource.java | 56 +---- .../AbstractDependentResourceNode.java | 103 --------- .../workflow/AbstractWorkflowExecutor.java | 7 +- .../DefaultDependentResourceNode.java | 31 --- .../workflow/DefaultManagedWorkflow.java | 123 ++++++++--- .../dependent/workflow/DefaultWorkflow.java | 146 ++++++++++++ .../workflow/DependentResourceNode.java | 107 ++++++++- .../dependent/workflow/ManagedWorkflow.java | 57 ++--- .../workflow/ManagedWorkflowFactory.java | 27 ++- .../workflow/ManagedWorkflowSupport.java | 50 +++-- .../dependent/workflow/NodeExecutor.java | 2 +- .../workflow/SpecDependentResourceNode.java | 49 ---- .../dependent/workflow/Workflow.java | 149 ++----------- .../dependent/workflow/WorkflowBuilder.java | 28 +-- .../workflow/WorkflowCleanupExecutor.java | 7 +- .../workflow/WorkflowReconcileExecutor.java | 11 +- .../ControllerConfigurationOverriderTest.java | 24 +- .../operator/api/config/UtilsTest.java | 6 - ...dentResourceConfigurationResolverTest.java | 209 ++++++++++++++++++ .../workflow/ManagedWorkflowSupportTest.java | 12 +- .../workflow/ManagedWorkflowTest.java | 14 +- .../workflow/ManagedWorkflowTestUtils.java | 2 +- .../dependent/workflow/WorkflowTest.java | 14 +- ...AnnotationControllerConfigurationTest.java | 121 +++++++++- .../ExternalStateBulkDependentReconciler.java | 11 +- .../dependent/SchemaDependentResource.java | 38 ++-- .../src/main/resources/log4j2.xml | 2 +- .../sample/MySQLSchemaOperatorE2E.java | 2 +- 42 files changed, 1188 insertions(+), 722 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index b996eeead9..4e4f945515 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -20,7 +20,6 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @@ -121,19 +120,17 @@ public ResourceEventFilter

getEventFilter() { Class>[] filterTypes = (Class>[]) valueOrDefault(annotation, ControllerConfiguration::eventFilters, new Object[] {}); - if (filterTypes.length > 0) { - for (var filterType : filterTypes) { - try { - ResourceEventFilter

filter = filterType.getConstructor().newInstance(); - - if (answer == null) { - answer = filter; - } else { - answer = answer.and(filter); - } - } catch (Exception e) { - throw new IllegalArgumentException(e); + for (var filterType : filterTypes) { + try { + ResourceEventFilter

filter = filterType.getConstructor().newInstance(); + + if (answer == null) { + answer = filter; + } else { + answer = answer.and(filter); } + } catch (Exception e) { + throw new IllegalArgumentException(e); } } return answer != null ? answer : ResourceEventFilters.passthrough(); @@ -177,22 +174,6 @@ private void configureFromAnnotatedReconciler(Object instance) { } } - @SuppressWarnings("unchecked") - private void configureFromCustomAnnotation(Object instance) { - if (instance instanceof AnnotationDependentResourceConfigurator) { - AnnotationDependentResourceConfigurator configurator = - (AnnotationDependentResourceConfigurator) instance; - final Class configurationClass = - (Class) Utils.getFirstTypeArgumentFromInterface( - instance.getClass(), AnnotationDependentResourceConfigurator.class); - final var configAnnotation = instance.getClass().getAnnotation(configurationClass); - // always called even if the annotation is null so that implementations can provide default - // values - final var config = configurator.configFrom(configAnnotation, this); - configurator.configureWith(config); - } - } - @Override @SuppressWarnings("unchecked") public Optional> onAddFilter() { @@ -239,15 +220,10 @@ public List getDependentResources() { "A DependentResource named '" + name + "' already exists: " + spec); } - final var dependentResource = Utils.instantiateAndConfigureIfNeeded(dependentType, - DependentResource.class, - Utils.contextFor(this, dependentType, Dependent.class), - this::configureFromCustomAnnotation); - var eventSourceName = dependent.useEventSourceWithName(); eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; final var context = Utils.contextFor(this, dependentType, null); - spec = new DependentResourceSpec(dependentResource, name, + spec = new DependentResourceSpec(dependentType, name, Set.of(dependent.dependsOn()), Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), 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 036aa76bbf..45d7162184 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 @@ -145,9 +145,9 @@ default ObjectMapper getObjectMapper() { return Serialization.jsonMapper(); } - @Deprecated(forRemoval = true) + @SuppressWarnings("rawtypes") default DependentResourceFactory dependentResourceFactory() { - return null; + return DependentResourceFactory.DEFAULT; } default Optional getLeaderElectionConfiguration() { 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 1f5494050e..31de28631d 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 @@ -1,17 +1,15 @@ package io.javaoperatorsdk.operator.api.config; import java.time.Duration; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; import java.util.Set; import java.util.function.UnaryOperator; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -34,12 +32,12 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private final LinkedHashMap namedDependentResourceSpecs; private OnAddFilter onAddFilter; private OnUpdateFilter onUpdateFilter; private GenericFilter genericFilter; private RateLimiter rateLimiter; private UnaryOperator cachePruneFunction; + private Map configurations; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizerName(); @@ -49,13 +47,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); reconciliationMaxInterval = original.maxReconciliationInterval().orElse(null); - // make the original specs modifiable - final var dependentResources = original.getDependentResources(); - namedDependentResourceSpecs = new LinkedHashMap<>(dependentResources.size()); this.onAddFilter = original.onAddFilter().orElse(null); this.onUpdateFilter = original.onUpdateFilter().orElse(null); this.genericFilter = original.genericFilter().orElse(null); - dependentResources.forEach(drs -> namedDependentResourceSpecs.put(drs.getName(), drs)); this.original = original; this.rateLimiter = original.getRateLimiter(); this.cachePruneFunction = original.cachePruneFunction().orElse(null); @@ -167,40 +161,24 @@ public ControllerConfigurationOverrider withCachePruneFunction( return this; } - @SuppressWarnings("unchecked") public ControllerConfigurationOverrider replacingNamedDependentResourceConfig(String name, Object dependentResourceConfig) { - var current = namedDependentResourceSpecs.get(name); - if (current == null) { - throw new IllegalArgumentException("Cannot find a DependentResource named: " + name); - } + final var specs = original.getDependentResources(); + final var spec = specs.stream() + .filter(drs -> drs.getName().equals(name)).findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Cannot find a DependentResource named: " + name)); - var dependentResource = current.getDependentResource(); - if (dependentResource instanceof DependentResourceConfigurator) { - var configurator = (DependentResourceConfigurator) dependentResource; - configurator.configureWith(dependentResourceConfig); + if (configurations == null) { + configurations = new HashMap<>(specs.size()); } + configurations.put(spec, dependentResourceConfig); return this; } public ControllerConfiguration build() { - final var hasModifiedNamespaces = !original.getNamespaces().equals(namespaces); - final var newDependentSpecs = namedDependentResourceSpecs.values().stream() - .peek(spec -> { - // if the dependent resource has a NamespaceChangeable config - // update the namespaces if needed, otherwise, do nothing - if (hasModifiedNamespaces) { - final Optional maybeConfig = spec.getDependentResourceConfiguration(); - maybeConfig - .filter(NamespaceChangeable.class::isInstance) - .map(NamespaceChangeable.class::cast) - .filter(NamespaceChangeable::allowsNamespaceChanges) - .ifPresent(nc -> nc.changeNamespaces(namespaces)); - } - }).collect(Collectors.toList()); - return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), original.getName(), @@ -217,7 +195,9 @@ public ControllerConfiguration build() { onUpdateFilter, genericFilter, rateLimiter, - newDependentSpecs, cachePruneFunction); + original.getDependentResources(), + cachePruneFunction, + configurations); } public static ControllerConfigurationOverrider override( 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 f6277c4139..6dd99ecbe2 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 @@ -3,11 +3,13 @@ import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationProvider; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -20,7 +22,7 @@ @SuppressWarnings("rawtypes") public class DefaultControllerConfiguration extends DefaultResourceConfiguration - implements ControllerConfiguration { + implements ControllerConfiguration, DependentResourceConfigurationProvider { private final String associatedControllerClassName; private final String name; @@ -32,6 +34,7 @@ public class DefaultControllerConfiguration private final List dependents; private final Duration reconciliationMaxInterval; private final RateLimiter rateLimiter; + private final Map configurations; // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( @@ -52,6 +55,31 @@ public DefaultControllerConfiguration( RateLimiter rateLimiter, List dependents, UnaryOperator cachePruneFunction) { + this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, + retry, labelSelector, resourceEventFilter, resourceClass, reconciliationMaxInterval, + onAddFilter, onUpdateFilter, genericFilter, rateLimiter, dependents, cachePruneFunction, + null); + } + + DefaultControllerConfiguration( + String associatedControllerClassName, + String name, + String crdName, + String finalizer, + boolean generationAware, + Set namespaces, + Retry retry, + String labelSelector, + ResourceEventFilter resourceEventFilter, + Class resourceClass, + Duration reconciliationMaxInterval, + OnAddFilter onAddFilter, + OnUpdateFilter onUpdateFilter, + GenericFilter genericFilter, + RateLimiter rateLimiter, + List dependents, + UnaryOperator cachePruneFunction, + Map configurations) { super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces, cachePruneFunction); this.associatedControllerClassName = associatedControllerClassName; @@ -68,6 +96,7 @@ public DefaultControllerConfiguration( this.rateLimiter = rateLimiter != null ? rateLimiter : LinearRateLimiter.deactivatedRateLimiter(); this.dependents = dependents != null ? dependents : Collections.emptyList(); + this.configurations = configurations != null ? configurations : Collections.emptyMap(); } @Override @@ -120,4 +149,8 @@ public RateLimiter getRateLimiter() { return rateLimiter; } + @Override + public Object getConfigurationFor(DependentResourceSpec spec) { + return configurations.get(spec); + } } 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 fd0f612fa1..00927b7f14 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 @@ -237,7 +237,6 @@ public static String contextFor(ControllerConfiguration controllerConfigurati } context += "reconciler: " + controllerConfiguration.getName(); - return context; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java new file mode 100644 index 0000000000..68e0f521de --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; + +public interface ConfigurationConverter> { + + C configFrom(A configAnnotation, ControllerConfiguration parentConfiguration, + Class originatingClass); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java new file mode 100644 index 0000000000..db8c6f6db3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Configured { + + Class by(); + + Class with(); + + @SuppressWarnings("rawtypes") + Class converter(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java new file mode 100644 index 0000000000..a0c9dc67ae --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +public interface DependentResourceConfigurationProvider { + @SuppressWarnings("rawtypes") + Object getConfigurationFor(DependentResourceSpec spec); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java new file mode 100644 index 0000000000..9a143fc57c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java @@ -0,0 +1,181 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class DependentResourceConfigurationResolver { + + private DependentResourceConfigurationResolver() {} + + private static final Map, ConverterAnnotationPair> converters = + new HashMap<>(); + private static final Map, ConfigurationConverter> knownConverters = + new HashMap<>(); + + public static > void configure( + DependentResource dependentResource, DependentResourceSpec spec, C parentConfiguration) { + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + final var config = configurationFor(spec, parentConfiguration); + configurator.configureWith(config); + } + } + + public static > Object configurationFor( + DependentResourceSpec spec, C parentConfiguration) { + + // first check if the parent configuration has potentially already resolved the configuration + if (parentConfiguration instanceof DependentResourceConfigurationProvider) { + final var provider = (DependentResourceConfigurationProvider) parentConfiguration; + final var configuration = provider.getConfigurationFor(spec); + if (configuration != null) { + return configuration; + } + } + + // find Configured-annotated class if it exists + return extractConfigurationFromConfigured(spec.getDependentResourceClass(), + parentConfiguration); + } + + public static > Object extractConfigurationFromConfigured( + Class dependentResourceClass, C parentConfiguration) { + var converterAnnotationPair = converters.get(dependentResourceClass); + + Annotation configAnnotation; + if (converterAnnotationPair == null) { + var configuredClassPair = getConfigured(dependentResourceClass); + if (configuredClassPair == null) { + return null; + } + + // check if we already have a converter registered for the found Configured annotated class + converterAnnotationPair = converters.get(configuredClassPair.annotatedClass); + if (converterAnnotationPair == null) { + final var configured = configuredClassPair.configured; + converterAnnotationPair = + getOrCreateConverter(dependentResourceClass, parentConfiguration, + configured.converter(), + configured.by()); + } else { + // only register the converter pair for this dependent resource class as well + converters.put(dependentResourceClass, converterAnnotationPair); + } + } + + // find the associated configuration annotation + configAnnotation = + dependentResourceClass.getAnnotation(converterAnnotationPair.annotationClass); + final var converter = converterAnnotationPair.converter; + + // always called even if the annotation is null so that implementations can provide default + // values + return converter.configFrom(configAnnotation, parentConfiguration, dependentResourceClass); + } + + private static ConfiguredClassPair getConfigured( + Class dependentResourceClass) { + Class currentClass = dependentResourceClass; + Configured configured; + ConfiguredClassPair result = null; + while (DependentResource.class.isAssignableFrom(currentClass)) { + configured = currentClass.getAnnotation(Configured.class); + if (configured != null) { + result = new ConfiguredClassPair(configured, currentClass); + break; + } + currentClass = (Class) currentClass.getSuperclass(); + } + return result; + } + + private static > ConverterAnnotationPair getOrCreateConverter( + Class dependentResourceClass, C parentConfiguration, + Class converterClass, + Class annotationClass) { + var converterPair = converters.get(dependentResourceClass); + if (converterPair == null) { + // only instantiate a new converter if we haven't done so already for this converter type + var converter = knownConverters.get(converterClass); + if (converter == null) { + converter = Utils.instantiate(converterClass, + ConfigurationConverter.class, + Utils.contextFor(parentConfiguration, dependentResourceClass, Configured.class)); + knownConverters.put(converterClass, converter); + } + // record dependent class - converter association for faster future retrieval + converterPair = new ConverterAnnotationPair(converter, annotationClass); + converters.put(dependentResourceClass, converterPair); + } + return converterPair; + } + + static ConfigurationConverter getConverter( + Class dependentResourceClass) { + final var converterAnnotationPair = converters.get(dependentResourceClass); + return converterAnnotationPair != null ? converterAnnotationPair.converter : null; + } + + @SuppressWarnings("unused") + public static void registerConverter(Class dependentResourceClass, + ConfigurationConverter converter) { + var configured = getConfigured(dependentResourceClass); + if (configured == null) { + throw new IllegalArgumentException("There is no @" + Configured.class.getSimpleName() + + " annotation on " + dependentResourceClass.getName() + + " or its superclasses and thus doesn't need to be associated with a converter"); + } + + // find the associated configuration annotation + final var toRegister = new ConverterAnnotationPair(converter, configured.configured.by()); + final Class converterClass = converter.getClass(); + converters.put(dependentResourceClass, toRegister); + + // also register the Configured-annotated class if not the one we're registering + if (!dependentResourceClass.equals(configured.annotatedClass)) { + converters.put(configured.annotatedClass, toRegister); + } + + knownConverters.put(converterClass, converter); + } + + private static class ConfiguredClassPair { + private final Configured configured; + private final Class annotatedClass; + + private ConfiguredClassPair(Configured configured, + Class annotatedClass) { + this.configured = configured; + this.annotatedClass = annotatedClass; + } + + @Override + public String toString() { + return annotatedClass.getName() + " -> " + configured; + } + } + + private static class ConverterAnnotationPair { + private final ConfigurationConverter converter; + private final Class annotationClass; + + private ConverterAnnotationPair(ConfigurationConverter converter, + Class annotationClass) { + this.converter = converter; + this.annotationClass = annotationClass; + } + + @Override + public String toString() { + return converter.toString() + " -> " + annotationClass.getName(); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java index 5d0b8c6b01..58fd9ace4b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -6,12 +6,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -public class DependentResourceSpec { +public class DependentResourceSpec { - private final DependentResource dependentResource; + private final Class> dependentResourceClass; private final String name; @@ -25,11 +24,11 @@ public class DependentResourceSpec { private final String useEventSourceWithName; - public DependentResourceSpec(DependentResource dependentResource, + public DependentResourceSpec(Class> dependentResourceClass, String name, Set dependsOn, Condition readyCondition, Condition reconcileCondition, Condition deletePostCondition, String useEventSourceWithName) { - this.dependentResource = dependentResource; + this.dependentResourceClass = dependentResourceClass; this.name = name; this.dependsOn = dependsOn; this.readyCondition = readyCondition; @@ -38,28 +37,8 @@ public DependentResourceSpec(DependentResource dependentResource, this.useEventSourceWithName = useEventSourceWithName; } - public DependentResourceSpec(DependentResourceSpec other) { - this.dependentResource = other.dependentResource; - this.name = other.name; - this.dependsOn = other.dependsOn; - this.readyCondition = other.readyCondition; - this.reconcileCondition = other.reconcileCondition; - this.deletePostCondition = other.deletePostCondition; - this.useEventSourceWithName = other.useEventSourceWithName; - } - - @SuppressWarnings("unchecked") - public Class> getDependentResourceClass() { - return (Class>) dependentResource.getClass(); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - public Optional getDependentResourceConfiguration() { - if (dependentResource instanceof DependentResourceConfigurator) { - var configurator = (DependentResourceConfigurator) dependentResource; - return configurator.configuration(); - } - return Optional.empty(); + public Class> getDependentResourceClass() { + return dependentResourceClass; } public String getName() { @@ -80,7 +59,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - DependentResourceSpec that = (DependentResourceSpec) o; + DependentResourceSpec that = (DependentResourceSpec) o; return name.equals(that.name); } @@ -108,10 +87,6 @@ public Condition getDeletePostCondition() { return deletePostCondition; } - public DependentResource getDependentResource() { - return dependentResource; - } - public Optional getUseEventSourceWithName() { return Optional.ofNullable(useEventSourceWithName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java index 139d70b002..e9e47f6d97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java @@ -1,18 +1,21 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -@Deprecated +import static io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver.configure; + @SuppressWarnings({"rawtypes", "unchecked"}) -public interface DependentResourceFactory { +public interface DependentResourceFactory> { - default DependentResource createFrom(DependentResourceSpec spec) { - return createFrom(spec.getDependentResourceClass()); - } + DependentResourceFactory DEFAULT = new DependentResourceFactory() {}; - default T createFrom(Class dependentResourceClass) { - return (T) Utils.instantiate(dependentResourceClass, DependentResource.class, null); + default DependentResource createFrom(DependentResourceSpec spec, C configuration) { + final var dependentResourceClass = spec.getDependentResourceClass(); + return Utils.instantiateAndConfigureIfNeeded(dependentResourceClass, + DependentResource.class, + Utils.contextFor(configuration, dependentResourceClass, Dependent.class), + (instance) -> configure(instance, spec, configuration)); } - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java deleted file mode 100644 index d65249b753..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; - -import java.lang.annotation.Annotation; - -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; - -public interface AnnotationDependentResourceConfigurator - extends DependentResourceConfigurator { - - C configFrom(A annotation, ControllerConfiguration parentConfiguration); -} 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 0fa167e3c2..8753e0862b 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 @@ -40,7 +40,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; import io.javaoperatorsdk.operator.health.ControllerHealthInfo; -import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; import io.javaoperatorsdk.operator.processing.event.EventProcessor; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -64,7 +64,7 @@ public class Controller

private final boolean contextInitializer; private final boolean isCleaner; private final Metrics metrics; - private final ManagedWorkflow

managedWorkflow; + private final Workflow

managedWorkflow; private final GroupVersionKind associatedGVK; private final EventProcessor

eventProcessor; @@ -83,8 +83,9 @@ public Controller(Reconciler

reconciler, this.metrics = Optional.ofNullable(configurationService.getMetrics()).orElse(Metrics.NOOP); contextInitializer = reconciler instanceof ContextInitializer; isCleaner = reconciler instanceof Cleaner; - managedWorkflow = configurationService.getWorkflowFactory().workflowFor(configuration); - managedWorkflow.resolve(kubernetesClient, configuration); + + final var managed = configurationService.getWorkflowFactory().workflowFor(configuration); + managedWorkflow = managed.resolve(kubernetesClient, configuration); eventSourceManager = new EventSourceManager<>(this); eventProcessor = new EventProcessor<>(eventSourceManager); @@ -135,7 +136,7 @@ public Map metadata() { @Override public UpdateControl

execute() throws Exception { initContextIfNeeded(resource, context); - if (!managedWorkflow.isEmptyWorkflow()) { + if (!managedWorkflow.isEmpty()) { var res = managedWorkflow.reconcile(resource, context); ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) .setWorkflowExecutionResult(res); @@ -180,7 +181,7 @@ public Map metadata() { public DeleteControl execute() { initContextIfNeeded(resource, context); WorkflowCleanupResult workflowCleanupResult = null; - if (managedWorkflow.isCleaner()) { + if (managedWorkflow.hasCleaner()) { workflowCleanupResult = managedWorkflow.cleanup(resource, context); ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) .setWorkflowCleanupResult(workflowCleanupResult); @@ -231,13 +232,13 @@ public void initAndRegisterEventSources(EventSourceContext

context) { final var dependentResourcesByName = managedWorkflow.getDependentResourcesByName(); final var size = dependentResourcesByName.size(); if (size > 0) { - dependentResourcesByName.forEach((key, value) -> { - if (value instanceof EventSourceProvider) { - final var provider = (EventSourceProvider) value; + dependentResourcesByName.forEach((key, dependentResource) -> { + if (dependentResource instanceof EventSourceProvider) { + final var provider = (EventSourceProvider) dependentResource; final var source = provider.initEventSource(context); eventSourceManager.registerEventSource(key, source); } else { - Optional eventSource = value.eventSource(context); + Optional eventSource = dependentResource.eventSource(context); eventSource.ifPresent(es -> eventSourceManager.registerEventSource(key, es)); } }); @@ -417,7 +418,7 @@ public synchronized void stop() { } public boolean useFinalizer() { - return isCleaner || managedWorkflow.isCleaner(); + return isCleaner || managedWorkflow.hasCleaner(); } public GroupVersionKind getAssociatedGroupVersionKind() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java new file mode 100644 index 0000000000..a9a60f8e0a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java @@ -0,0 +1,61 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Arrays; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; + +public class KubernetesDependentConverter implements + ConfigurationConverter, KubernetesDependentResource> { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public KubernetesDependentResourceConfig configFrom(KubernetesDependent configAnnotation, + ControllerConfiguration parentConfiguration, + Class> originatingClass) { + var namespaces = parentConfiguration.getNamespaces(); + var configuredNS = false; + String labelSelector = null; + OnAddFilter onAddFilter = null; + OnUpdateFilter onUpdateFilter = null; + OnDeleteFilter onDeleteFilter = null; + GenericFilter genericFilter = null; + ResourceDiscriminator resourceDiscriminator = null; + if (configAnnotation != null) { + if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, configAnnotation.namespaces())) { + namespaces = Set.of(configAnnotation.namespaces()); + configuredNS = true; + } + + final var fromAnnotation = configAnnotation.labelSelector(); + labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; + + final var context = + Utils.contextFor(parentConfiguration, originatingClass, + configAnnotation.annotationType()); + onAddFilter = Utils.instantiate(configAnnotation.onAddFilter(), OnAddFilter.class, context); + onUpdateFilter = + Utils.instantiate(configAnnotation.onUpdateFilter(), OnUpdateFilter.class, context); + onDeleteFilter = + Utils.instantiate(configAnnotation.onDeleteFilter(), OnDeleteFilter.class, context); + genericFilter = + Utils.instantiate(configAnnotation.genericFilter(), GenericFilter.class, context); + + resourceDiscriminator = + Utils.instantiate(configAnnotation.resourceDiscriminator(), ResourceDiscriminator.class, + context); + } + + return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, + resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 5631536da5..a8bfcb798c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; -import java.util.Arrays; import java.util.HashMap; import java.util.Optional; import java.util.Set; @@ -12,35 +11,30 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource; import io.javaoperatorsdk.operator.processing.dependent.Matcher; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @Ignore -@SuppressWarnings("rawtypes") +@Configured(by = KubernetesDependent.class, with = KubernetesDependentResourceConfig.class, + converter = KubernetesDependentConverter.class) public abstract class KubernetesDependentResource extends AbstractEventSourceHolderDependentResource> implements KubernetesClientAware, - AnnotationDependentResourceConfigurator> { + DependentResourceConfigurator> { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -250,46 +244,6 @@ private void cleanupAfterEventFiltering(ResourceID resourceID) { .cleanupOnCreateOrUpdateEventFiltering(resourceID); } - @Override - @SuppressWarnings("unchecked") - public KubernetesDependentResourceConfig configFrom(KubernetesDependent kubeDependent, - ControllerConfiguration parentConfiguration) { - var namespaces = parentConfiguration.getNamespaces(); - var configuredNS = false; - String labelSelector = null; - OnAddFilter onAddFilter = null; - OnUpdateFilter onUpdateFilter = null; - OnDeleteFilter onDeleteFilter = null; - GenericFilter genericFilter = null; - ResourceDiscriminator resourceDiscriminator = null; - if (kubeDependent != null) { - if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, kubeDependent.namespaces())) { - namespaces = Set.of(kubeDependent.namespaces()); - configuredNS = true; - } - - final var fromAnnotation = kubeDependent.labelSelector(); - labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; - - final var context = - Utils.contextFor(parentConfiguration, getClass(), kubeDependent.annotationType()); - onAddFilter = Utils.instantiate(kubeDependent.onAddFilter(), OnAddFilter.class, context); - onUpdateFilter = - Utils.instantiate(kubeDependent.onUpdateFilter(), OnUpdateFilter.class, context); - onDeleteFilter = - Utils.instantiate(kubeDependent.onDeleteFilter(), OnDeleteFilter.class, context); - genericFilter = - Utils.instantiate(kubeDependent.genericFilter(), GenericFilter.class, context); - - resourceDiscriminator = - Utils.instantiate(kubeDependent.resourceDiscriminator(), ResourceDiscriminator.class, - context); - } - - return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, - resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter); - } - @Override public Optional> configuration() { return Optional.ofNullable(kubernetesDependentResourceConfig); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java deleted file mode 100644 index 22e368249a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -@SuppressWarnings("rawtypes") -abstract class AbstractDependentResourceNode - implements DependentResourceNode { - - private final List dependsOn = new LinkedList<>(); - private final List parents = new LinkedList<>(); - private final String name; - private Condition reconcilePrecondition; - private Condition deletePostcondition; - private Condition readyPostcondition; - private DependentResource dependentResource; - - protected AbstractDependentResourceNode(String name) { - this.name = name; - } - - @Override - public List getDependsOn() { - return dependsOn; - } - - @Override - public void addParent(DependentResourceNode parent) { - parents.add(parent); - } - - @Override - public void addDependsOnRelation(DependentResourceNode node) { - node.addParent(this); - dependsOn.add(node); - } - - @Override - public List getParents() { - return parents; - } - - @Override - public String getName() { - return name; - } - - @Override - public Optional> getReconcilePrecondition() { - return Optional.ofNullable(reconcilePrecondition); - } - - @Override - public Optional> getDeletePostcondition() { - return Optional.ofNullable(deletePostcondition); - } - - public void setReconcilePrecondition(Condition reconcilePrecondition) { - this.reconcilePrecondition = reconcilePrecondition; - } - - public void setDeletePostcondition(Condition cleanupCondition) { - this.deletePostcondition = cleanupCondition; - } - - @Override - public Optional> getReadyPostcondition() { - return Optional.ofNullable(readyPostcondition); - } - - public void setReadyPostcondition(Condition readyPostcondition) { - this.readyPostcondition = readyPostcondition; - } - - public DependentResource getDependentResource() { - return dependentResource; - } - - public void setDependentResource(DependentResource dependentResource) { - this.dependentResource = dependentResource; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AbstractDependentResourceNode that = (AbstractDependentResourceNode) o; - return name.equals(that.name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java index 354975ebad..76db792468 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java @@ -87,7 +87,7 @@ protected boolean isInError(DependentResourceNode dependentResourceNode) { protected Map getErroredDependents() { return exceptionsDuringExecution.entrySet().stream() .collect( - Collectors.toMap(e -> workflow.getDependentResourceFor(e.getKey()), Entry::getValue)); + Collectors.toMap(e -> e.getKey().getDependentResource(), Entry::getValue)); } protected synchronized void handleNodeExecutionFinish( @@ -99,11 +99,6 @@ protected synchronized void handleNodeExecutionFinish( } } - @SuppressWarnings("unchecked") - protected DependentResource getDependentResourceFor(DependentResourceNode drn) { - return (DependentResource) workflow.getDependentResourceFor(drn); - } - protected boolean isConditionMet(Optional> condition, DependentResource dependentResource) { return condition.map(c -> c.isMet(primary, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java deleted file mode 100644 index d0d844ea9c..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -class DefaultDependentResourceNode - extends AbstractDependentResourceNode { - - public DefaultDependentResourceNode(DependentResource dependentResource) { - this(dependentResource, null, null); - } - - public DefaultDependentResourceNode(DependentResource dependentResource, - Condition reconcilePrecondition, Condition deletePostcondition) { - super(getNameFor(dependentResource)); - setDependentResource(dependentResource); - setReconcilePrecondition(reconcilePrecondition); - setDeletePostcondition(deletePostcondition); - } - - @SuppressWarnings("rawtypes") - static String getNameFor(DependentResource dependentResource) { - return DependentResource.defaultNameFor(dependentResource.getClass()) + "#" - + dependentResource.hashCode(); - } - - @Override - public String toString() { - return "DependentResourceNode{" + getDependentResource() + '}'; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java index b9bc29a32f..55b9ee6edd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -1,67 +1,126 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; + +import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; @SuppressWarnings("rawtypes") public class DefaultManagedWorkflow

implements ManagedWorkflow

{ - private final Workflow

workflow; - private final boolean isEmptyWorkflow; - private boolean resolved; + private final Set topLevelResources; + private final Set bottomLevelResources; + private final List> orderedSpecs; + private final boolean hasCleaner; + + protected DefaultManagedWorkflow(List> orderedSpecs, + boolean hasCleaner) { + this.hasCleaner = hasCleaner; + topLevelResources = new HashSet<>(orderedSpecs.size()); + bottomLevelResources = orderedSpecs.stream() + .map(DependentResourceSpec::getName) + .collect(Collectors.toSet()); + this.orderedSpecs = orderedSpecs; + orderedSpecs.forEach(spec -> { + // add cycle detection? + if (spec.getDependsOn().isEmpty()) { + topLevelResources.add(spec.getName()); + } else { + for (String dependsOn : spec.getDependsOn()) { + bottomLevelResources.remove(dependsOn); + } + } + }); + } - DefaultManagedWorkflow(List dependentResourceSpecs, Workflow

workflow) { - isEmptyWorkflow = dependentResourceSpecs.isEmpty(); - this.workflow = workflow; + @Override + @SuppressWarnings("unused") + public List> getOrderedSpecs() { + return orderedSpecs; } - public WorkflowReconcileResult reconcile(P primary, Context

context) { - checkIfResolved(); - return workflow.reconcile(primary, context); + protected Set getTopLevelResources() { + return topLevelResources; } - public WorkflowCleanupResult cleanup(P primary, Context

context) { - checkIfResolved(); - return workflow.cleanup(primary, context); + protected Set getBottomLevelResources() { + return bottomLevelResources; } - public boolean isCleaner() { - return workflow.hasCleaner(); + List nodeNames() { + return orderedSpecs.stream().map(DependentResourceSpec::getName).collect(Collectors.toList()); } - public boolean isEmptyWorkflow() { - return isEmptyWorkflow; + @Override + public boolean hasCleaner() { + return hasCleaner; } - public Map getDependentResourcesByName() { - checkIfResolved(); - final var nodes = workflow.nodes(); - final var result = new HashMap(nodes.size()); - nodes.forEach((key, drn) -> result.put(key, workflow.getDependentResourceFor(drn))); - return result; + @Override + public boolean isEmpty() { + return orderedSpecs.isEmpty(); } @Override - public ManagedWorkflow

resolve(KubernetesClient client, + @SuppressWarnings("unchecked") + public Workflow

resolve(KubernetesClient client, ControllerConfiguration

configuration) { - if (!resolved) { - workflow.resolve(client, configuration); - resolved = true; + final var alreadyResolved = new HashMap(orderedSpecs.size()); + for (DependentResourceSpec spec : orderedSpecs) { + final var node = new DependentResourceNode(spec.getName(), + spec.getReconcileCondition(), + spec.getDeletePostCondition(), + spec.getReadyCondition(), + resolve(spec, client, configuration)); + alreadyResolved.put(node.getName(), node); + spec.getDependsOn() + .forEach(depend -> node.addDependsOnRelation(alreadyResolved.get((String) depend))); } - return this; + + final var bottom = + bottomLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet()); + final var top = + topLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet()); + return new DefaultWorkflow<>(alreadyResolved, bottom, top, + THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, hasCleaner); } - private void checkIfResolved() { - if (!resolved) { - throw new IllegalStateException("resolve should be called before"); + @SuppressWarnings({"rawtypes", "unchecked"}) + private DependentResource resolve(DependentResourceSpec spec, + KubernetesClient client, + ControllerConfiguration

configuration) { + final DependentResource dependentResource = + ConfigurationServiceProvider.instance().dependentResourceFactory() + .createFrom(spec, configuration); + + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } + + spec.getUseEventSourceWithName() + .ifPresent(esName -> { + if (dependentResource instanceof EventSourceReferencer) { + ((EventSourceReferencer) dependentResource).useEventSourceWithName(esName); + } else { + throw new IllegalStateException( + "DependentResource " + spec + " wants to use EventSource named " + esName + + " but doesn't implement support for this feature by implementing " + + EventSourceReferencer.class.getSimpleName()); + } + }); + + return dependentResource; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java new file mode 100644 index 0000000000..a151ff0947 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java @@ -0,0 +1,146 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +/** + * Dependents definition: so if B depends on A, the B is dependent of A. + * + * @param

primary resource + */ +@SuppressWarnings("rawtypes") +public class DefaultWorkflow

implements Workflow

{ + + private final Map dependentResourceNodes; + private final Set topLevelResources; + private final Set bottomLevelResource; + + private final boolean throwExceptionAutomatically; + private final boolean hasCleaner; + + DefaultWorkflow(Set dependentResourceNodes) { + this(dependentResourceNodes, THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false); + } + + DefaultWorkflow(Set dependentResourceNodes, + boolean throwExceptionAutomatically, + boolean hasCleaner) { + this.throwExceptionAutomatically = throwExceptionAutomatically; + this.hasCleaner = hasCleaner; + + if (dependentResourceNodes == null) { + this.topLevelResources = Collections.emptySet(); + this.bottomLevelResource = Collections.emptySet(); + this.dependentResourceNodes = Collections.emptyMap(); + } else { + this.topLevelResources = new HashSet<>(dependentResourceNodes.size()); + this.bottomLevelResource = new HashSet<>(dependentResourceNodes); + this.dependentResourceNodes = toMap(dependentResourceNodes); + } + } + + protected DefaultWorkflow(Map dependentResourceNodes, + Set bottomLevelResource, Set topLevelResources, + boolean throwExceptionAutomatically, + boolean hasCleaner) { + this.throwExceptionAutomatically = throwExceptionAutomatically; + this.hasCleaner = hasCleaner; + + this.topLevelResources = topLevelResources; + this.bottomLevelResource = bottomLevelResource; + this.dependentResourceNodes = dependentResourceNodes; + } + + @SuppressWarnings("unchecked") + private Map toMap( + Set dependentResourceNodes) { + return dependentResourceNodes.stream() + .peek(drn -> { + // add cycle detection? + if (drn.getDependsOn().isEmpty()) { + topLevelResources.add(drn); + } else { + for (DependentResourceNode dependsOn : (List) drn + .getDependsOn()) { + bottomLevelResource.remove(dependsOn); + } + } + }) + .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); + } + + @Override + public WorkflowReconcileResult reconcile(P primary, Context

context) { + WorkflowReconcileExecutor

workflowReconcileExecutor = + new WorkflowReconcileExecutor<>(this, primary, context); + var result = workflowReconcileExecutor.reconcile(); + if (throwExceptionAutomatically) { + result.throwAggregateExceptionIfErrorsPresent(); + } + return result; + } + + @Override + public WorkflowCleanupResult cleanup(P primary, Context

context) { + WorkflowCleanupExecutor

workflowCleanupExecutor = + new WorkflowCleanupExecutor<>(this, primary, context); + var result = workflowCleanupExecutor.cleanup(); + if (throwExceptionAutomatically) { + result.throwAggregateExceptionIfErrorsPresent(); + } + return result; + } + + @Override + public Set getTopLevelDependentResources() { + return topLevelResources; + } + + @Override + public Set getBottomLevelResource() { + return bottomLevelResource; + } + + @Override + public boolean hasCleaner() { + return hasCleaner; + } + + static boolean isDeletable(Class drClass) { + final var isDeleter = Deleter.class.isAssignableFrom(drClass); + if (!isDeleter) { + return false; + } + + if (KubernetesDependentResource.class.isAssignableFrom(drClass)) { + return !GarbageCollected.class.isAssignableFrom(drClass); + } + return true; + } + + @Override + public boolean isEmpty() { + return dependentResourceNodes.isEmpty(); + } + + @Override + public Map getDependentResourcesByName() { + final var resources = new HashMap(dependentResourceNodes.size()); + dependentResourceNodes + .forEach((name, node) -> resources.put(name, node.getDependentResource())); + return resources; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java index 11700c8d25..6d82c8cb17 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java @@ -1,30 +1,113 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; +import java.util.LinkedList; import java.util.List; import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @SuppressWarnings("rawtypes") -public interface DependentResourceNode { +public class DependentResourceNode { - Optional> getReconcilePrecondition(); + private final List dependsOn = new LinkedList<>(); + private final List parents = new LinkedList<>(); + private final String name; + private Condition reconcilePrecondition; + private Condition deletePostcondition; + private Condition readyPostcondition; + private final DependentResource dependentResource; - Optional> getDeletePostcondition(); + DependentResourceNode(DependentResource dependentResource) { + this(getNameFor(dependentResource), null, null, null, dependentResource); + } - List getDependsOn(); + public DependentResourceNode(String name, Condition reconcilePrecondition, + Condition deletePostcondition, Condition readyPostcondition, + DependentResource dependentResource) { + this.name = name; + this.reconcilePrecondition = reconcilePrecondition; + this.deletePostcondition = deletePostcondition; + this.readyPostcondition = readyPostcondition; + this.dependentResource = dependentResource; + } - void addDependsOnRelation(DependentResourceNode node); + public List getDependsOn() { + return dependsOn; + } - Optional> getReadyPostcondition(); + void addParent(DependentResourceNode parent) { + parents.add(parent); + } - List getParents(); + void addDependsOnRelation(DependentResourceNode node) { + node.addParent(this); + dependsOn.add(node); + } - void addParent(DependentResourceNode parent); + public List getParents() { + return parents; + } - String getName(); + public String getName() { + return name; + } - default void resolve(KubernetesClient client, ControllerConfiguration

configuration) {} + + public Optional> getReconcilePrecondition() { + return Optional.ofNullable(reconcilePrecondition); + } + + + public Optional> getDeletePostcondition() { + return Optional.ofNullable(deletePostcondition); + } + + void setReconcilePrecondition(Condition reconcilePrecondition) { + this.reconcilePrecondition = reconcilePrecondition; + } + + void setDeletePostcondition(Condition cleanupCondition) { + this.deletePostcondition = cleanupCondition; + } + + public Optional> getReadyPostcondition() { + return Optional.ofNullable(readyPostcondition); + } + + void setReadyPostcondition(Condition readyPostcondition) { + this.readyPostcondition = readyPostcondition; + } + + public DependentResource getDependentResource() { + return dependentResource; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DependentResourceNode that = (DependentResourceNode) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @SuppressWarnings("rawtypes") + static String getNameFor(DependentResource dependentResource) { + return DependentResource.defaultNameFor(dependentResource.getClass()) + "#" + + dependentResource.hashCode(); + } + + @Override + public String toString() { + return "DependentResourceNode{" + getDependentResource() + '}'; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java index 518ab69289..2de3075818 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java @@ -1,58 +1,27 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; import java.util.Collections; -import java.util.Map; +import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -@SuppressWarnings("rawtypes") public interface ManagedWorkflow

{ - ManagedWorkflow noOpWorkflow = new ManagedWorkflow() { - @Override - public WorkflowReconcileResult reconcile(HasMetadata primary, Context context) { - throw new IllegalStateException("Shouldn't be called"); - } + @SuppressWarnings("unused") + default List> getOrderedSpecs() { + return Collections.emptyList(); + } - @Override - public WorkflowCleanupResult cleanup(HasMetadata primary, Context context) { - throw new IllegalStateException("Shouldn't be called"); - } + default boolean hasCleaner() { + return false; + } - @Override - public boolean isCleaner() { - return false; - } + default boolean isEmpty() { + return true; + } - @Override - public boolean isEmptyWorkflow() { - return true; - } - - @Override - public Map getDependentResourcesByName() { - return Collections.emptyMap(); - } - - @Override - public ManagedWorkflow resolve(KubernetesClient client, ControllerConfiguration configuration) { - return this; - } - }; - - WorkflowReconcileResult reconcile(P primary, Context

context); - - WorkflowCleanupResult cleanup(P primary, Context

context); - - boolean isCleaner(); - - boolean isEmptyWorkflow(); - - Map getDependentResourcesByName(); - - ManagedWorkflow

resolve(KubernetesClient client, ControllerConfiguration

configuration); + Workflow

resolve(KubernetesClient client, ControllerConfiguration

configuration); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java index 5b5135e95a..4923084de8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java @@ -1,22 +1,35 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow.noOpWorkflow; - -public interface ManagedWorkflowFactory { +public interface ManagedWorkflowFactory> { @SuppressWarnings({"rawtypes", "unchecked"}) ManagedWorkflowFactory DEFAULT = (configuration) -> { final var dependentResourceSpecs = configuration.getDependentResources(); if (dependentResourceSpecs == null || dependentResourceSpecs.isEmpty()) { - return noOpWorkflow; + return new ManagedWorkflow() { + @Override + public boolean hasCleaner() { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Workflow resolve(KubernetesClient client, ControllerConfiguration configuration) { + return new DefaultWorkflow(null); + } + }; } - return new DefaultManagedWorkflow(dependentResourceSpecs, - ManagedWorkflowSupport.instance().createWorkflow(dependentResourceSpecs)); + return ManagedWorkflowSupport.instance().createWorkflow(dependentResourceSpecs); }; @SuppressWarnings("rawtypes") - ManagedWorkflow workflowFor(ControllerConfiguration configuration); + ManagedWorkflow workflowFor(C configuration); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java index 24525357cf..430acfd784 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java @@ -46,30 +46,17 @@ public void checkForNameDuplication(List dependentResourc } } - public

Workflow

createWorkflow( + + public

ManagedWorkflow

createWorkflow( List dependentResourceSpecs) { - var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs); - final var alreadyCreated = new ArrayList(orderedResourceSpecs.size()); - final boolean[] cleanerHolder = {false}; - final var nodes = orderedResourceSpecs.stream() - .map(spec -> createFrom(spec, alreadyCreated, cleanerHolder)) - .collect(Collectors.toSet()); - return new Workflow<>(nodes, cleanerHolder[0]); + return createAsDefault(dependentResourceSpecs); } - private DependentResourceNode createFrom(DependentResourceSpec spec, - List alreadyCreated, boolean[] cleanerHolder) { - final var node = new SpecDependentResourceNode<>(spec); - alreadyCreated.add(node); - // if any previously checked dependent was a cleaner, no need to check further - cleanerHolder[0] = cleanerHolder[0] || Workflow.isDeletable(spec.getDependentResourceClass()); - spec.getDependsOn().forEach(depend -> { - final DependentResourceNode dependsOn = alreadyCreated.stream() - .filter(drn -> depend.equals(drn.getName())).findFirst() - .orElseThrow(); - node.addDependsOnRelation(dependsOn); - }); - return node; +

DefaultManagedWorkflow

createAsDefault( + List dependentResourceSpecs) { + final boolean[] cleanerHolder = {false}; + var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs, cleanerHolder); + return new DefaultManagedWorkflow<>(orderedResourceSpecs, cleanerHolder[0]); } /** @@ -77,17 +64,22 @@ private DependentResourceNode createFrom(DependentResourceSpec spec, * @return top-bottom ordered resources that can be added safely to workflow * @throws OperatorException if there is a cycle in the dependencies */ - public List orderAndDetectCycles( - List dependentResourceSpecs) { + private List> orderAndDetectCycles( + List dependentResourceSpecs, boolean[] cleanerHolder) { final var drInfosByName = createDRInfos(dependentResourceSpecs); - final var orderedSpecs = new ArrayList(dependentResourceSpecs.size()); + final var orderedSpecs = + new ArrayList>(dependentResourceSpecs.size()); final var alreadyVisited = new HashSet(); var toVisit = getTopDependentResources(dependentResourceSpecs); while (!toVisit.isEmpty()) { final var toVisitNext = new HashSet(); toVisit.forEach(dr -> { + if (cleanerHolder != null) { + cleanerHolder[0] = + cleanerHolder[0] || DefaultWorkflow.isDeletable(dr.getDependentResourceClass()); + } final var name = dr.getName(); var drInfo = drInfosByName.get(name); if (drInfo != null) { @@ -111,6 +103,16 @@ public List orderAndDetectCycles( return orderedSpecs; } + /** + * @param dependentResourceSpecs list of specs + * @return top-bottom ordered resources that can be added safely to workflow + * @throws OperatorException if there is a cycle in the dependencies + */ + public List> orderAndDetectCycles( + List dependentResourceSpecs) { + return orderAndDetectCycles(dependentResourceSpecs, null); + } + private static class DRInfo { private final DependentResourceSpec spec; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java index 393e3d2e2e..0067c55321 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java @@ -17,7 +17,7 @@ protected NodeExecutor(DependentResourceNode dependentResourceNode, @Override public void run() { try { - var dependentResource = workflowExecutor.getDependentResourceFor(dependentResourceNode); + var dependentResource = dependentResourceNode.getDependentResource(); doRun(dependentResourceNode, dependentResource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java deleted file mode 100644 index fc6e77db6d..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; - -class SpecDependentResourceNode - extends AbstractDependentResourceNode { - @SuppressWarnings("unchecked") - public SpecDependentResourceNode(DependentResourceSpec spec) { - super(spec.getName()); - setReadyPostcondition(spec.getReadyCondition()); - setDeletePostcondition(spec.getDeletePostCondition()); - setReconcilePrecondition(spec.getReconcileCondition()); - } - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void resolve(KubernetesClient client, ControllerConfiguration

configuration) { - final var spec = configuration.getDependentResources().stream() - .filter(drs -> drs.getName().equals(getName())) - .findFirst().orElseThrow(); - - final DependentResource dependentResource = spec.getDependentResource(); - - if (dependentResource instanceof KubernetesClientAware) { - ((KubernetesClientAware) dependentResource).setKubernetesClient(client); - } - - spec.getUseEventSourceWithName() - .ifPresent(esName -> { - final var name = (String) esName; - if (dependentResource instanceof EventSourceReferencer) { - ((EventSourceReferencer) dependentResource).useEventSourceWithName(name); - } else { - throw new IllegalStateException( - "DependentResource " + spec + " wants to use EventSource named " + name - + " but doesn't implement support for this feature by implementing " - + EventSourceReferencer.class.getSimpleName()); - } - }); - - setDependentResource(dependentResource); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java index 99e2fa94e1..c06f17b7d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java @@ -1,154 +1,45 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.function.Function; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -/** - * Dependents definition: so if B depends on A, the B is dependent of A. - * - * @param

primary resource - */ -@SuppressWarnings("rawtypes") -public class Workflow

{ +public interface Workflow

{ - public static final boolean THROW_EXCEPTION_AUTOMATICALLY_DEFAULT = true; + boolean THROW_EXCEPTION_AUTOMATICALLY_DEFAULT = true; - private final Map dependentResourceNodes; - private final Set topLevelResources = new HashSet<>(); - private final Set bottomLevelResource = new HashSet<>(); - - private final boolean throwExceptionAutomatically; - // it's "global" executor service shared between multiple reconciliations running parallel - private final ExecutorService executorService; - private boolean resolved; - private final boolean hasCleaner; - - Workflow(Set dependentResourceNodes, boolean hasCleaner) { - this(dependentResourceNodes, ExecutorServiceManager.instance().workflowExecutorService(), - THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false, hasCleaner); - } - - Workflow(Set dependentResourceNodes, - ExecutorService executorService, boolean throwExceptionAutomatically, boolean resolved, - boolean hasCleaner) { - this.executorService = executorService; - this.dependentResourceNodes = toMap(dependentResourceNodes); - this.throwExceptionAutomatically = throwExceptionAutomatically; - this.resolved = resolved; - this.hasCleaner = hasCleaner; - } - - private Map toMap( - Set dependentResourceNodes) { - final var nodes = new ArrayList<>(dependentResourceNodes); - bottomLevelResource.addAll(nodes); - return dependentResourceNodes.stream() - .peek(drn -> { - // add cycle detection? - if (drn.getDependsOn().isEmpty()) { - topLevelResources.add(drn); - } else { - for (DependentResourceNode dependsOn : (List) drn - .getDependsOn()) { - bottomLevelResource.remove(dependsOn); - } - } - }) - .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); + default WorkflowReconcileResult reconcile(P primary, Context

context) { + throw new UnsupportedOperationException("Implement this"); } - public DependentResource getDependentResourceFor(DependentResourceNode node) { - throwIfUnresolved(); - return dependentResource(node); + default WorkflowCleanupResult cleanup(P primary, Context

context) { + throw new UnsupportedOperationException("Implement this"); } - private DependentResource dependentResource(DependentResourceNode node) { - return ((AbstractDependentResourceNode) dependentResourceNodes.get(node.getName())) - .getDependentResource(); + @SuppressWarnings("rawtypes") + default Set getTopLevelDependentResources() { + return Collections.emptySet(); } - private void throwIfUnresolved() { - if (!resolved) { - throw new IllegalStateException( - "Should call resolved before trying to access DependentResources"); - } + @SuppressWarnings("rawtypes") + default Set getBottomLevelResource() { + return Collections.emptySet(); } - public WorkflowReconcileResult reconcile(P primary, Context

context) { - throwIfUnresolved(); - WorkflowReconcileExecutor

workflowReconcileExecutor = - new WorkflowReconcileExecutor<>(this, primary, context); - var result = workflowReconcileExecutor.reconcile(); - if (throwExceptionAutomatically) { - result.throwAggregateExceptionIfErrorsPresent(); - } - return result; + default boolean hasCleaner() { + return false; } - public WorkflowCleanupResult cleanup(P primary, Context

context) { - throwIfUnresolved(); - WorkflowCleanupExecutor

workflowCleanupExecutor = - new WorkflowCleanupExecutor<>(this, primary, context); - var result = workflowCleanupExecutor.cleanup(); - if (throwExceptionAutomatically) { - result.throwAggregateExceptionIfErrorsPresent(); - } - return result; - } - - Set getTopLevelDependentResources() { - return topLevelResources; - } - - Set getBottomLevelResource() { - return bottomLevelResource; - } - - ExecutorService getExecutorService() { - return executorService; - } - - Map nodes() { - return dependentResourceNodes; - } - - @SuppressWarnings("unchecked") - void resolve(KubernetesClient client, ControllerConfiguration

configuration) { - if (!resolved) { - dependentResourceNodes.values().forEach(drn -> drn.resolve(client, configuration)); - resolved = true; - } - } - - boolean hasCleaner() { - return hasCleaner; + default boolean isEmpty() { + return true; } - static boolean isDeletable(Class drClass) { - final var isDeleter = Deleter.class.isAssignableFrom(drClass); - if (!isDeleter) { - return false; - } - - if (KubernetesDependentResource.class.isAssignableFrom(drClass)) { - return !GarbageCollected.class.isAssignableFrom(drClass); - } - return true; + @SuppressWarnings("rawtypes") + default Map getDependentResourcesByName() { + return Collections.emptyMap(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java index 9f24ff3bd2..c9107a4266 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java @@ -5,11 +5,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; @@ -17,14 +14,14 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class WorkflowBuilder

{ - private final Map> dependentResourceNodes = + private final Map> dependentResourceNodes = new HashMap<>(); private boolean throwExceptionAutomatically = THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; - private DefaultDependentResourceNode currentNode; + private DependentResourceNode currentNode; private boolean isCleaner = false; public WorkflowBuilder

addDependentResource(DependentResource dependentResource) { - currentNode = new DefaultDependentResourceNode<>(dependentResource); + currentNode = new DependentResourceNode<>(dependentResource); isCleaner = dependentResource.isDeletable(); final var name = currentNode.getName(); dependentResourceNodes.put(name, currentNode); @@ -64,7 +61,7 @@ public WorkflowBuilder

withDeletePostcondition(Condition deletePostcondition) DependentResourceNode getNodeByDependentResource(DependentResource dependentResource) { // first check by name final var node = - dependentResourceNodes.get(DefaultDependentResourceNode.getNameFor(dependentResource)); + dependentResourceNodes.get(DependentResourceNode.getNameFor(dependentResource)); if (node != null) { return node; } else { @@ -75,26 +72,13 @@ DependentResourceNode getNodeByDependentResource(DependentResource depende } } - public boolean isThrowExceptionAutomatically() { - return throwExceptionAutomatically; - } - public WorkflowBuilder

withThrowExceptionFurther(boolean throwExceptionFurther) { this.throwExceptionAutomatically = throwExceptionFurther; return this; } public Workflow

build() { - return build(ExecutorServiceManager.instance().workflowExecutorService()); - } - - public Workflow

build(int parallelism) { - return build(Executors.newFixedThreadPool(parallelism)); - } - - public Workflow

build(ExecutorService executorService) { - // workflow has been built from dependent resources so it is already resolved - return new Workflow(new HashSet<>(dependentResourceNodes.values()), executorService, - throwExceptionAutomatically, true, isCleaner); + return new DefaultWorkflow(new HashSet<>(dependentResourceNodes.values()), + throwExceptionAutomatically, isCleaner); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java index 12656c2a8a..3a65d8b112 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -52,7 +53,7 @@ private synchronized void handleCleanup(DependentResourceNode dependentResourceN return; } - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new CleanupExecutor<>(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted for cleanup: {}", dependentResourceNode); @@ -116,10 +117,10 @@ private boolean hasErroredDependent(DependentResourceNode dependentResourceNode) private WorkflowCleanupResult createCleanupResult() { final var erroredDependents = getErroredDependents(); final var postConditionNotMet = postDeleteConditionNotMet.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()); final var deleteCalled = this.deleteCalled.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()); return new WorkflowCleanupResult(erroredDependents, postConditionNotMet, deleteCalled); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index 98abe41977..773bb0332f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -63,11 +64,11 @@ private synchronized void handleReconcile(DependentResourceNode depend } boolean reconcileConditionMet = isConditionMet(dependentResourceNode.getReconcilePrecondition(), - getDependentResourceFor(dependentResourceNode)); + dependentResourceNode.getDependentResource()); if (!reconcileConditionMet) { handleReconcileConditionNotMet(dependentResourceNode); } else { - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new NodeReconcileExecutor(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted to reconcile: {}", dependentResourceNode); @@ -85,7 +86,7 @@ private synchronized void handleDelete(DependentResourceNode dependentResourceNo return; } - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new NodeDeleteExecutor(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted to delete: {}", dependentResourceNode); @@ -214,10 +215,10 @@ private boolean hasErroredParent(DependentResourceNode dependentResourceNo private WorkflowReconcileResult createReconcileResult() { return new WorkflowReconcileResult( reconciled.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()), notReady.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()), getErroredDependents(), reconcileResults); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java index f9f75d6796..766ef6d824 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -6,8 +6,12 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +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.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; @@ -19,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -107,8 +112,8 @@ private KubernetesDependentResourceConfig extractFirstDependentKubernetesResourc private Object extractDependentKubernetesResourceConfig( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { - return configuration.getDependentResources().get(index).getDependentResourceConfiguration() - .orElseThrow(); + final var spec = configuration.getDependentResources().get(index); + return DependentResourceConfigurationResolver.configurationFor(spec, configuration); } private io.javaoperatorsdk.operator.api.config.ControllerConfiguration createConfiguration( @@ -297,11 +302,12 @@ void replaceNamedDependentResourceConfigShouldWork() { .filter(dr -> dr.getName().equals(dependentResourceName)) .findFirst().orElseThrow(); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - var maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + var maybeConfig = + DependentResourceConfigurationResolver.configurationFor(dependentSpec, configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); - var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + var config = (KubernetesDependentResourceConfig) maybeConfig; // check that the DependentResource inherits the controller's configuration if applicable assertEquals(1, config.namespaces().size()); assertNull(config.labelSelector()); @@ -318,8 +324,8 @@ void replaceNamedDependentResourceConfigShouldWork() { dependents = overridden.getDependentResources(); dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName)) .findFirst().orElseThrow(); - config = (KubernetesDependentResourceConfig) dependentSpec.getDependentResourceConfiguration() - .orElseThrow(); + config = (KubernetesDependentResourceConfig) DependentResourceConfigurationResolver + .configurationFor(dependentSpec, overridden); assertEquals(1, config.namespaces().size()); assertEquals(labelSelector, config.labelSelector()); assertEquals(Set.of(overriddenNS), config.namespaces()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 11200f55d9..ec1223377c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -9,10 +9,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -96,10 +94,6 @@ void getsFirstTypeArgumentFromInterface() { assertThatIllegalArgumentException().isThrownBy( () -> Utils.getFirstTypeArgumentFromInterface(TestKubernetesDependentResource.class, DependentResource.class)); - - assertThat(Utils.getTypeArgumentFromInterfaceByIndex(TestKubernetesDependentResource.class, - AnnotationDependentResourceConfigurator.class, 1)) - .isEqualTo(KubernetesDependentResourceConfig.class); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java new file mode 100644 index 0000000000..983215bcb9 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java @@ -0,0 +1,209 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +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.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentConverter; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +import static io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolverTest.CustomAnnotationReconciler.DR_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DependentResourceConfigurationResolverTest { + + @Test + void controllerConfigurationProvidedShouldBeReturnedIfAvailable() { + final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + final var customConfig = DependentResourceConfigurationResolver + .extractConfigurationFromConfigured(CustomAnnotatedDep.class, cfg); + assertTrue(customConfig instanceof CustomConfig); + assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, ((CustomConfig) customConfig).getValue()); + final var newConfig = new CustomConfig(72); + final var overridden = ControllerConfigurationOverrider.override(cfg) + .replacingNamedDependentResourceConfig(DR_NAME, newConfig) + .build(); + final var spec = cfg.getDependentResources().stream() + .filter(s -> DR_NAME.equals(s.getName())) + .findFirst() + .orElseThrow(); + assertEquals(newConfig, + DependentResourceConfigurationResolver.configurationFor(spec, overridden)); + } + + @Test + void getConverterShouldWork() { + final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + var converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class); + assertNull(converter); + assertNull(DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class)); + + // extracting configuration should trigger converter creation + DependentResourceConfigurationResolver.extractConfigurationFromConfigured( + CustomAnnotatedDep.class, cfg); + converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class); + assertNotNull(converter); + assertEquals(CustomConfigConverter.class, converter.getClass()); + + converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class); + assertNull(converter); + DependentResourceConfigurationResolver.extractConfigurationFromConfigured( + ChildCustomAnnotatedDep.class, cfg); + converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class); + assertNotNull(converter); + assertEquals(CustomConfigConverter.class, converter.getClass()); + assertEquals(DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class), + converter); + } + + @SuppressWarnings("rawtypes") + @Test + void registerConverterShouldWork() { + final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + var converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertNull(converter); + DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ConfigMapDep.class, + cfg); + converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertTrue(converter instanceof KubernetesDependentConverter); + final var overriddenConverter = new ConfigurationConverter() { + @Override + public Object configFrom(Annotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + return null; + } + }; + DependentResourceConfigurationResolver.registerConverter(KubernetesDependentResource.class, + overriddenConverter); + + // already resolved converters are kept unchanged + converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertTrue(converter instanceof KubernetesDependentConverter); + + // but new converters should use the overridden version + DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ServiceDep.class, + cfg); + converter = DependentResourceConfigurationResolver.getConverter(ServiceDep.class); + assertEquals(overriddenConverter, converter); + } + + @ControllerConfiguration(dependents = { + @Dependent(type = CustomAnnotatedDep.class, name = DR_NAME), + @Dependent(type = ChildCustomAnnotatedDep.class), + @Dependent(type = ConfigMapDep.class), + @Dependent(type = ServiceDep.class) + }) + static class CustomAnnotationReconciler implements Reconciler { + + public static final String DR_NAME = "first"; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + + private static class ConfigMapDep extends KubernetesDependentResource { + + public ConfigMapDep() { + super(ConfigMap.class); + } + } + + private static class ServiceDep extends KubernetesDependentResource { + + public ServiceDep() { + super(Service.class); + } + } + + @CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE) + @Configured(by = CustomAnnotation.class, with = CustomConfig.class, + converter = CustomConfigConverter.class) + private static class CustomAnnotatedDep implements DependentResource, + DependentResourceConfigurator { + + public static final int PROVIDED_VALUE = 42; + private CustomConfig config; + + @Override + public ReconcileResult reconcile(ConfigMap primary, Context context) { + return null; + } + + @Override + public Class resourceType() { + return ConfigMap.class; + } + + @Override + public void configureWith(CustomConfig config) { + this.config = config; + } + + @Override + public Optional configuration() { + return Optional.ofNullable(config); + } + } + + private static class ChildCustomAnnotatedDep extends CustomAnnotatedDep { + + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface CustomAnnotation { + + int value(); + } + + private static class CustomConfig { + + private final int value; + + private CustomConfig(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private static class CustomConfigConverter + implements ConfigurationConverter { + + static final int CONVERTER_PROVIDED_DEFAULT = 7; + + @Override + public CustomConfig configFrom(CustomAnnotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation == null) { + return new CustomConfig(CONVERTER_PROVIDED_DEFAULT); + } else { + return new CustomConfig(configAnnotation.value()); + } + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java index 3d5bde29db..0f6fea2449 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java @@ -138,16 +138,12 @@ void createsWorkflow() { createDRS(NAME_3, NAME_1), createDRS(NAME_4, NAME_3, NAME_2)); - var workflow = managedWorkflowSupport.createWorkflow(specs); + var workflow = managedWorkflowSupport.createAsDefault(specs); - assertThat(workflow.nodes().values()).map(DependentResourceNode::getName) + assertThat(workflow.nodeNames()) .containsExactlyInAnyOrder(NAME_1, NAME_2, NAME_3, NAME_4); - assertThat(workflow.getTopLevelDependentResources()) - .map(DependentResourceNode::getName) - .containsExactly(NAME_1); - assertThat(workflow.getBottomLevelResource()) - .map(DependentResourceNode::getName) - .containsExactly(NAME_4); + assertThat(workflow.getTopLevelResources()).containsExactly(NAME_1); + assertThat(workflow.getBottomLevelResources()).containsExactly(NAME_4); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java index d8771af5e5..68e3068de2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java @@ -23,19 +23,19 @@ class ManagedWorkflowTest { @Test void checksIfWorkflowEmpty() { - assertThat(managedWorkflow().isEmptyWorkflow()).isTrue(); - assertThat(managedWorkflow(createDRS(NAME)).isEmptyWorkflow()).isFalse(); + assertThat(managedWorkflow().isEmpty()).isTrue(); + assertThat(managedWorkflow(createDRS(NAME)).isEmpty()).isFalse(); } @Test void isNotCleanerIfNoDeleter() { - assertThat(managedWorkflow(createDRS(NAME)).isCleaner()).isFalse(); + assertThat(managedWorkflow(createDRS(NAME)).hasCleaner()).isFalse(); } @Test void isNotCleanerIfGarbageCollected() { assertThat(managedWorkflow(createDRSWithTraits(NAME, GarbageCollected.class)) - .isCleaner()).isFalse(); + .hasCleaner()).isFalse(); } @Test @@ -43,18 +43,18 @@ void isCleanerShouldWork() { assertThat(managedWorkflow( createDRSWithTraits(NAME, GarbageCollected.class), createDRSWithTraits("foo", Deleter.class)) - .isCleaner()).isTrue(); + .hasCleaner()).isTrue(); assertThat(managedWorkflow( createDRSWithTraits("foo", Deleter.class), createDRSWithTraits(NAME, GarbageCollected.class)) - .isCleaner()).isTrue(); + .hasCleaner()).isTrue(); } @Test void isCleanerIfHasDeleter() { var spec = createDRSWithTraits(NAME, Deleter.class); - assertThat(managedWorkflow(spec).isCleaner()).isTrue(); + assertThat(managedWorkflow(spec).hasCleaner()).isTrue(); } ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java index 9b97963366..25c0ad139b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -20,7 +20,7 @@ public class ManagedWorkflowTestUtils { @SuppressWarnings("unchecked") public static DependentResourceSpec createDRS(String name, String... dependOns) { - return new DependentResourceSpec(new EmptyTestDependentResource(), name, Set.of(dependOns), + return new DependentResourceSpec(EmptyTestDependentResource.class, name, Set.of(dependOns), null, null, null, null); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java index 382022782e..4e6cc5c329 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java @@ -34,7 +34,7 @@ void calculatesTopLevelResources() { Set topResources = workflow.getTopLevelDependentResources().stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toSet()); assertThat(topResources).containsExactlyInAnyOrder(dr1, independentDR); @@ -54,7 +54,7 @@ void calculatesBottomLevelResources() { Set bottomResources = workflow.getBottomLevelResource().stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toSet()); assertThat(bottomResources).containsExactlyInAnyOrder(dr2, independentDR); @@ -64,19 +64,19 @@ void calculatesBottomLevelResources() { @Test void isDeletableShouldWork() { var dr = mock(DependentResource.class); - assertFalse(Workflow.isDeletable(dr.getClass())); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); dr = mock(DependentResource.class, withSettings().extraInterfaces(Deleter.class)); - assertTrue(Workflow.isDeletable(dr.getClass())); + assertTrue(DefaultWorkflow.isDeletable(dr.getClass())); dr = mock(KubernetesDependentResource.class); - assertFalse(Workflow.isDeletable(dr.getClass())); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class)); - assertTrue(Workflow.isDeletable(dr.getClass())); + assertTrue(DefaultWorkflow.isDeletable(dr.getClass())); dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class, GarbageCollected.class)); - assertFalse(Workflow.isDeletable(dr.getClass())); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java index daf1964d13..bbc49ebf33 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -16,6 +16,9 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -24,6 +27,8 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -58,9 +63,9 @@ void defaultValuesShouldBeConsistent() { @SuppressWarnings("rawtypes") private KubernetesDependentResourceConfig extractDependentKubernetesResourceConfig( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { - return (KubernetesDependentResourceConfig) configuration.getDependentResources().get(index) - .getDependentResourceConfiguration() - .orElseThrow(); + final var spec = configuration.getDependentResources().get(index); + return (KubernetesDependentResourceConfig) DependentResourceConfigurationResolver + .configurationFor(spec, configuration); } @Test @@ -78,10 +83,11 @@ void getDependentResources() { assertTrue(dependents.stream().anyMatch(d -> d.getName().equals(dependentResourceName))); var dependentSpec = findByName(dependents, dependentResourceName); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - var maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); - final var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + var maybeConfig = + DependentResourceConfigurationResolver.configurationFor(dependentSpec, configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); + final var config = (KubernetesDependentResourceConfig) maybeConfig; // check that the DependentResource inherits the controller's configuration if applicable assertEquals(1, config.namespaces().size()); assertEquals(Set.of(OneDepReconciler.CONFIGURED_NS), config.namespaces()); @@ -92,9 +98,10 @@ void getDependentResources() { assertEquals(1, dependents.size()); dependentSpec = findByName(dependents, NamedDepReconciler.NAME); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + maybeConfig = DependentResourceConfigurationResolver.configurationFor(dependentSpec, + configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); } @Test @@ -183,6 +190,20 @@ void controllerConfigurationOnSuperClassShouldWork() { assertNotNull(config.getName()); } + @Test + void configuringFromCustomAnnotationsShouldWork() { + var config = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, getValue(config, 0)); + assertEquals(CustomConfigConverter.CONVERTER_PROVIDED_DEFAULT, getValue(config, 1)); + } + + private static int getValue( + io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { + return ((CustomConfig) DependentResourceConfigurationResolver + .configurationFor(configuration.getDependentResources().get(index), configuration)) + .getValue(); + } + @ControllerConfiguration( maxReconciliationInterval = @MaxReconciliationInterval(interval = 50, timeUnit = TimeUnit.SECONDS)) @@ -353,4 +374,84 @@ public UpdateControl reconcile(ConfigMap resource, Context return null; } } + + @ControllerConfiguration(dependents = { + @Dependent(type = CustomAnnotatedDep.class), + @Dependent(type = ChildCustomAnnotatedDep.class) + }) + private static class CustomAnnotationReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + + @CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE) + @Configured(by = CustomAnnotation.class, with = CustomConfig.class, + converter = CustomConfigConverter.class) + private static class CustomAnnotatedDep implements DependentResource, + DependentResourceConfigurator { + + public static final int PROVIDED_VALUE = 42; + private CustomConfig config; + + @Override + public ReconcileResult reconcile(ConfigMap primary, Context context) { + return null; + } + + @Override + public Class resourceType() { + return ConfigMap.class; + } + + @Override + public void configureWith(CustomConfig config) { + this.config = config; + } + + @Override + public Optional configuration() { + return Optional.ofNullable(config); + } + } + + private static class ChildCustomAnnotatedDep extends CustomAnnotatedDep { + + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface CustomAnnotation { + int value(); + } + + private static class CustomConfig { + private final int value; + + private CustomConfig(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private static class CustomConfigConverter + implements ConfigurationConverter { + static final int CONVERTER_PROVIDED_DEFAULT = 7; + + @Override + public CustomConfig configFrom(CustomAnnotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation == null) { + return new CustomConfig(CONVERTER_PROVIDED_DEFAULT); + } else { + return new CustomConfig(configAnnotation.value()); + } + } + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java index a572572a97..ebc1655c38 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java @@ -5,21 +5,24 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +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.Dependent; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @ControllerConfiguration( - dependents = @Dependent( - type = BulkDependentResourceExternalWithState.class)) + dependents = @Dependent(type = BulkDependentResourceExternalWithState.class)) public class ExternalStateBulkDependentReconciler implements Reconciler, EventSourceInitializer, TestExecutionInfoProvider { - public static final String ID_KEY = "id"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index 8fb8855e41..d7ee5d368f 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -13,13 +13,16 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.MySQLDbConfig; import io.javaoperatorsdk.operator.sample.MySQLSchema; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource.ResourcePollerConfigConverter; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -30,9 +33,11 @@ @SchemaConfig(pollPeriod = 700, host = "127.0.0.1", port = SchemaDependentResource.LOCAL_PORT, user = "root", password = "password") // NOSONAR: password is only used locally, example only +@Configured(by = SchemaConfig.class, with = ResourcePollerConfig.class, + converter = ResourcePollerConfigConverter.class) public class SchemaDependentResource extends PerResourcePollingDependentResource - implements AnnotationDependentResourceConfigurator, + implements DependentResourceConfigurator, Creator, Deleter { public static final String NAME = "schema"; @@ -56,18 +61,6 @@ public void configureWith(ResourcePollerConfig config) { setPollingPeriod(config.getPollPeriod()); } - @Override - public ResourcePollerConfig configFrom(SchemaConfig annotation, - ControllerConfiguration parentConfiguration) { - if (annotation != null) { - return new ResourcePollerConfig(annotation.pollPeriod(), - new MySQLDbConfig(annotation.host(), "" + annotation.port(), - annotation.user(), annotation.password())); - } - return new ResourcePollerConfig(SchemaConfig.DEFAULT_POLL_PERIOD, - MySQLDbConfig.loadFromEnvironmentVars()); - } - @Override public Schema desired(MySQLSchema primary, Context context) { return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); @@ -120,4 +113,21 @@ public Set fetchResources(MySQLSchema primaryResource) { throw new RuntimeException("Error while trying read Schema", e); } } + + static class ResourcePollerConfigConverter implements + ConfigurationConverter { + + @Override + public ResourcePollerConfig configFrom(SchemaConfig configAnnotation, + ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation != null) { + return new ResourcePollerConfig(configAnnotation.pollPeriod(), + new MySQLDbConfig(configAnnotation.host(), "" + configAnnotation.port(), + configAnnotation.user(), configAnnotation.password())); + } + return new ResourcePollerConfig(SchemaConfig.DEFAULT_POLL_PERIOD, + MySQLDbConfig.loadFromEnvironmentVars()); + } + } } diff --git a/sample-operators/mysql-schema/src/main/resources/log4j2.xml b/sample-operators/mysql-schema/src/main/resources/log4j2.xml index 5ab4735126..01484221f9 100644 --- a/sample-operators/mysql-schema/src/main/resources/log4j2.xml +++ b/sample-operators/mysql-schema/src/main/resources/log4j2.xml @@ -6,7 +6,7 @@ - + diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 24e257956b..e6aa796656 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -53,7 +53,7 @@ class MySQLSchemaOperatorE2E { boolean isLocal() { String deployment = System.getProperty("test.deployment"); boolean remote = (deployment != null && deployment.equals("remote")); - log.info("Running the operator " + (remote ? "remote" : "locally")); + log.info("Running the operator " + (remote ? "remotely" : "locally")); return !remote; } From 5d5dc001385e8f82e37dd49a0c2aee413612e8f4 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 9 Dec 2022 16:38:13 +0100 Subject: [PATCH 11/14] refactor: simplify controller configuration (#1635) * refactor: move configuration processing to BaseConfigurationService Replaced all the different controller configuration implementations by only one: ResolvedControllerConfiguration. --- .../AnnotationControllerConfiguration.java | 259 ------------------ .../api/config/BaseConfigurationService.java | 202 +++++++++++++- .../api/config/ControllerConfiguration.java | 32 ++- .../ControllerConfigurationOverrider.java | 25 +- .../DefaultControllerConfiguration.java | 156 ----------- .../config/DefaultResourceConfiguration.java | 35 +-- .../ResolvedControllerConfiguration.java | 169 ++++++++++++ .../api/config/ResourceConfiguration.java | 14 + .../operator/api/config/Utils.java | 9 +- .../informer/InformerConfiguration.java | 10 +- .../operator/ControllerManagerTest.java | 23 +- .../ControllerConfigurationOverriderTest.java | 3 +- ...dentResourceConfigurationResolverTest.java | 27 +- .../event/source/ResourceEventFilterTest.java | 20 +- .../ControllerResourceEventSourceTest.java | 27 +- .../AnnotationControllerConfiguration.java | 17 -- .../runtime/DefaultConfigurationService.java | 5 +- ...java => BaseConfigurationServiceTest.java} | 51 ++-- 18 files changed, 541 insertions(+), 543 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java delete mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/config/{runtime/AnnotationControllerConfigurationTest.java => BaseConfigurationServiceTest.java} (90%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java deleted file mode 100644 index 4e4f945515..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ /dev/null @@ -1,259 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.lang.annotation.Annotation; -import java.time.Duration; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import io.javaoperatorsdk.operator.processing.retry.Retry; - -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; - -@SuppressWarnings("rawtypes") -public class AnnotationControllerConfiguration

- implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration

{ - - protected final Reconciler

reconciler; - private final ControllerConfiguration annotation; - private List specs; - private Class

resourceClass; - - public AnnotationControllerConfiguration(Reconciler

reconciler) { - this.reconciler = reconciler; - this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); - if (annotation == null) { - throw new OperatorException( - "Missing mandatory @" + ControllerConfiguration.class.getSimpleName() + - " annotation for reconciler: " + reconciler); - } - } - - @Override - public String getName() { - return ReconcilerUtils.getNameFor(reconciler); - } - - @Override - public String getFinalizerName() { - if (annotation == null || annotation.finalizerName().isBlank()) { - return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); - } else { - final var finalizer = annotation.finalizerName(); - if (ReconcilerUtils.isFinalizerValid(finalizer)) { - return finalizer; - } else { - throw new IllegalArgumentException( - finalizer - + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); - } - } - } - - @Override - public boolean isGenerationAware() { - return valueOrDefault( - annotation, ControllerConfiguration::generationAwareEventProcessing, true); - } - - @Override - public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, - DEFAULT_NAMESPACES_SET.toArray(String[]::new))); - } - - @Override - @SuppressWarnings("unchecked") - public Optional> cachePruneFunction() { - return Optional.ofNullable( - Utils.instantiate(annotation.cachePruneFunction(), UnaryOperator.class, - Utils.contextFor(this, null, null))); - } - - @Override - @SuppressWarnings("unchecked") - public Class

getResourceClass() { - if (resourceClass == null) { - resourceClass = - (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface(reconciler.getClass(), - Reconciler.class); - } - return resourceClass; - } - - @Override - public String getLabelSelector() { - return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); - } - - @Override - public String getAssociatedReconcilerClassName() { - return reconciler.getClass().getCanonicalName(); - } - - @SuppressWarnings("unchecked") - @Override - public ResourceEventFilter

getEventFilter() { - ResourceEventFilter

answer = null; - - Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, new Object[] {}); - for (var filterType : filterTypes) { - try { - ResourceEventFilter

filter = filterType.getConstructor().newInstance(); - - if (answer == null) { - answer = filter; - } else { - answer = answer.and(filter); - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - } - return answer != null ? answer : ResourceEventFilters.passthrough(); - } - - @Override - public Optional maxReconciliationInterval() { - final var newConfig = annotation.maxReconciliationInterval(); - if (newConfig != null && newConfig.interval() > 0) { - return Optional.of(Duration.of(newConfig.interval(), newConfig.timeUnit().toChronoUnit())); - } - return Optional.empty(); - } - - @Override - public RateLimiter getRateLimiter() { - final Class rateLimiterClass = annotation.rateLimiter(); - return Utils.instantiateAndConfigureIfNeeded(rateLimiterClass, RateLimiter.class, - Utils.contextFor(this, null, null), this::configureFromAnnotatedReconciler); - } - - @Override - public Retry getRetry() { - final Class retryClass = annotation.retry(); - return Utils.instantiateAndConfigureIfNeeded(retryClass, Retry.class, - Utils.contextFor(this, null, null), this::configureFromAnnotatedReconciler); - } - - - @SuppressWarnings("unchecked") - private void configureFromAnnotatedReconciler(Object instance) { - if (instance instanceof AnnotationConfigurable) { - AnnotationConfigurable configurable = (AnnotationConfigurable) instance; - final Class configurationClass = - (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface( - instance.getClass(), AnnotationConfigurable.class); - final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass); - if (configAnnotation != null) { - configurable.initFrom(configAnnotation); - } - } - } - - @Override - @SuppressWarnings("unchecked") - public Optional> onAddFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.onAddFilter(), OnAddFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> onUpdateFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.onUpdateFilter(), OnUpdateFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> genericFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.genericFilter(), GenericFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public List getDependentResources() { - if (specs == null) { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - specs = Collections.emptyList(); - return specs; - } - - final var specsMap = new LinkedHashMap(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - - final var name = getName(dependent, dependentType); - var spec = specsMap.get(name); - if (spec != null) { - throw new IllegalArgumentException( - "A DependentResource named '" + name + "' already exists: " + spec); - } - - var eventSourceName = dependent.useEventSourceWithName(); - eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; - final var context = Utils.contextFor(this, dependentType, null); - spec = new DependentResourceSpec(dependentType, name, - Set.of(dependent.dependsOn()), - Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), - Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), - Utils.instantiate(dependent.deletePostcondition(), Condition.class, context), - eventSourceName); - specsMap.put(name, spec); - } - - specs = specsMap.values().stream().collect(Collectors.toUnmodifiableList()); - } - return specs; - } - - private String getName(Dependent dependent, Class dependentType) { - var name = dependent.name(); - if (name.isBlank()) { - name = DependentResource.defaultNameFor(dependentType); - } - return name; - } - - public static T valueOrDefault( - ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); - } - } - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 1e4b535202..db91ee22af 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -1,13 +1,40 @@ package io.javaoperatorsdk.operator.api.config; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.Utils.Configurator; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.Retry; import com.fasterxml.jackson.databind.ObjectMapper; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; + public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -67,8 +94,140 @@ public ControllerConfiguration getConfigurationFor( return config; } - protected ControllerConfiguration configFor(Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + @SuppressWarnings({"unchecked", "rawtypes"}) + protected

ControllerConfiguration

configFor(Reconciler

reconciler) { + final var annotation = reconciler.getClass().getAnnotation( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class); + if (annotation == null) { + throw new OperatorException( + "Missing mandatory @" + + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class + .getSimpleName() + + + " annotation for reconciler: " + reconciler); + } + + final var resourceClass = (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface( + reconciler.getClass(), Reconciler.class); + final var name = ReconcilerUtils.getNameFor(reconciler); + final var generationAware = valueOrDefault( + annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::generationAwareEventProcessing, + true); + final var associatedReconcilerClass = + ResolvedControllerConfiguration.getAssociatedReconcilerClassName(reconciler.getClass()); + + final Class retryClass = annotation.retry(); + final var retry = Utils.instantiateAndConfigureIfNeeded(retryClass, Retry.class, + Utils.contextFor(name, null, null), configuratorFor(Retry.class, reconciler)); + + final Class rateLimiterClass = annotation.rateLimiter(); + final var rateLimiter = Utils.instantiateAndConfigureIfNeeded(rateLimiterClass, + RateLimiter.class, + Utils.contextFor(name, null, null), configuratorFor(RateLimiter.class, reconciler)); + + final var reconciliationInterval = annotation.maxReconciliationInterval(); + long interval = -1; + TimeUnit timeUnit = null; + if (reconciliationInterval != null && reconciliationInterval.interval() > 0) { + interval = reconciliationInterval.interval(); + timeUnit = reconciliationInterval.timeUnit(); + } + + final var config = new ResolvedControllerConfiguration

( + resourceClass, name, generationAware, + associatedReconcilerClass, retry, rateLimiter, + ResolvedControllerConfiguration.getMaxReconciliationInterval(interval, timeUnit), + Utils.instantiate(annotation.onAddFilter(), OnAddFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.onUpdateFilter(), OnUpdateFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.genericFilter(), GenericFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.cachePruneFunction(), UnaryOperator.class, + Utils.contextFor(name, null, null)), + Set.of(valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::namespaces, + DEFAULT_NAMESPACES_SET.toArray(String[]::new))), + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::finalizerName, + Constants.NO_VALUE_SET), + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::labelSelector, + Constants.NO_VALUE_SET), + null); + + ResourceEventFilter

answer = deprecatedEventFilter(annotation); + config.setEventFilter(answer != null ? answer : ResourceEventFilters.passthrough()); + + List specs = dependentResources(annotation, config); + config.setDependentResources(specs); + + return config; + } + + @SuppressWarnings("unchecked") + private static

ResourceEventFilter

deprecatedEventFilter( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation) { + ResourceEventFilter

answer = null; + + Class>[] filterTypes = + (Class>[]) valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::eventFilters, + new Object[] {}); + for (var filterType : filterTypes) { + try { + ResourceEventFilter

filter = filterType.getConstructor().newInstance(); + + if (answer == null) { + answer = filter; + } else { + answer = answer.and(filter); + } + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + return answer; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static List dependentResources( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation, + ControllerConfiguration parent) { + final var dependents = + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::dependents, + new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + final var specsMap = new LinkedHashMap(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + + final var dependentName = getName(dependent.name(), dependentType); + var spec = specsMap.get(dependentName); + if (spec != null) { + throw new IllegalArgumentException( + "A DependentResource named '" + dependentName + "' already exists: " + spec); + } + + final var name = parent.getName(); + + var eventSourceName = dependent.useEventSourceWithName(); + eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; + final var context = Utils.contextFor(name, dependentType, null); + spec = new DependentResourceSpec(dependentType, dependentName, + Set.of(dependent.dependsOn()), + Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), + Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), + Utils.instantiate(dependent.deletePostcondition(), Condition.class, context), + eventSourceName); + specsMap.put(dependentName, spec); + } + return specsMap.values().stream().collect(Collectors.toUnmodifiableList()); } protected boolean createIfNeeded() { @@ -79,4 +238,43 @@ protected boolean createIfNeeded() { public boolean checkCRDAndValidateLocalModel() { return Utils.shouldCheckCRDAndValidateLocalModel(); } + + private static T valueOrDefault( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration controllerConfiguration, + Function mapper, + T defaultValue) { + if (controllerConfiguration == null) { + return defaultValue; + } else { + return mapper.apply(controllerConfiguration); + } + } + + @SuppressWarnings("rawtypes") + private static String getName(String name, Class dependentType) { + if (name.isBlank()) { + name = DependentResource.defaultNameFor(dependentType); + } + return name; + } + + @SuppressWarnings("unused") + private static Configurator configuratorFor(Class instanceType, + Reconciler reconciler) { + return instance -> configureFromAnnotatedReconciler(instance, reconciler); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void configureFromAnnotatedReconciler(Object instance, Reconciler reconciler) { + if (instance instanceof AnnotationConfigurable) { + AnnotationConfigurable configurable = (AnnotationConfigurable) instance; + final Class configurationClass = + (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface( + instance.getClass(), AnnotationConfigurable.class); + final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass); + if (configAnnotation != null) { + configurable.initFrom(configAnnotation); + } + } + } } 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 74c40ed9a8..b2640777a7 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 @@ -17,19 +17,37 @@ import io.javaoperatorsdk.operator.processing.retry.GradualRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; -public interface ControllerConfiguration extends ResourceConfiguration { +public interface ControllerConfiguration

extends ResourceConfiguration

{ @SuppressWarnings("rawtypes") RateLimiter DEFAULT_RATE_LIMITER = LinearRateLimiter.deactivatedRateLimiter(); default String getName() { - return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); + return ensureValidName(null, getAssociatedReconcilerClassName()); } default String getFinalizerName() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } + static String ensureValidName(String name, String reconcilerClassName) { + return name != null ? name : ReconcilerUtils.getDefaultReconcilerName(reconcilerClassName); + } + + static String ensureValidFinalizerName(String finalizer, String resourceTypeName) { + if (finalizer != null && !finalizer.isBlank()) { + if (ReconcilerUtils.isFinalizerValid(finalizer)) { + return finalizer; + } else { + throw new IllegalArgumentException( + finalizer + + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); + } + } else { + return ReconcilerUtils.getDefaultFinalizerName(resourceTypeName); + } + } + default boolean isGenerationAware() { return true; } @@ -70,8 +88,12 @@ default RateLimiter getRateLimiter() { *

* * @return filter + * @deprecated use {@link ResourceConfiguration#onAddFilter()}, + * {@link ResourceConfiguration#onUpdateFilter()} or + * {@link ResourceConfiguration#genericFilter()} instead */ - default ResourceEventFilter getEventFilter() { + @Deprecated(forRemoval = true) + default ResourceEventFilter

getEventFilter() { return ResourceEventFilters.passthrough(); } @@ -91,8 +113,8 @@ default ConfigurationService getConfigurationService() { @SuppressWarnings("unchecked") @Override - default Class getResourceClass() { - return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), + default Class

getResourceClass() { + return (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), ControllerConfiguration.class); } } 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 31de28631d..789de127f5 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 @@ -179,25 +179,14 @@ public ControllerConfigurationOverrider replacingNamedDependentResourceConfig } public ControllerConfiguration build() { - return new DefaultControllerConfiguration<>( - original.getAssociatedReconcilerClassName(), - original.getName(), - original.getResourceTypeName(), - finalizer, - generationAware, - namespaces, - retry, - labelSelector, - customResourcePredicate, - original.getResourceClass(), - reconciliationMaxInterval, - onAddFilter, - onUpdateFilter, - genericFilter, - rateLimiter, + final var overridden = new ResolvedControllerConfiguration<>( + original.getResourceClass(), original.getName(), + generationAware, original.getAssociatedReconcilerClassName(), retry, rateLimiter, + reconciliationMaxInterval, onAddFilter, onUpdateFilter, genericFilter, cachePruneFunction, original.getDependentResources(), - cachePruneFunction, - configurations); + namespaces, finalizer, labelSelector, configurations); + overridden.setEventFilter(customResourcePredicate); + return overridden; } public static ControllerConfigurationOverrider override( 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 deleted file mode 100644 index 6dd99ecbe2..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ /dev/null @@ -1,156 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.UnaryOperator; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationProvider; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; -import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import io.javaoperatorsdk.operator.processing.retry.Retry; - -@SuppressWarnings("rawtypes") -public class DefaultControllerConfiguration - extends DefaultResourceConfiguration - implements ControllerConfiguration, DependentResourceConfigurationProvider { - - private final String associatedControllerClassName; - private final String name; - private final String crdName; - private final String finalizer; - private final boolean generationAware; - private final Retry retry; - private final ResourceEventFilter resourceEventFilter; - private final List dependents; - private final Duration reconciliationMaxInterval; - private final RateLimiter rateLimiter; - private final Map configurations; - - // NOSONAR constructor is meant to provide all information - public DefaultControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - Retry retry, - String labelSelector, - ResourceEventFilter resourceEventFilter, - Class resourceClass, - Duration reconciliationMaxInterval, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, - GenericFilter genericFilter, - RateLimiter rateLimiter, - List dependents, - UnaryOperator cachePruneFunction) { - this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retry, labelSelector, resourceEventFilter, resourceClass, reconciliationMaxInterval, - onAddFilter, onUpdateFilter, genericFilter, rateLimiter, dependents, cachePruneFunction, - null); - } - - DefaultControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - Retry retry, - String labelSelector, - ResourceEventFilter resourceEventFilter, - Class resourceClass, - Duration reconciliationMaxInterval, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, - GenericFilter genericFilter, - RateLimiter rateLimiter, - List dependents, - UnaryOperator cachePruneFunction, - Map configurations) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces, - cachePruneFunction); - this.associatedControllerClassName = associatedControllerClassName; - this.name = name; - this.crdName = crdName; - this.finalizer = finalizer; - this.generationAware = generationAware; - this.reconciliationMaxInterval = reconciliationMaxInterval; - this.retry = - retry == null - ? ControllerConfiguration.super.getRetry() - : retry; - this.resourceEventFilter = resourceEventFilter; - this.rateLimiter = - rateLimiter != null ? rateLimiter : LinearRateLimiter.deactivatedRateLimiter(); - this.dependents = dependents != null ? dependents : Collections.emptyList(); - this.configurations = configurations != null ? configurations : Collections.emptyMap(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getResourceTypeName() { - return crdName; - } - - @Override - public String getFinalizerName() { - return finalizer; - } - - @Override - public boolean isGenerationAware() { - return generationAware; - } - - @Override - public String getAssociatedReconcilerClassName() { - return associatedControllerClassName; - } - - @Override - public Retry getRetry() { - return retry; - } - - @Override - public ResourceEventFilter getEventFilter() { - return resourceEventFilter; - } - - @Override - public List getDependentResources() { - return dependents; - } - - @Override - public Optional maxReconciliationInterval() { - return Optional.ofNullable(reconciliationMaxInterval); - } - - @Override - public RateLimiter getRateLimiter() { - return rateLimiter; - } - - @Override - public Object getConfigurationFor(DependentResourceSpec spec) { - return configurations.get(spec); - } -} 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 index 8e42284a25..bb9f365d58 100644 --- 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 @@ -5,52 +5,41 @@ import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; - public class DefaultResourceConfiguration implements ResourceConfiguration { - private final String labelSelector; - private final Set namespaces; private final Class resourceClass; + private final String resourceTypeName; private final OnAddFilter onAddFilter; private final OnUpdateFilter onUpdateFilter; private final GenericFilter genericFilter; + private final String labelSelector; + private final Set namespaces; private final UnaryOperator cachePruneFunction; - public DefaultResourceConfiguration(String labelSelector, Class resourceClass, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, String... namespaces) { - this(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, - namespaces == null || namespaces.length == 0 ? DEFAULT_NAMESPACES_SET - : Set.of(namespaces), - null); - } - - public DefaultResourceConfiguration(String labelSelector, Class resourceClass, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, - GenericFilter genericFilter, - Set namespaces, + protected DefaultResourceConfiguration(Class resourceClass, + Set namespaces, String labelSelector, OnAddFilter onAddFilter, + OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, UnaryOperator cachePruneFunction) { - this.labelSelector = labelSelector; this.resourceClass = resourceClass; + this.resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass); this.onAddFilter = onAddFilter; this.onUpdateFilter = onUpdateFilter; this.genericFilter = genericFilter; - this.namespaces = - namespaces == null || namespaces.isEmpty() ? DEFAULT_NAMESPACES_SET - : namespaces; + + this.namespaces = ResourceConfiguration.ensureValidNamespaces(namespaces); + this.labelSelector = ResourceConfiguration.ensureValidLabelSelector(labelSelector); this.cachePruneFunction = cachePruneFunction; } @Override public String getResourceTypeName() { - return ResourceConfiguration.super.getResourceTypeName(); + return resourceTypeName; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java new file mode 100644 index 0000000000..b1f653cb75 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java @@ -0,0 +1,169 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationProvider; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.Retry; + +@SuppressWarnings("rawtypes") +public class ResolvedControllerConfiguration

+ extends DefaultResourceConfiguration

+ implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration

, + DependentResourceConfigurationProvider { + + private final String name; + private final boolean generationAware; + private final String associatedReconcilerClassName; + private final Retry retry; + private final RateLimiter rateLimiter; + private final Optional maxReconciliationInterval; + private final String finalizer; + private final Map configurations; + + private ResourceEventFilter

eventFilter; + private List dependentResources; + + public ResolvedControllerConfiguration(Class

resourceClass, ControllerConfiguration

other) { + this(resourceClass, other.getName(), other.isGenerationAware(), + other.getAssociatedReconcilerClassName(), other.getRetry(), other.getRateLimiter(), + other.maxReconciliationInterval().orElse(null), + other.onAddFilter().orElse(null), other.onUpdateFilter().orElse(null), + other.genericFilter().orElse(null), other.cachePruneFunction().orElse(null), + other.getDependentResources(), other.getNamespaces(), + other.getFinalizerName(), other.getLabelSelector(), Collections.emptyMap()); + } + + public static Duration getMaxReconciliationInterval(long interval, TimeUnit timeUnit) { + return interval > 0 ? Duration.of(interval, timeUnit.toChronoUnit()) : null; + } + + public static String getAssociatedReconcilerClassName( + Class reconcilerClass) { + return reconcilerClass.getCanonicalName(); + } + + protected Retry ensureRetry(Retry given) { + return given == null ? ControllerConfiguration.super.getRetry() : given; + } + + protected RateLimiter ensureRateLimiter(RateLimiter given) { + return given == null ? ControllerConfiguration.super.getRateLimiter() : given; + } + + public ResolvedControllerConfiguration(Class

resourceClass, String name, + boolean generationAware, String associatedReconcilerClassName, Retry retry, + RateLimiter rateLimiter, Duration maxReconciliationInterval, + OnAddFilter

onAddFilter, OnUpdateFilter

onUpdateFilter, + GenericFilter

genericFilter, UnaryOperator

cachePruneFunction, + List dependentResources, + Set namespaces, String finalizer, String labelSelector, + Map configurations) { + this(resourceClass, name, generationAware, associatedReconcilerClassName, retry, rateLimiter, + maxReconciliationInterval, onAddFilter, onUpdateFilter, genericFilter, cachePruneFunction, + namespaces, finalizer, labelSelector, configurations); + setDependentResources(dependentResources); + } + + protected ResolvedControllerConfiguration(Class

resourceClass, String name, + boolean generationAware, String associatedReconcilerClassName, Retry retry, + RateLimiter rateLimiter, Duration maxReconciliationInterval, + OnAddFilter

onAddFilter, OnUpdateFilter

onUpdateFilter, GenericFilter

genericFilter, + UnaryOperator

cachePruneFunction, + Set namespaces, String finalizer, String labelSelector, + Map configurations) { + super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter, + cachePruneFunction); + this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName); + this.generationAware = generationAware; + this.associatedReconcilerClassName = associatedReconcilerClassName; + this.retry = ensureRetry(retry); + this.rateLimiter = ensureRateLimiter(rateLimiter); + this.maxReconciliationInterval = Optional.ofNullable(maxReconciliationInterval); + this.configurations = configurations != null ? configurations : Collections.emptyMap(); + + this.finalizer = + ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName()); + } + + protected ResolvedControllerConfiguration(Class

resourceClass, String name, + Class reconcilerClas) { + this(resourceClass, name, false, getAssociatedReconcilerClassName(reconcilerClas), null, null, + null, null, null, null, null, + null, null, null, null); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFinalizerName() { + return finalizer; + } + + @Override + public boolean isGenerationAware() { + return generationAware; + } + + @Override + public String getAssociatedReconcilerClassName() { + return associatedReconcilerClassName; + } + + @Override + public Retry getRetry() { + return retry; + } + + @Override + public RateLimiter getRateLimiter() { + return rateLimiter; + } + + @Override + public List getDependentResources() { + return dependentResources; + } + + protected void setDependentResources(List dependentResources) { + this.dependentResources = dependentResources == null ? Collections.emptyList() + : Collections.unmodifiableList(dependentResources); + } + + @Override + public Optional maxReconciliationInterval() { + return maxReconciliationInterval; + } + + @Override + public ResourceEventFilter

getEventFilter() { + return eventFilter; + } + + @Deprecated(forRemoval = true) + protected void setEventFilter(ResourceEventFilter

eventFilter) { + this.eventFilter = eventFilter; + } + + @Override + public Object getConfigurationFor(DependentResourceSpec spec) { + return configurations.get(spec); + } +} 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 6a6574182a..85f3bc3b36 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; @@ -47,6 +48,11 @@ default String getLabelSelector() { return null; } + static String ensureValidLabelSelector(String labelSelector) { + // might want to implement validation here? + return labelSelector; + } + @SuppressWarnings("unchecked") default Class getResourceClass() { return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), @@ -90,6 +96,14 @@ static void failIfNotValid(Set namespaces) { + Constants.WATCH_CURRENT_NAMESPACE + "'"); } + static Set ensureValidNamespaces(Collection namespaces) { + if (namespaces != null && !namespaces.isEmpty()) { + return Set.copyOf(namespaces); + } else { + return Constants.DEFAULT_NAMESPACES_SET; + } + } + /** * 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 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 00927b7f14..5838eb9b97 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 @@ -227,6 +227,13 @@ public interface Configurator { public static String contextFor(ControllerConfiguration controllerConfiguration, Class dependentType, Class configurationAnnotation) { + return contextFor(controllerConfiguration.getName(), dependentType, configurationAnnotation); + } + + @SuppressWarnings("rawtypes") + public static String contextFor(String reconcilerName, + Class dependentType, + Class configurationAnnotation) { final var annotationName = configurationAnnotation != null ? configurationAnnotation.getSimpleName() : io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class @@ -235,7 +242,7 @@ public static String contextFor(ControllerConfiguration controllerConfigurati if (dependentType != null) { context += "DependentResource: " + dependentType.getName() + ", "; } - context += "reconciler: " + controllerConfiguration.getName(); + context += "reconciler: " + reconcilerName; return context; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 03e37863df..e26597dde5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -30,7 +30,6 @@ class DefaultInformerConfiguration extends private final SecondaryToPrimaryMapper secondaryToPrimaryMapper; private final boolean followControllerNamespaceChanges; private final OnDeleteFilter onDeleteFilter; - private final UnaryOperator cachePruneFunction; protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, @@ -42,7 +41,7 @@ protected DefaultInformerConfiguration(String labelSelector, OnDeleteFilter onDeleteFilter, GenericFilter genericFilter, UnaryOperator cachePruneFunction) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces, + super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter, cachePruneFunction); this.followControllerNamespaceChanges = followControllerNamespaceChanges; @@ -51,7 +50,6 @@ protected DefaultInformerConfiguration(String labelSelector, Objects.requireNonNullElse(secondaryToPrimaryMapper, Mappers.fromOwnerReference()); this.onDeleteFilter = onDeleteFilter; - this.cachePruneFunction = cachePruneFunction; } @Override @@ -69,14 +67,10 @@ public Optional> onDeleteFilter() { } @Override + @SuppressWarnings("unchecked") public

PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper() { return (PrimaryToSecondaryMapper

) primaryToSecondaryMapper; } - - @Override - public Optional> cachePruneFunction() { - return Optional.ofNullable(this.cachePruneFunction); - } } /** diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index 94af637b38..1cc98403f8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -3,11 +3,14 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.sample.simple.*; +import io.javaoperatorsdk.operator.sample.simple.DuplicateCRController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconcilerOtherV1; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceOtherV1; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,19 +49,17 @@ private void checkException( }); final var msg = exception.getMessage(); assertTrue( - msg.contains("Cannot register controller '" + duplicated.getControllerName() + "'") - && msg.contains(registered.getControllerName()) + msg.contains("Cannot register controller '" + duplicated.getName() + "'") + && msg.contains(registered.getName()) && msg.contains(registered.getResourceTypeName())); } private static class TestControllerConfiguration - extends DefaultControllerConfiguration { + extends ResolvedControllerConfiguration { private final Reconciler controller; public TestControllerConfiguration(Reconciler controller, Class crClass) { - super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, - null, null, null, null, null, null, null); + super(crClass, getControllerName(controller), controller.getClass()); this.controller = controller; } @@ -66,9 +67,5 @@ static String getControllerName( Reconciler controller) { return controller.getClass().getSimpleName() + "Controller"; } - - private String getControllerName() { - return getControllerName(controller); - } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java index 766ef6d824..98c071e942 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class ControllerConfigurationOverriderTest { + private final BaseConfigurationService configurationService = new BaseConfigurationService(); @Test void overridingNSShouldPreserveUntouchedDependents() { @@ -118,7 +119,7 @@ private Object extractDependentKubernetesResourceConfig( private io.javaoperatorsdk.operator.api.config.ControllerConfiguration createConfiguration( Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + return configurationService.configFor(reconciler); } @ControllerConfiguration(namespaces = "foo") diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java index 983215bcb9..3187b32645 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java @@ -8,8 +8,9 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Service; -import io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -30,9 +31,27 @@ class DependentResourceConfigurationResolverTest { + // subclass to expose configFor method to this test class + private final static class TestConfigurationService extends BaseConfigurationService { + + @Override + protected

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + return super.configFor(reconciler); + } + } + + private final TestConfigurationService configurationService = new TestConfigurationService(); + + private

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + // ensure that a new configuration is created each time + return configurationService.configFor(reconciler); + } + @Test void controllerConfigurationProvidedShouldBeReturnedIfAvailable() { - final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + final var cfg = configFor(new CustomAnnotationReconciler()); final var customConfig = DependentResourceConfigurationResolver .extractConfigurationFromConfigured(CustomAnnotatedDep.class, cfg); assertTrue(customConfig instanceof CustomConfig); @@ -51,7 +70,7 @@ void controllerConfigurationProvidedShouldBeReturnedIfAvailable() { @Test void getConverterShouldWork() { - final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + final var cfg = configFor(new CustomAnnotationReconciler()); var converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class); assertNull(converter); assertNull(DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class)); @@ -77,7 +96,7 @@ void getConverterShouldWork() { @SuppressWarnings("rawtypes") @Test void registerConverterShouldWork() { - final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + final var cfg = configFor(new CustomAnnotationReconciler()); var converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); assertNull(converter); DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ConfigMapDep.class, 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 df73d79164..26994cee1a 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 @@ -8,9 +8,10 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -25,7 +26,8 @@ import static org.mockito.Mockito.verify; class ResourceEventFilterTest { - public static final String FINALIZER = "finalizer"; + public static final String FINALIZER = + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private EventHandler eventHandler; @@ -129,23 +131,21 @@ public TestControllerConfig(String finalizer, boolean generationAware, } private static class ControllerConfig extends - DefaultControllerConfiguration { + ResolvedControllerConfiguration { public ControllerConfig(String finalizer, boolean generationAware, ResourceEventFilter eventFilter, Class customResourceClass) { - super( + super(customResourceClass, + "test", + generationAware, null, - "testController", null, - finalizer, - generationAware, null, null, null, - eventFilter, - customResourceClass, null, - null, null, null, null, null, null); + null, null, null, finalizer, null, null); + setEventFilter(eventFilter); } } 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 27b43656be..5a171195e1 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 @@ -7,8 +7,9 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -19,12 +20,17 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; class ControllerResourceEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { - public static final String FINALIZER = "finalizer"; + public static final String FINALIZER = + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private final TestController testController = new TestController(true); @@ -171,24 +177,27 @@ public boolean useFinalizer() { } private static class TestConfiguration extends - DefaultControllerConfiguration { + ResolvedControllerConfiguration { public TestConfiguration(boolean generationAware, OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, GenericFilter genericFilter) { super( + TestCustomResource.class, + "test", + generationAware, null, - "testController", null, - FINALIZER, - generationAware, null, null, + onAddFilter, + onUpdateFilter, + genericFilter, null, null, - TestCustomResource.class, null, - onAddFilter, onUpdateFilter, genericFilter, null, null, null); + FINALIZER, + null, null); } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java deleted file mode 100644 index 47eee2a249..0000000000 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.javaoperatorsdk.operator.config.runtime; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; - -public class AnnotationControllerConfiguration - extends io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration { - - public AnnotationControllerConfiguration(Reconciler reconciler) { - super(reconciler); - } - - @Override - public Class getResourceClass() { - return RuntimeControllerMetadata.getResourceClass(reconciler); - } -} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java index 34e0b3f1d7..49f0ed2b67 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java @@ -4,6 +4,7 @@ import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class DefaultConfigurationService extends BaseConfigurationService { @@ -46,6 +47,8 @@ public void setCreateIfNeeded(boolean createIfNeeded) { @Override protected ControllerConfiguration configFor(Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + final var other = super.configFor(reconciler); + return new ResolvedControllerConfiguration<>( + RuntimeControllerMetadata.getResourceClass(reconciler), other); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java similarity index 90% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index bbc49ebf33..22af954661 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.config.runtime; +package io.javaoperatorsdk.operator.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -14,8 +14,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; @@ -48,11 +50,29 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class AnnotationControllerConfigurationTest { +class BaseConfigurationServiceTest { + + // subclass to expose configFor method to this test class + private final static class TestConfigurationService extends BaseConfigurationService { + + @Override + protected

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + return super.configFor(reconciler); + } + } + + private final TestConfigurationService configurationService = new TestConfigurationService(); + + private

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + // ensure that a new configuration is created each time + return configurationService.configFor(reconciler); + } @Test void defaultValuesShouldBeConsistent() { - final var configuration = new AnnotationControllerConfiguration<>(new SelectorReconciler()); + final var configuration = configFor(new SelectorReconciler()); final var annotated = extractDependentKubernetesResourceConfig(configuration, 1); final var unannotated = extractDependentKubernetesResourceConfig(configuration, 0); @@ -71,11 +91,11 @@ private KubernetesDependentResourceConfig extractDependentKubernetesResourceConf @Test @SuppressWarnings("rawtypes") void getDependentResources() { - var configuration = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var configuration = configFor(new NoDepReconciler()); var dependents = configuration.getDependentResources(); assertTrue(dependents.isEmpty()); - configuration = new AnnotationControllerConfiguration<>(new OneDepReconciler()); + configuration = configFor(new OneDepReconciler()); dependents = configuration.getDependentResources(); assertFalse(dependents.isEmpty()); assertEquals(1, dependents.size()); @@ -92,7 +112,7 @@ void getDependentResources() { assertEquals(1, config.namespaces().size()); assertEquals(Set.of(OneDepReconciler.CONFIGURED_NS), config.namespaces()); - configuration = new AnnotationControllerConfiguration<>(new NamedDepReconciler()); + configuration = configFor(new NamedDepReconciler()); dependents = configuration.getDependentResources(); assertFalse(dependents.isEmpty()); assertEquals(1, dependents.size()); @@ -107,7 +127,7 @@ void getDependentResources() { @Test void missingAnnotationThrowsException() { Assertions.assertThrows(OperatorException.class, - () -> new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler())); + () -> configFor(new MissingAnnotationReconciler())); } @SuppressWarnings("rawtypes") @@ -125,13 +145,12 @@ private Optional findByNameOptional( @Test void tryingToAddDuplicatedDependentsWithoutNameShouldFail() { - var configuration = new AnnotationControllerConfiguration<>(new DuplicatedDepReconciler()); - assertThrows(IllegalArgumentException.class, configuration::getDependentResources); + assertThrows(IllegalArgumentException.class, () -> configFor(new DuplicatedDepReconciler())); } @Test void addingDuplicatedDependentsWithNameShouldWork() { - var config = new AnnotationControllerConfiguration<>(new NamedDuplicatedDepReconciler()); + var config = configFor(new NamedDuplicatedDepReconciler()); var dependents = config.getDependentResources(); assertEquals(2, dependents.size()); assertTrue(findByNameOptional(dependents, NamedDuplicatedDepReconciler.NAME).isPresent() @@ -141,13 +160,13 @@ && findByNameOptional(dependents, DependentResource.defaultNameFor(ReadOnlyDepen @Test void maxIntervalCanBeConfigured() { - var config = new AnnotationControllerConfiguration<>(new MaxIntervalReconciler()); + var config = configFor(new MaxIntervalReconciler()); assertEquals(50, config.maxReconciliationInterval().map(Duration::getSeconds).orElseThrow()); } @Test void checkDefaultRateAndRetryConfigurations() { - var config = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var config = configFor(new NoDepReconciler()); final var retry = assertInstanceOf(GenericRetry.class, config.getRetry()); assertEquals(GradualRetry.DEFAULT_MAX_ATTEMPTS, retry.getMaxAttempts()); assertEquals(GradualRetry.DEFAULT_MULTIPLIER, retry.getIntervalMultiplier()); @@ -161,7 +180,7 @@ void checkDefaultRateAndRetryConfigurations() { @Test void configuringRateAndRetryViaAnnotationsShouldWork() { var config = - new AnnotationControllerConfiguration<>(new ConfigurableRateLimitAndRetryReconciler()); + configFor(new ConfigurableRateLimitAndRetryReconciler()); final var retry = config.getRetry(); final var testRetry = assertInstanceOf(TestRetry.class, retry); assertEquals(12, testRetry.getValue()); @@ -173,7 +192,7 @@ void configuringRateAndRetryViaAnnotationsShouldWork() { @Test void checkingRetryingGraduallyWorks() { - var config = new AnnotationControllerConfiguration<>(new CheckRetryingGraduallyConfiguration()); + var config = configFor(new CheckRetryingGraduallyConfiguration()); final var retry = config.getRetry(); final var genericRetry = assertInstanceOf(GenericRetry.class, retry); assertEquals(CheckRetryingGraduallyConfiguration.INITIAL_INTERVAL, @@ -186,13 +205,13 @@ void checkingRetryingGraduallyWorks() { @Test void controllerConfigurationOnSuperClassShouldWork() { - var config = new AnnotationControllerConfiguration<>(new ControllerConfigurationOnSuperClass()); + var config = configFor(new ControllerConfigurationOnSuperClass()); assertNotNull(config.getName()); } @Test void configuringFromCustomAnnotationsShouldWork() { - var config = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler()); + var config = configFor(new CustomAnnotationReconciler()); assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, getValue(config, 0)); assertEquals(CustomConfigConverter.CONVERTER_PROVIDED_DEFAULT, getValue(config, 1)); } From c72baa2ee19c3e2a8a8d6cb2cb39ca5f0f0e7522 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 9 Dec 2022 19:56:24 +0100 Subject: [PATCH 12/14] chore: temporarily revert SSA for finalizers (#1654) --- .../event/ReconciliationDispatcher.java | 47 +++++++++++++++++-- .../event/ReconciliationDispatcherTest.java | 7 ++- .../operator/CachePruneIT.java | 2 + 3 files changed, 50 insertions(+), 6 deletions(-) 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 4a7ac68082..0328261c74 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 @@ -295,8 +295,10 @@ private PostExecutionControl

handleCleanup(P originalResource, P resource, // cleanup is finished, nothing left to done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) { - P customResource = conflictRetryingPatch(resource, originalResource, - r -> r.removeFinalizer(finalizerName)); + P customResource = removeFinalizer(resource, finalizerName); + // todo: restore SSA + // P customResource = conflictRetryingPatch(resource, originalResource, + // r -> r.removeFinalizer(finalizerName)); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -311,13 +313,49 @@ private PostExecutionControl

handleCleanup(P originalResource, P resource, return postExecutionControl; } + // todo: remove after restoring SSA + public P removeFinalizer(P resource, String finalizer) { + if (log.isDebugEnabled()) { + log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource)); + } + int retryIndex = 0; + while (true) { + try { + var removed = resource.removeFinalizer(finalizer); + if (!removed) { + return resource; + } + return customResourceFacade.updateResource(resource); + } catch (KubernetesClientException e) { + log.trace("Exception during finalizer removal for resource: {}", resource); + retryIndex++; + // only retry on conflict (HTTP 409), otherwise fail + if (e.getCode() != 409) { + throw e; + } + if (retryIndex >= MAX_FINALIZER_REMOVAL_RETRY) { + throw new OperatorException( + "Exceeded maximum (" + MAX_FINALIZER_REMOVAL_RETRY + + ") retry attempts to remove finalizer '" + finalizer + "' for resource " + + ResourceID.fromResource(resource)); + } + resource = customResourceFacade.getResource(resource.getMetadata().getNamespace(), + resource.getMetadata().getName()); + } + } + } + private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(originalResource), getVersion(originalResource)); - return conflictRetryingPatch(resourceForExecution, originalResource, - r -> r.addFinalizer(configuration().getFinalizerName())); + originalResource.addFinalizer(configuration().getFinalizerName()); + return customResourceFacade.updateResource(originalResource); + + // todo: restore SSA + // return conflictRetryingPatch(resourceForExecution, originalResource, + // r -> r.addFinalizer(configuration().getFinalizerName())); } private P updateCustomResource(P resource) { @@ -396,7 +434,6 @@ public R updateResource(R resource) { .replace(); } - @SuppressWarnings({"rawtypes", "unchecked"}) public R updateStatus(R resource) { log.trace("Updating status for resource: {}", resource); return resource(resource) 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 ef7a27677b..2f43b3ad62 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 @@ -9,6 +9,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; @@ -22,7 +23,10 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.*; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -55,6 +59,7 @@ import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked", "rawtypes"}) +@Disabled(value = "todo: reactivate when restoring SSA") class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java index 9bf57ab372..15106015ee 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java @@ -3,6 +3,7 @@ import java.time.Duration; import java.util.Map; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -17,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Disabled(value = "todo: reactivate when restoring SSA") class CachePruneIT { public static final String DEFAULT_DATA = "default_data"; From 96d3294aae376628640dcc030641ab879918310a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 12 Dec 2022 12:27:29 +0100 Subject: [PATCH 13/14] Revert "chore: temporarily revert SSA for finalizers (#1654)" (#1655) This reverts commit c72baa2ee19c3e2a8a8d6cb2cb39ca5f0f0e7522. --- .../event/ReconciliationDispatcher.java | 47 ++----------------- .../event/ReconciliationDispatcherTest.java | 7 +-- .../operator/CachePruneIT.java | 2 - 3 files changed, 6 insertions(+), 50 deletions(-) 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 0328261c74..4a7ac68082 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 @@ -295,10 +295,8 @@ private PostExecutionControl

handleCleanup(P originalResource, P resource, // cleanup is finished, nothing left to done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) { - P customResource = removeFinalizer(resource, finalizerName); - // todo: restore SSA - // P customResource = conflictRetryingPatch(resource, originalResource, - // r -> r.removeFinalizer(finalizerName)); + P customResource = conflictRetryingPatch(resource, originalResource, + r -> r.removeFinalizer(finalizerName)); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -313,49 +311,13 @@ private PostExecutionControl

handleCleanup(P originalResource, P resource, return postExecutionControl; } - // todo: remove after restoring SSA - public P removeFinalizer(P resource, String finalizer) { - if (log.isDebugEnabled()) { - log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource)); - } - int retryIndex = 0; - while (true) { - try { - var removed = resource.removeFinalizer(finalizer); - if (!removed) { - return resource; - } - return customResourceFacade.updateResource(resource); - } catch (KubernetesClientException e) { - log.trace("Exception during finalizer removal for resource: {}", resource); - retryIndex++; - // only retry on conflict (HTTP 409), otherwise fail - if (e.getCode() != 409) { - throw e; - } - if (retryIndex >= MAX_FINALIZER_REMOVAL_RETRY) { - throw new OperatorException( - "Exceeded maximum (" + MAX_FINALIZER_REMOVAL_RETRY - + ") retry attempts to remove finalizer '" + finalizer + "' for resource " - + ResourceID.fromResource(resource)); - } - resource = customResourceFacade.getResource(resource.getMetadata().getNamespace(), - resource.getMetadata().getName()); - } - } - } - private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(originalResource), getVersion(originalResource)); - originalResource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.updateResource(originalResource); - - // todo: restore SSA - // return conflictRetryingPatch(resourceForExecution, originalResource, - // r -> r.addFinalizer(configuration().getFinalizerName())); + return conflictRetryingPatch(resourceForExecution, originalResource, + r -> r.addFinalizer(configuration().getFinalizerName())); } private P updateCustomResource(P resource) { @@ -434,6 +396,7 @@ public R updateResource(R resource) { .replace(); } + @SuppressWarnings({"rawtypes", "unchecked"}) public R updateStatus(R resource) { log.trace("Updating status for resource: {}", resource); return resource(resource) 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 2f43b3ad62..ef7a27677b 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 @@ -9,7 +9,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; @@ -23,10 +22,7 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.*; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -59,7 +55,6 @@ import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked", "rawtypes"}) -@Disabled(value = "todo: reactivate when restoring SSA") class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java index 15106015ee..9bf57ab372 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.util.Map; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -18,7 +17,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -@Disabled(value = "todo: reactivate when restoring SSA") class CachePruneIT { public static final String DEFAULT_DATA = "default_data"; From 7dc93b06eb65256460f9a8d7bbba713d517f8afb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 12 Dec 2022 20:35:58 +0100 Subject: [PATCH 14/14] chore: upgrade to fabric8 client 6.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34006aeeef..4abd8540c1 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ https://sonarcloud.io 5.9.1 - 6.2.0 + 6.3.0 1.7.36 2.19.0 4.9.0