From bc9b499ba3ead7734b43fe8093653a68e826c069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 11 Aug 2025 10:58:18 +0200 Subject: [PATCH 01/41] feat: all-event mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- notes.txt | 1 + .../operator/api/config/ControllerConfiguration.java | 4 ++++ .../operator/api/config/ControllerMode.java | 6 ++++++ .../operator/api/reconciler/ControllerConfiguration.java | 3 +++ .../operator/processing/event/EventProcessor.java | 9 +++++++-- .../operator/processing/event/ResourceState.java | 9 ++++++++- 6 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 notes.txt create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000000..253d135578 --- /dev/null +++ b/notes.txt @@ -0,0 +1 @@ +- check that Cleaner interface is not present 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 3898493c82..8cbd0763bd 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 @@ -92,4 +92,8 @@ default String fieldManager() { } C getConfigurationFor(DependentResourceSpec spec); + + default ControllerMode getMode() { + return ControllerMode.DEFAULT; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java new file mode 100644 index 0000000000..330313015b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.config; + +public enum ControllerMode { + DEFAULT, + ALL_EVENT_MODE +} 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 d407ed0fc6..bf64009997 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 @@ -6,6 +6,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -77,4 +78,6 @@ MaxReconciliationInterval maxReconciliationInterval() default * @return the name used as field manager for SSA operations */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; + + ControllerMode allEventMode() default ControllerMode.DEFAULT; } 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 e029e287a0..52d15a6138 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 @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.LifecycleAware; @@ -130,7 +131,7 @@ public synchronized void handleEvent(Event event) { } private void handleMarkedEventForResource(ResourceState state) { - if (state.deleteEventPresent()) { + if (state.deleteEventPresent() && !isAllEventMode()) { cleanupForDeletedEvent(state.getId()); } else if (!state.processedMarkForDeletionPresent()) { submitReconciliationExecution(state); @@ -187,7 +188,7 @@ private void handleEventMarking(Event event, ResourceState state) { if (event instanceof ResourceEvent resourceEvent) { if (resourceEvent.getAction() == ResourceAction.DELETED) { log.debug("Marking delete event received for: {}", relatedCustomResourceID); - state.markDeleteEventReceived(); + state.markDeleteEventReceived(resourceEvent.getResource().orElseThrow()); } else { if (state.processedMarkForDeletionPresent() && isResourceMarkedForDeletion(resourceEvent)) { log.debug( @@ -509,4 +510,8 @@ public synchronized boolean isUnderProcessing(ResourceID resourceID) { public synchronized boolean isRunning() { return running; } + + private boolean isAllEventMode() { + return controllerConfiguration.getMode() == ControllerMode.ALL_EVENT_MODE; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 5d4e74d681..59ad479c0d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; @@ -29,6 +30,7 @@ private enum EventingState { private RetryExecution retry; private EventingState eventing; private RateLimitState rateLimit; + private HasMetadata lastKnownResource; public ResourceState(ResourceID id) { this.id = id; @@ -63,8 +65,9 @@ public void setUnderProcessing(boolean underProcessing) { this.underProcessing = underProcessing; } - public void markDeleteEventReceived() { + public void markDeleteEventReceived(HasMetadata lastKnownResource) { eventing = EventingState.DELETE_EVENT_PRESENT; + this.lastKnownResource = lastKnownResource; } public boolean deleteEventPresent() { @@ -94,6 +97,10 @@ public boolean noEventPresent() { return eventing == EventingState.NO_EVENT_PRESENT; } + public HasMetadata getLastKnownResource() { + return lastKnownResource; + } + public void unMarkEventReceived() { switch (eventing) { case EVENT_PRESENT: From cf376ee09b62d9bbf371a0fabebc5e1c95cb73d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 11 Aug 2025 13:39:21 +0200 Subject: [PATCH 02/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/ControllerMode.java | 2 +- .../processing/event/EventProcessor.java | 9 +++++- .../event/ResourceStateManager.java | 4 +++ .../controller/ControllerEventSource.java | 23 ++++++++++---- .../controller/ResourceDeleteEvent.java | 18 +++++++++++ .../event/ResourceStateManagerTest.java | 10 ++++--- .../controller/ControllerEventSourceTest.java | 30 +++++++++---------- 7 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java index 330313015b..536cefc7cf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java @@ -2,5 +2,5 @@ public enum ControllerMode { DEFAULT, - ALL_EVENT_MODE + RECONCILE_ALL_EVENT } 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 52d15a6138..15c260e2e9 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 @@ -473,6 +473,13 @@ public void run() { try { var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { + if (isAllEventMode()) { + var state = resourceStateManager.get(resourceID); + actualResource = + (Optional

) + state.filter(s -> s.deleteEventPresent()).map(s -> s.getLastKnownResource()); + } + log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); return; } @@ -512,6 +519,6 @@ public synchronized boolean isRunning() { } private boolean isAllEventMode() { - return controllerConfiguration.getMode() == ControllerMode.ALL_EVENT_MODE; + return controllerConfiguration.getMode() == ControllerMode.RECONCILE_ALL_EVENT; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java index 481fd317ff..b55b432d4b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManager.java @@ -33,6 +33,10 @@ public ResourceState getOrCreate(ResourceID resourceID) { return states.computeIfAbsent(resourceID, ResourceState::new); } + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(states.get(resourceID)); + } + public ResourceState remove(ResourceID resourceID) { return states.remove(resourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index eb9f65eafc..a505a97702 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -62,7 +62,8 @@ public synchronized void start() { } } - public void eventReceived(ResourceAction action, T resource, T oldResource) { + public void eventReceived( + ResourceAction action, T resource, T oldResource, Boolean deletedFinalStateUnknown) { try { if (log.isDebugEnabled()) { log.debug( @@ -76,8 +77,18 @@ public void eventReceived(ResourceAction action, T resource, T oldResource) { MDCUtils.addResourceInfo(resource); controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); if (isAcceptedByFilters(action, resource, oldResource)) { - getEventHandler() - .handleEvent(new ResourceEvent(action, ResourceID.fromResource(resource), resource)); + if (deletedFinalStateUnknown != null) { + getEventHandler() + .handleEvent( + new ResourceDeleteEvent( + action, + ResourceID.fromResource(resource), + resource, + deletedFinalStateUnknown)); + } else { + getEventHandler() + .handleEvent(new ResourceEvent(action, ResourceID.fromResource(resource), resource)); + } } else { log.debug("Skipping event handling resource {}", ResourceID.fromResource(resource)); } @@ -103,19 +114,19 @@ private boolean isAcceptedByFilters(ResourceAction action, T resource, T oldReso @Override public void onAdd(T resource) { super.onAdd(resource); - eventReceived(ResourceAction.ADDED, resource, null); + eventReceived(ResourceAction.ADDED, resource, null, null); } @Override public void onUpdate(T oldCustomResource, T newCustomResource) { super.onUpdate(oldCustomResource, newCustomResource); - eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); + eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource, null); } @Override public void onDelete(T resource, boolean b) { super.onDelete(resource, b); - eventReceived(ResourceAction.DELETED, resource, null); + eventReceived(ResourceAction.DELETED, resource, null, b); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java new file mode 100644 index 0000000000..83cee7fc77 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.processing.event.source.controller; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class ResourceDeleteEvent extends ResourceEvent { + + private final boolean deletedFinalStateUnknown; + + public ResourceDeleteEvent( + ResourceAction action, + ResourceID resourceID, + HasMetadata resource, + boolean deletedFinalStateUnknown) { + super(action, resourceID, resource); + this.deletedFinalStateUnknown = deletedFinalStateUnknown; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index 487ba25885..3473c82624 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -8,6 +8,8 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; +import io.javaoperatorsdk.operator.TestUtils; + import static org.assertj.core.api.Assertions.assertThat; class ResourceStateManagerTest { @@ -42,7 +44,7 @@ public void marksEvent() { @Test public void marksDeleteEvent() { - state.markDeleteEventReceived(); + state.markDeleteEventReceived(TestUtils.testCustomResource()); assertThat(state.deleteEventPresent()).isTrue(); assertThat(state.eventPresent()).isFalse(); @@ -52,7 +54,7 @@ public void marksDeleteEvent() { public void afterDeleteEventMarkEventIsNotRelevant() { state.markEventReceived(); - state.markDeleteEventReceived(); + state.markDeleteEventReceived(TestUtils.testCustomResource()); assertThat(state.deleteEventPresent()).isTrue(); assertThat(state.eventPresent()).isFalse(); @@ -61,7 +63,7 @@ public void afterDeleteEventMarkEventIsNotRelevant() { @Test public void cleansUp() { state.markEventReceived(); - state.markDeleteEventReceived(); + state.markDeleteEventReceived(TestUtils.testCustomResource()); manager.remove(sampleResourceID); @@ -75,7 +77,7 @@ public void cannotMarkEventAfterDeleteEventReceived() { Assertions.assertThrows( IllegalStateException.class, () -> { - state.markDeleteEventReceived(); + state.markDeleteEventReceived(TestUtils.testCustomResource()); state.markEventReceived(); }); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index 6548bbddc7..257af38e0c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -53,10 +53,10 @@ void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource oldCustomResource = TestUtils.testCustomResource(); oldCustomResource.getMetadata().setFinalizers(List.of(FINALIZER)); - source.eventReceived(ResourceAction.UPDATED, customResource, oldCustomResource); + source.eventReceived(ResourceAction.UPDATED, customResource, oldCustomResource, null); verify(eventHandler, times(1)).handleEvent(any()); - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); + source.eventReceived(ResourceAction.UPDATED, customResource, customResource, null); verify(eventHandler, times(1)).handleEvent(any()); } @@ -64,12 +64,12 @@ void skipsEventHandlingIfGenerationNotIncreased() { void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(1)).handleEvent(any()); // mark for deletion customResource1.getMetadata().setDeletionTimestamp(LocalDateTime.now().toString()); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(2)).handleEvent(any()); } @@ -77,11 +77,11 @@ void dontSkipEventHandlingIfMarkedForDeletion() { void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(1)).handleEvent(any()); customResource1.getMetadata().setGeneration(2L); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(2)).handleEvent(any()); } @@ -92,10 +92,10 @@ void handlesAllEventIfNotGenerationAware() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(1)).handleEvent(any()); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(2)).handleEvent(any()); } @@ -103,7 +103,7 @@ void handlesAllEventIfNotGenerationAware() { void eventWithNoGenerationProcessedIfNoFinalizer() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(eventHandler, times(1)).handleEvent(any()); } @@ -112,7 +112,7 @@ void eventWithNoGenerationProcessedIfNoFinalizer() { void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1, null); verify(testController.getEventSourceManager(), times(1)) .broadcastOnResourceEvent( @@ -128,8 +128,8 @@ void filtersOutEventsOnAddAndUpdate() { source = new ControllerEventSource<>(new TestController(onAddFilter, onUpdatePredicate, null)); setUpSource(source, true, controllerConfig); - source.eventReceived(ResourceAction.ADDED, cr, null); - source.eventReceived(ResourceAction.UPDATED, cr, cr); + source.eventReceived(ResourceAction.ADDED, cr, null, null); + source.eventReceived(ResourceAction.UPDATED, cr, cr, null); verify(eventHandler, never()).handleEvent(any()); } @@ -141,9 +141,9 @@ void genericFilterFiltersOutAddUpdateAndDeleteEvents() { source = new ControllerEventSource<>(new TestController(null, null, res -> false)); setUpSource(source, true, controllerConfig); - source.eventReceived(ResourceAction.ADDED, cr, null); - source.eventReceived(ResourceAction.UPDATED, cr, cr); - source.eventReceived(ResourceAction.DELETED, cr, cr); + source.eventReceived(ResourceAction.ADDED, cr, null, null); + source.eventReceived(ResourceAction.UPDATED, cr, cr, null); + source.eventReceived(ResourceAction.DELETED, cr, cr, true); verify(eventHandler, never()).handleEvent(any()); } From 028bc95fdb343edb7c93a4e75b65d816f3acf688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 15 Aug 2025 10:49:35 +0200 Subject: [PATCH 03/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/BaseConfigurationService.java | 7 +++- .../api/config/ControllerConfiguration.java | 6 ++- .../ControllerConfigurationOverrider.java | 8 ++++ .../operator/api/config/ControllerMode.java | 2 +- .../ResolvedControllerConfiguration.java | 18 +++++++-- .../operator/api/reconciler/Context.java | 4 ++ .../reconciler/ControllerConfiguration.java | 2 +- .../api/reconciler/DefaultContext.java | 23 +++++++++++- .../processing/event/EventProcessor.java | 37 ++++++++++++------- .../processing/event/ExecutionScope.java | 18 +++++++++ .../event/ReconciliationDispatcher.java | 16 ++++++-- .../processing/event/ResourceState.java | 9 ++++- .../controller/ResourceDeleteEvent.java | 4 ++ .../api/reconciler/DefaultContextTest.java | 3 +- .../operator/processing/ControllerTest.java | 3 +- .../processing/event/EventProcessorTest.java | 6 ++- .../event/ResourceStateManagerTest.java | 8 ++-- .../controller/ControllerEventSourceTest.java | 3 +- 18 files changed, 141 insertions(+), 36 deletions(-) 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 891f199dbe..785cb99e75 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 @@ -304,12 +304,14 @@ private

ResolvedControllerConfiguration

controllerCon final var dependentFieldManager = fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager; + var controllerMode = annotation == null ? ControllerMode.DEFAULT : annotation.controllerMode(); + InformerConfiguration

informerConfig = InformerConfiguration.builder(resourceClass) .initFromAnnotation(annotation != null ? annotation.informer() : null, context) .buildForController(); - return new ResolvedControllerConfiguration

( + return new ResolvedControllerConfiguration<>( name, generationAware, associatedReconcilerClass, @@ -323,7 +325,8 @@ private

ResolvedControllerConfiguration

controllerCon null, dependentFieldManager, this, - informerConfig); + informerConfig, + controllerMode); } /** 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 8cbd0763bd..44a1fbc46d 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 @@ -93,7 +93,11 @@ default String fieldManager() { C getConfigurationFor(DependentResourceSpec spec); - default ControllerMode getMode() { + default ControllerMode getControllerMode() { return ControllerMode.DEFAULT; } + + default boolean isAllEventReconcileMode() { + return getControllerMode() == ControllerMode.ALL_EVENT_RECONCILE; + } } 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 d2e37a397d..5138c666a9 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 @@ -30,6 +30,7 @@ public class ControllerConfigurationOverrider { private Duration reconciliationMaxInterval; private Map configurations; private final InformerConfiguration.Builder config; + private ControllerMode controllerMode; private ControllerConfigurationOverrider(ControllerConfiguration original) { this.finalizer = original.getFinalizerName(); @@ -42,6 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { this.rateLimiter = original.getRateLimiter(); this.name = original.getName(); this.fieldManager = original.fieldManager(); + this.controllerMode = original.getControllerMode(); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -154,6 +156,11 @@ public ControllerConfigurationOverrider withFieldManager(String dependentFiel return this; } + public ControllerConfigurationOverrider withControllerMode(ControllerMode controllerMode) { + this.controllerMode = controllerMode; + return this; + } + /** * Sets a max page size limit when starting the informer. This will result in pagination while * populating the cache. This means that longer lists will take multiple requests to fetch. See @@ -198,6 +205,7 @@ public ControllerConfiguration build() { fieldManager, original.getConfigurationService(), config.buildForController(), + controllerMode, original.getWorkflowSpec().orElse(null)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java index 536cefc7cf..2e73b28c15 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java @@ -2,5 +2,5 @@ public enum ControllerMode { DEFAULT, - RECONCILE_ALL_EVENT + ALL_EVENT_RECONCILE } 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 index 3c26659ed2..4880eee48c 100644 --- 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 @@ -30,6 +30,7 @@ public class ResolvedControllerConfiguration

private final ConfigurationService configurationService; private final String fieldManager; private WorkflowSpec workflowSpec; + private ControllerMode controllerMode; public ResolvedControllerConfiguration(ControllerConfiguration

other) { this( @@ -44,6 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration

other) { other.fieldManager(), other.getConfigurationService(), other.getInformerConfig(), + other.getControllerMode(), other.getWorkflowSpec().orElse(null)); } @@ -59,6 +61,7 @@ public ResolvedControllerConfiguration( String fieldManager, ConfigurationService configurationService, InformerConfiguration

informerConfig, + ControllerMode controllerMode, WorkflowSpec workflowSpec) { this( name, @@ -71,7 +74,8 @@ public ResolvedControllerConfiguration( configurations, fieldManager, configurationService, - informerConfig); + informerConfig, + controllerMode); setWorkflowSpec(workflowSpec); } @@ -86,7 +90,8 @@ protected ResolvedControllerConfiguration( Map configurations, String fieldManager, ConfigurationService configurationService, - InformerConfiguration

informerConfig) { + InformerConfiguration

informerConfig, + ControllerMode controllerMode) { this.informerConfig = informerConfig; this.configurationService = configurationService; this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName); @@ -99,6 +104,7 @@ protected ResolvedControllerConfiguration( this.finalizer = ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName()); this.fieldManager = fieldManager; + this.controllerMode = controllerMode; } protected ResolvedControllerConfiguration( @@ -117,7 +123,8 @@ protected ResolvedControllerConfiguration( null, null, configurationService, - InformerConfiguration.builder(resourceClass).buildForController()); + InformerConfiguration.builder(resourceClass).buildForController(), + null); } @Override @@ -207,4 +214,9 @@ public C getConfigurationFor(DependentResourceSpec spec) { public String fieldManager() { return fieldManager; } + + @Override + public ControllerMode getControllerMode() { + return controllerMode; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index f47deb9734..b063dfedaf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -72,4 +72,8 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { * @return {@code true} is another reconciliation is already scheduled, {@code false} otherwise */ boolean isNextReconciliationImminent(); + + boolean isDeleteEventPresent(); + + boolean isDeleteFinalStateUnknown(); } 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 bf64009997..d235124463 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 @@ -79,5 +79,5 @@ MaxReconciliationInterval maxReconciliationInterval() default */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; - ControllerMode allEventMode() default ControllerMode.DEFAULT; + ControllerMode controllerMode() default ControllerMode.DEFAULT; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 2acf8d13ca..d6d454bd3f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -24,12 +24,21 @@ public class DefaultContext

implements Context

{ private final ControllerConfiguration

controllerConfiguration; private final DefaultManagedWorkflowAndDependentResourceContext

defaultManagedDependentResourceContext; - - public DefaultContext(RetryInfo retryInfo, Controller

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

controller, + P primaryResource, + boolean isDeleteEventPresent, + boolean isDeleteFinalStateUnknown) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; this.controllerConfiguration = controller.getConfiguration(); + this.isDeleteEventPresent = isDeleteEventPresent; + this.isDeleteFinalStateUnknown = isDeleteFinalStateUnknown; this.defaultManagedDependentResourceContext = new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this); } @@ -119,6 +128,16 @@ public boolean isNextReconciliationImminent() { .isNextReconciliationImminent(ResourceID.fromResource(primaryResource)); } + @Override + public boolean isDeleteEventPresent() { + return isDeleteEventPresent; + } + + @Override + public boolean isDeleteFinalStateUnknown() { + return isDeleteFinalStateUnknown; + } + public DefaultContext

setRetryInfo(RetryInfo retryInfo) { this.retryInfo = retryInfo; return this; 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 15c260e2e9..0881176ea1 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 @@ -15,7 +15,6 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.LifecycleAware; @@ -24,6 +23,7 @@ import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceDeleteEvent; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.Retry; @@ -131,7 +131,7 @@ public synchronized void handleEvent(Event event) { } private void handleMarkedEventForResource(ResourceState state) { - if (state.deleteEventPresent() && !isAllEventMode()) { + if (state.deleteEventPresent() && !controllerConfiguration.isAllEventReconcileMode()) { cleanupForDeletedEvent(state.getId()); } else if (!state.processedMarkForDeletionPresent()) { submitReconciliationExecution(state); @@ -188,7 +188,9 @@ private void handleEventMarking(Event event, ResourceState state) { if (event instanceof ResourceEvent resourceEvent) { if (resourceEvent.getAction() == ResourceAction.DELETED) { log.debug("Marking delete event received for: {}", relatedCustomResourceID); - state.markDeleteEventReceived(resourceEvent.getResource().orElseThrow()); + state.markDeleteEventReceived( + resourceEvent.getResource().orElseThrow(), + ((ResourceDeleteEvent) resourceEvent).isDeletedFinalStateUnknown()); } else { if (state.processedMarkForDeletionPresent() && isResourceMarkedForDeletion(resourceEvent)) { log.debug( @@ -260,7 +262,8 @@ synchronized void eventProcessingFinished( } cleanupOnSuccessfulExecution(executionScope); metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); - if (state.deleteEventPresent()) { + if ((controllerConfiguration.isAllEventReconcileMode() && executionScope.isDeleteEvent()) + || (!controllerConfiguration.isAllEventReconcileMode() && state.deleteEventPresent())) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { state.markProcessedMarkForDeletion(); @@ -459,6 +462,7 @@ private ReconcilerExecutor(ResourceID resourceID, ExecutionScope

executionSco } @Override + @SuppressWarnings("unchecked") public void run() { if (!running) { // this is needed for the case when controller stopped, but there is a graceful shutdown @@ -473,15 +477,26 @@ public void run() { try { var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { - if (isAllEventMode()) { + if (controllerConfiguration.isAllEventReconcileMode()) { + log.debug( + "Resource not found in the cache, checking for delete event resource: {}", + resourceID); var state = resourceStateManager.get(resourceID); actualResource = (Optional

) - state.filter(s -> s.deleteEventPresent()).map(s -> s.getLastKnownResource()); + state + .filter(ResourceState::deleteEventPresent) + .map(ResourceState::getLastKnownResource); + if (actualResource.isEmpty()) { + log.debug( + "Skipping execution; delete event resource not found in state: {}", resourceID); + return; + } + executionScope.setDeleteEvent(true); + } else { + log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); + return; } - - log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); - return; } actualResource.ifPresent(executionScope::setResource); MDCUtils.addResourceInfo(executionScope.getResource()); @@ -517,8 +532,4 @@ public synchronized boolean isUnderProcessing(ResourceID resourceID) { public synchronized boolean isRunning() { return running; } - - private boolean isAllEventMode() { - return controllerConfiguration.getMode() == ControllerMode.RECONCILE_ALL_EVENT; - } } 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 90899a6e1a..b7a253faa1 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 @@ -8,6 +8,8 @@ class ExecutionScope { // the latest custom resource from cache private R resource; private final RetryInfo retryInfo; + private boolean deleteEvent = false; + private boolean isDeleteFinalStateUnknown = false; ExecutionScope(RetryInfo retryInfo) { this.retryInfo = retryInfo; @@ -26,6 +28,22 @@ public ResourceID getResourceID() { return ResourceID.fromResource(resource); } + public boolean isDeleteEvent() { + return deleteEvent; + } + + public void setDeleteEvent(boolean deleteEvent) { + this.deleteEvent = deleteEvent; + } + + public boolean isDeleteFinalStateUnknown() { + return isDeleteFinalStateUnknown; + } + + public void setDeleteFinalStateUnknown(boolean deleteFinalStateUnknown) { + isDeleteFinalStateUnknown = deleteFinalStateUnknown; + } + @Override public String toString() { if (resource == null) { 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 41d7a4f493..ac4d600a0e 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 @@ -81,7 +81,9 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) originalResource.getMetadata().getNamespace()); final var markedForDeletion = originalResource.isMarkedForDeletion(); - if (markedForDeletion && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { + if (!configuration().isAllEventReconcileMode() + && markedForDeletion + && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { log.debug( "Skipping cleanup of resource {} because finalizer(s) {} don't allow processing yet", getName(originalResource), @@ -90,8 +92,13 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) } Context

context = - new DefaultContext<>(executionScope.getRetryInfo(), controller, resourceForExecution); - if (markedForDeletion) { + new DefaultContext<>( + executionScope.getRetryInfo(), + controller, + resourceForExecution, + executionScope.isDeleteEvent(), + executionScope.isDeleteFinalStateUnknown()); + if (markedForDeletion && !configuration().isAllEventReconcileMode()) { return handleCleanup(resourceForExecution, originalResource, context); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); @@ -110,7 +117,8 @@ private PostExecutionControl

handleReconcile( P originalResource, Context

context) throws Exception { - if (controller.useFinalizer() + if (!configuration().isAllEventReconcileMode() + && controller.useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 59ad479c0d..3ed12d963b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -31,6 +31,7 @@ private enum EventingState { private EventingState eventing; private RateLimitState rateLimit; private HasMetadata lastKnownResource; + private boolean isDeleteFinalStateUnknown; public ResourceState(ResourceID id) { this.id = id; @@ -65,9 +66,11 @@ public void setUnderProcessing(boolean underProcessing) { this.underProcessing = underProcessing; } - public void markDeleteEventReceived(HasMetadata lastKnownResource) { + public void markDeleteEventReceived( + HasMetadata lastKnownResource, boolean isDeleteFinalStateUnknown) { eventing = EventingState.DELETE_EVENT_PRESENT; this.lastKnownResource = lastKnownResource; + this.isDeleteFinalStateUnknown = isDeleteFinalStateUnknown; } public boolean deleteEventPresent() { @@ -97,6 +100,10 @@ public boolean noEventPresent() { return eventing == EventingState.NO_EVENT_PRESENT; } + public boolean isDeleteFinalStateUnknown() { + return isDeleteFinalStateUnknown; + } + public HasMetadata getLastKnownResource() { return lastKnownResource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java index 83cee7fc77..73d856e922 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java @@ -15,4 +15,8 @@ public ResourceDeleteEvent( super(action, resourceID, resource); this.deletedFinalStateUnknown = deletedFinalStateUnknown; } + + public boolean isDeletedFinalStateUnknown() { + return deletedFinalStateUnknown; + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java index b289d68b22..1f59b8912c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java @@ -18,7 +18,8 @@ class DefaultContextTest { private final Secret primary = new Secret(); private final Controller mockController = mock(); - private final DefaultContext context = new DefaultContext<>(null, mockController, primary); + private final DefaultContext context = + new DefaultContext<>(null, mockController, primary, false, false); @Test @SuppressWarnings("unchecked") diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 82ecdb111a..ca14fbc76b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -122,7 +122,8 @@ void callsCleanupOnWorkflowWhenHasCleanerAndReconcilerIsNotCleaner( new Controller( reconciler, configuration, MockKubernetesClient.client(Secret.class)); - controller.cleanup(new Secret(), new DefaultContext<>(null, controller, new Secret())); + controller.cleanup( + new Secret(), new DefaultContext<>(null, controller, new Secret(), false, false)); verify(managedWorkflowMock, times(workflowCleanerExecuted ? 1 : 0)).cleanup(any(), any()); } 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 9819eb7ee9..f5060a3492 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 @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -24,6 +25,7 @@ import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceDeleteEvent; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -482,7 +484,9 @@ void cleansUpForDeleteEventEvenIfProcessorNotStarted() { null)); eventProcessor.handleEvent(prepareCREvent(resourceID)); - eventProcessor.handleEvent(new ResourceEvent(ResourceAction.DELETED, resourceID, null)); + eventProcessor.handleEvent( + new ResourceDeleteEvent( + ResourceAction.DELETED, resourceID, TestUtils.testCustomResource(), true)); eventProcessor.handleEvent(prepareCREvent(resourceID)); // no exception thrown } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index 3473c82624..cd5e5381fd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -44,7 +44,7 @@ public void marksEvent() { @Test public void marksDeleteEvent() { - state.markDeleteEventReceived(TestUtils.testCustomResource()); + state.markDeleteEventReceived(TestUtils.testCustomResource(), true); assertThat(state.deleteEventPresent()).isTrue(); assertThat(state.eventPresent()).isFalse(); @@ -54,7 +54,7 @@ public void marksDeleteEvent() { public void afterDeleteEventMarkEventIsNotRelevant() { state.markEventReceived(); - state.markDeleteEventReceived(TestUtils.testCustomResource()); + state.markDeleteEventReceived(TestUtils.testCustomResource(), true); assertThat(state.deleteEventPresent()).isTrue(); assertThat(state.eventPresent()).isFalse(); @@ -63,7 +63,7 @@ public void afterDeleteEventMarkEventIsNotRelevant() { @Test public void cleansUp() { state.markEventReceived(); - state.markDeleteEventReceived(TestUtils.testCustomResource()); + state.markDeleteEventReceived(TestUtils.testCustomResource(), true); manager.remove(sampleResourceID); @@ -77,7 +77,7 @@ public void cannotMarkEventAfterDeleteEventReceived() { Assertions.assertThrows( IllegalStateException.class, () -> { - state.markDeleteEventReceived(TestUtils.testCustomResource()); + state.markDeleteEventReceived(TestUtils.testCustomResource(), true); state.markEventReceived(); }); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index 257af38e0c..e4891d0456 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -208,7 +208,8 @@ public TestConfiguration( .withOnAddFilter(onAddFilter) .withOnUpdateFilter(onUpdateFilter) .withGenericFilter(genericFilter) - .buildForController()); + .buildForController(), + null); } } } From 6cb055ff1a66c0205ec8cc8272e6dc69bcbd77b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 15 Aug 2025 10:57:25 +0200 Subject: [PATCH 04/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0881176ea1..baae5d5158 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 @@ -255,7 +255,7 @@ synchronized void eventProcessingFinished( // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() - && !state.deleteEventPresent()) { + && (!state.deleteEventPresent() || controllerConfiguration.isAllEventReconcileMode())) { handleRetryOnException( executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; From 02024baf1178c7e3b6d8f2a12abd8e986dd371dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 15 Aug 2025 11:33:32 +0200 Subject: [PATCH 05/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/ResourceState.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 3ed12d963b..99cc783dc0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -22,6 +22,8 @@ private enum EventingState { PROCESSED_MARK_FOR_DELETION, /** Delete event present, from this point other events are not relevant */ DELETE_EVENT_PRESENT, + // todo we probably need an additional state for the case when procesing delete event + // that fails we want to retry the delete event but meanwhile additional event is received } private final ResourceID id; From a89597362723b5ce53d702082a67b544c9486e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 09:45:47 +0200 Subject: [PATCH 06/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/ResourceState.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 99cc783dc0..586637953e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -22,8 +22,8 @@ private enum EventingState { PROCESSED_MARK_FOR_DELETION, /** Delete event present, from this point other events are not relevant */ DELETE_EVENT_PRESENT, - // todo we probably need an additional state for the case when procesing delete event - // that fails we want to retry the delete event but meanwhile additional event is received + /** */ + ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT } private final ResourceID id; From b96fff7fee29d2c124f13bb3d17c851cac1d7a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 11:18:52 +0200 Subject: [PATCH 07/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/Controller.java | 4 +++ .../processing/event/EventProcessor.java | 13 +++---- .../event/ReconciliationDispatcher.java | 4 ++- .../processing/event/ResourceState.java | 34 +++++++++++++++++-- .../event/ResourceStateManagerTest.java | 2 +- 5 files changed, 44 insertions(+), 13 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 a53d52c429..80af4fc61a 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 @@ -486,4 +486,8 @@ public boolean workflowContainsDependentForType(Class clazz) { return managedWorkflow.getDependentResourcesByName().values().stream() .anyMatch(d -> d.resourceType().equals(clazz)); } + + public boolean isCleaner() { + return isCleaner; + } } 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 baae5d5158..15c7624cb6 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 @@ -158,7 +158,7 @@ private void submitReconciliationExecution(ResourceState state) { state.setUnderProcessing(true); final var latest = maybeLatest.get(); ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); - state.unMarkEventReceived(); + state.unMarkEventReceived(controllerConfiguration.isAllEventReconcileMode()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ReconcilerExecutor(resourceID, executionScope)); @@ -205,10 +205,12 @@ private void handleEventMarking(Event event, ResourceState state) { // removed, but also the informers websocket is disconnected and later reconnected. So // meanwhile the resource could be deleted and recreated. In this case we just mark a new // event as below. - markEventReceived(state); + state.markEventReceived(); } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { - markEventReceived(state); + state.markEventReceived(); + } else if (controllerConfiguration.isAllEventReconcileMode() && state.deleteEventPresent()) { + state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { log.debug( "Skipped marking event as received. Delete event present: {}, processed mark for" @@ -218,11 +220,6 @@ private void handleEventMarking(Event event, ResourceState state) { } } - private void markEventReceived(ResourceState state) { - log.debug("Marking event received for: {}", state.getId()); - state.markEventReceived(); - } - private boolean isResourceMarkedForDeletion(ResourceEvent resourceEvent) { return resourceEvent.getResource().map(HasMetadata::isMarkedForDeletion).orElse(false); } 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 ac4d600a0e..6c09494811 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 @@ -98,7 +98,9 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { resourceForExecution, executionScope.isDeleteEvent(), executionScope.isDeleteFinalStateUnknown()); - if (markedForDeletion && !configuration().isAllEventReconcileMode()) { + + // checking the cleaner for all-event-mode + if (markedForDeletion && controller.isCleaner()) { return handleCleanup(resourceForExecution, originalResource, context); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 586637953e..a953129a8f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -1,11 +1,16 @@ package io.javaoperatorsdk.operator.processing.event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; class ResourceState { + private static final Logger log = LoggerFactory.getLogger(ResourceState.class); + /** * Manages the state of received events. Basically there can be only three distinct states * relevant for event processing. Either an event is received, so we eventually process or no @@ -76,7 +81,8 @@ public void markDeleteEventReceived( } public boolean deleteEventPresent() { - return eventing == EventingState.DELETE_EVENT_PRESENT; + return eventing == EventingState.DELETE_EVENT_PRESENT + || eventing == EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; } public boolean processedMarkForDeletionPresent() { @@ -87,10 +93,22 @@ public void markEventReceived() { if (deleteEventPresent()) { throw new IllegalStateException("Cannot receive event after a delete event received"); } + log.debug("Marking event received for: {}", getId()); eventing = EventingState.EVENT_PRESENT; } + public void markAdditionalEventAfterDeleteEvent() { + if (!deleteEventPresent()) { + throw new IllegalStateException( + "Cannot mark additional event after delete event, if in current state not delete event" + + " present"); + } + log.debug("Marking additional event after delete event: {}", getId()); + eventing = EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; + } + public void markProcessedMarkForDeletion() { + log.debug("Marking processed mark for deletion: {}", getId()); eventing = EventingState.PROCESSED_MARK_FOR_DELETION; } @@ -110,7 +128,7 @@ public HasMetadata getLastKnownResource() { return lastKnownResource; } - public void unMarkEventReceived() { + public void unMarkEventReceived(boolean isAllEventReconcileMode) { switch (eventing) { case EVENT_PRESENT: eventing = EventingState.NO_EVENT_PRESENT; @@ -118,7 +136,17 @@ public void unMarkEventReceived() { case PROCESSED_MARK_FOR_DELETION: throw new IllegalStateException("Cannot unmark processed marked for deletion."); case DELETE_EVENT_PRESENT: - throw new IllegalStateException("Cannot unmark delete event."); + if (!isAllEventReconcileMode) { + throw new IllegalStateException("Cannot unmark delete event."); + } + break; + case ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT: + if (!isAllEventReconcileMode) { + throw new IllegalStateException( + "This state should not happen in non all-event-reconciliation mode"); + } + eventing = EventingState.DELETE_EVENT_PRESENT; + break; case NO_EVENT_PRESENT: // do nothing break; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index cd5e5381fd..8bdf44705c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -86,7 +86,7 @@ public void cannotMarkEventAfterDeleteEventReceived() { public void listsResourceIDSWithEventsPresent() { state.markEventReceived(); state2.markEventReceived(); - state.unMarkEventReceived(); + state.unMarkEventReceived(false); var res = manager.resourcesWithEventPresent(); From aa91d4f63ccab55f37923444d9d2b80c2da881ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 11:32:38 +0200 Subject: [PATCH 08/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/EventProcessor.java | 23 ++++++++++++------- .../processing/event/ResourceState.java | 4 ++++ 2 files changed, 19 insertions(+), 8 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 15c7624cb6..32f1341e19 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 @@ -131,7 +131,7 @@ public synchronized void handleEvent(Event event) { } private void handleMarkedEventForResource(ResourceState state) { - if (state.deleteEventPresent() && !controllerConfiguration.isAllEventReconcileMode()) { + if (state.deleteEventPresent() && !isAllEventMode()) { cleanupForDeletedEvent(state.getId()); } else if (!state.processedMarkForDeletionPresent()) { submitReconciliationExecution(state); @@ -158,7 +158,7 @@ private void submitReconciliationExecution(ResourceState state) { state.setUnderProcessing(true); final var latest = maybeLatest.get(); ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); - state.unMarkEventReceived(controllerConfiguration.isAllEventReconcileMode()); + state.unMarkEventReceived(isAllEventMode()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ReconcilerExecutor(resourceID, executionScope)); @@ -209,7 +209,7 @@ private void handleEventMarking(Event event, ResourceState state) { } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { state.markEventReceived(); - } else if (controllerConfiguration.isAllEventReconcileMode() && state.deleteEventPresent()) { + } else if (isAllEventMode() && state.deleteEventPresent()) { state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { log.debug( @@ -252,15 +252,15 @@ synchronized void eventProcessingFinished( // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() - && (!state.deleteEventPresent() || controllerConfiguration.isAllEventReconcileMode())) { + && (!state.deleteEventPresent() || isAllEventMode())) { handleRetryOnException( executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); - if ((controllerConfiguration.isAllEventReconcileMode() && executionScope.isDeleteEvent()) - || (!controllerConfiguration.isAllEventReconcileMode() && state.deleteEventPresent())) { + if ((isAllEventMode() && executionScope.isDeleteEvent()) + || (!isAllEventMode() && state.deleteEventPresent())) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { state.markProcessedMarkForDeletion(); @@ -329,7 +329,9 @@ TimerEventSource

retryEventSource() { private void handleRetryOnException(ExecutionScope

executionScope, Exception exception) { final var state = getOrInitRetryExecution(executionScope); var resourceID = state.getId(); - boolean eventPresent = state.eventPresent(); + boolean eventPresent = + state.eventPresent() + || (isAllEventMode() && state.isAdditionalEventPresentAfterDeleteEvent()); state.markEventReceived(); retryAwareErrorLogging(state.getRetry(), eventPresent, exception, executionScope); @@ -474,7 +476,7 @@ public void run() { try { var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { - if (controllerConfiguration.isAllEventReconcileMode()) { + if (isAllEventMode()) { log.debug( "Resource not found in the cache, checking for delete event resource: {}", resourceID); @@ -529,4 +531,9 @@ public synchronized boolean isUnderProcessing(ResourceID resourceID) { public synchronized boolean isRunning() { return running; } + + // shortening + private boolean isAllEventMode() { + return controllerConfiguration.isAllEventReconcileMode(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index a953129a8f..c11d7a11dd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -85,6 +85,10 @@ public boolean deleteEventPresent() { || eventing == EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; } + public boolean isAdditionalEventPresentAfterDeleteEvent() { + return eventing == EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; + } + public boolean processedMarkForDeletionPresent() { return eventing == EventingState.PROCESSED_MARK_FOR_DELETION; } From 17ba49dc87ce92e2f0dd64a8d155383f49e16719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 14:58:57 +0200 Subject: [PATCH 09/41] Working integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/BaseConfigurationService.java | 2 +- .../api/config/ControllerConfiguration.java | 4 +- .../ControllerConfigurationOverrider.java | 10 ++-- .../operator/api/config/ControllerMode.java | 2 +- .../ResolvedControllerConfiguration.java | 4 +- .../reconciler/ControllerConfiguration.java | 2 +- .../processing/event/EventProcessor.java | 18 +++++-- .../AbstractAllEventReconciler.java | 45 ++++++++++++++++ .../cleaner/AllEventCleanerIT.java | 3 ++ .../AllEventCleanerFinalizerIT.java | 3 ++ .../finalizer/AllEventFinalizerIT.java | 3 ++ .../onlyreconcile/AllEventCustomResource.java | 13 +++++ .../onlyreconcile/AllEventIT.java | 54 +++++++++++++++++++ .../onlyreconcile/AllEventReconciler.java | 44 +++++++++++++++ .../onlyreconcile/AllEventSpec.java | 14 +++++ 15 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java 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 785cb99e75..2d2db3e954 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 @@ -304,7 +304,7 @@ private

ResolvedControllerConfiguration

controllerCon final var dependentFieldManager = fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager; - var controllerMode = annotation == null ? ControllerMode.DEFAULT : annotation.controllerMode(); + var controllerMode = annotation == null ? ControllerMode.DEFAULT : annotation.mode(); InformerConfiguration

informerConfig = InformerConfiguration.builder(resourceClass) 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 44a1fbc46d..ac34318439 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 @@ -93,11 +93,11 @@ default String fieldManager() { C getConfigurationFor(DependentResourceSpec spec); - default ControllerMode getControllerMode() { + default ControllerMode mode() { return ControllerMode.DEFAULT; } default boolean isAllEventReconcileMode() { - return getControllerMode() == ControllerMode.ALL_EVENT_RECONCILE; + return mode() == ControllerMode.RECONCILE_ALL_EVENT; } } 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 5138c666a9..bf6a1265ac 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 @@ -30,7 +30,7 @@ public class ControllerConfigurationOverrider { private Duration reconciliationMaxInterval; private Map configurations; private final InformerConfiguration.Builder config; - private ControllerMode controllerMode; + private ControllerMode mode; private ControllerConfigurationOverrider(ControllerConfiguration original) { this.finalizer = original.getFinalizerName(); @@ -43,7 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { this.rateLimiter = original.getRateLimiter(); this.name = original.getName(); this.fieldManager = original.fieldManager(); - this.controllerMode = original.getControllerMode(); + this.mode = original.mode(); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -156,8 +156,8 @@ public ControllerConfigurationOverrider withFieldManager(String dependentFiel return this; } - public ControllerConfigurationOverrider withControllerMode(ControllerMode controllerMode) { - this.controllerMode = controllerMode; + public ControllerConfigurationOverrider withMode(ControllerMode controllerMode) { + this.mode = controllerMode; return this; } @@ -205,7 +205,7 @@ public ControllerConfiguration build() { fieldManager, original.getConfigurationService(), config.buildForController(), - controllerMode, + mode, original.getWorkflowSpec().orElse(null)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java index 2e73b28c15..536cefc7cf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java @@ -2,5 +2,5 @@ public enum ControllerMode { DEFAULT, - ALL_EVENT_RECONCILE + RECONCILE_ALL_EVENT } 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 index 4880eee48c..6ca03ae91b 100644 --- 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 @@ -45,7 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration

other) { other.fieldManager(), other.getConfigurationService(), other.getInformerConfig(), - other.getControllerMode(), + other.mode(), other.getWorkflowSpec().orElse(null)); } @@ -216,7 +216,7 @@ public String fieldManager() { } @Override - public ControllerMode getControllerMode() { + public ControllerMode mode() { return controllerMode; } } 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 d235124463..a2afceca2a 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 @@ -79,5 +79,5 @@ MaxReconciliationInterval maxReconciliationInterval() default */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; - ControllerMode controllerMode() default ControllerMode.DEFAULT; + ControllerMode mode() default ControllerMode.DEFAULT; } 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 32f1341e19..62167a3993 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 @@ -144,7 +144,8 @@ private void submitReconciliationExecution(ResourceState state) { final var resourceID = state.getId(); Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); - if (!controllerUnderExecution && maybeLatest.isPresent()) { + if (!controllerUnderExecution + && (maybeLatest.isPresent() || (isAllEventMode() && state.deleteEventPresent()))) { var rateLimit = state.getRateLimit(); if (rateLimit == null) { rateLimit = rateLimiter.initState(); @@ -156,7 +157,7 @@ private void submitReconciliationExecution(ResourceState state) { return; } state.setUnderProcessing(true); - final var latest = maybeLatest.get(); + final var latest = maybeLatest.orElseGet(() -> getResourceFromState(state)); ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); state.unMarkEventReceived(isAllEventMode()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); @@ -183,6 +184,17 @@ private void submitReconciliationExecution(ResourceState state) { } } + @SuppressWarnings("unchecked") + private P getResourceFromState(ResourceState state) { + if (isAllEventMode()) { + log.debug("Getting resource from state for {}", state.getId()); + return (P) state.getLastKnownResource(); + } else { + throw new IllegalStateException( + "No resource found, this indicates issue with implementation."); + } + } + private void handleEventMarking(Event event, ResourceState state) { final var relatedCustomResourceID = event.getRelatedCustomResourceID(); if (event instanceof ResourceEvent resourceEvent) { @@ -266,7 +278,7 @@ synchronized void eventProcessingFinished( state.markProcessedMarkForDeletion(); metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { - if (state.eventPresent()) { + if (state.eventPresent() || (isAllEventMode() && state.deleteEventPresent())) { submitReconciliationExecution(state); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java new file mode 100644 index 0000000000..b9389ee9f7 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode; + +import java.util.concurrent.atomic.AtomicInteger; + +public class AbstractAllEventReconciler { + + public static final String FINALIZER = "all.event.mode/finalizer"; + + private boolean resourceEvent = false; + private boolean deleteEvent = false; + private boolean eventOnMarkedForDeletion = false; + private AtomicInteger eventCounter = new AtomicInteger(0); + + public boolean isResourceEvent() { + return resourceEvent; + } + + public void setResourceEvent(boolean resourceEvent) { + this.resourceEvent = resourceEvent; + } + + public boolean isDeleteEvent() { + return deleteEvent; + } + + public void setDeleteEvent(boolean deleteEvent) { + this.deleteEvent = deleteEvent; + } + + public boolean isEventOnMarkedForDeletion() { + return eventOnMarkedForDeletion; + } + + public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { + this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; + } + + public int getEventCounter() { + return eventCounter.get(); + } + + public void increaseEventCount() { + eventCounter.incrementAndGet(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java new file mode 100644 index 0000000000..64017329e5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -0,0 +1,3 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; + +public class AllEventCleanerIT {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java new file mode 100644 index 0000000000..512822b97c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java @@ -0,0 +1,3 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.cleanerfinalizer; + +public class AllEventCleanerFinalizerIT {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java new file mode 100644 index 0000000000..69d5cfc464 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java @@ -0,0 +1,3 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.finalizer; + +public class AllEventFinalizerIT {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java new file mode 100644 index 0000000000..b12f879d51 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; + +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("aecs") +public class AllEventCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java new file mode 100644 index 0000000000..5274348d95 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class AllEventIT { + + public static final String TEST = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new AllEventReconciler()).build(); + + @Test + void eventsPresent() { + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + extension.serverSideApply(testResource()); + + await() + .untilAsserted( + () -> { + assertThat(reconciler.isResourceEvent()).isTrue(); + assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); + }); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.isDeleteEvent()).isTrue(); + assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); + }); + } + + AllEventCustomResource getResource() { + return extension.get(AllEventCustomResource.class, TEST); + } + + AllEventCustomResource testResource() { + var res = new AllEventCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java new file mode 100644 index 0000000000..76971beaa9 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java @@ -0,0 +1,44 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; + +import io.javaoperatorsdk.operator.api.config.ControllerMode; +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.baseapi.alleventmode.AbstractAllEventReconciler; + +@ControllerConfiguration(mode = ControllerMode.RECONCILE_ALL_EVENT) +public class AllEventReconciler extends AbstractAllEventReconciler + implements Reconciler { + + @Override + public UpdateControl reconcile( + AllEventCustomResource resource, Context context) { + + increaseEventCount(); + + if (!resource.isMarkedForDeletion()) { + setResourceEvent(true); + } + + if (!resource.hasFinalizer(FINALIZER)) { + resource.addFinalizer(FINALIZER); + context.getClient().resource(resource).update(); + return UpdateControl.noUpdate(); + } + + if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { + setEventOnMarkedForDeletion(true); + if (resource.hasFinalizer(FINALIZER)) { + resource.removeFinalizer(FINALIZER); + context.getClient().resource(resource).update(); + } + } + + if (context.isDeleteEventPresent()) { + setDeleteEvent(true); + } + + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java new file mode 100644 index 0000000000..0b14423606 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; + +public class AllEventSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} From 41366ca2503cb7ba1f89e4d615c765f7d7b2fb89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 15:24:44 +0200 Subject: [PATCH 10/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/ReconciliationDispatcher.java | 35 ++++++++---- .../AbstractAllEventReconciler.java | 1 + .../AllEventCleanerCustomResource.java | 13 +++++ .../cleaner/AllEventCleanerIT.java | 55 ++++++++++++++++++- .../cleaner/AllEventCleanerReconciler.java | 54 ++++++++++++++++++ .../cleaner/AllEventCleanerSpec.java | 14 +++++ .../AllEventCleanerFinalizerIT.java | 3 - .../finalizer/AllEventFinalizerIT.java | 3 - .../onlyreconcile/AllEventIT.java | 3 +- 9 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java 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 6c09494811..69fa4101e2 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 @@ -101,7 +101,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { // checking the cleaner for all-event-mode if (markedForDeletion && controller.isCleaner()) { - return handleCleanup(resourceForExecution, originalResource, context); + return handleCleanup(resourceForExecution, originalResource, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); } @@ -166,7 +166,7 @@ private PostExecutionControl

reconcileExecution( P updatedCustomResource = null; if (useSSA) { if (updateControl.isNoUpdate()) { - return createPostExecutionControl(null, updateControl); + return createPostExecutionControl(null, updateControl, executionScope); } else { toUpdate = updateControl.getResource().orElseThrow(); } @@ -187,7 +187,7 @@ private PostExecutionControl

reconcileExecution( if (updateControl.isPatchStatus()) { customResourceFacade.patchStatus(toUpdate, originalResource); } - return createPostExecutionControl(updatedCustomResource, updateControl); + return createPostExecutionControl(updatedCustomResource, updateControl, executionScope); } private PostExecutionControl

handleErrorStatusHandler( @@ -247,7 +247,7 @@ public boolean isLastAttempt() { } private PostExecutionControl

createPostExecutionControl( - P updatedCustomResource, UpdateControl

updateControl) { + P updatedCustomResource, UpdateControl

updateControl, ExecutionScope

executionScope) { PostExecutionControl

postExecutionControl; if (updatedCustomResource != null) { postExecutionControl = @@ -255,17 +255,32 @@ private PostExecutionControl

createPostExecutionControl( } else { postExecutionControl = PostExecutionControl.defaultDispatch(); } - updatePostExecutionControlWithReschedule(postExecutionControl, updateControl); + updatePostExecutionControlWithReschedule(postExecutionControl, updateControl, executionScope); return postExecutionControl; } + // todo test private void updatePostExecutionControlWithReschedule( - PostExecutionControl

postExecutionControl, BaseControl baseControl) { - baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + PostExecutionControl

postExecutionControl, + BaseControl baseControl, + ExecutionScope

executionScope) { + baseControl + .getScheduleDelay() + .ifPresent( + r -> { + if (executionScope.isDeleteEvent()) { + log.warn("No re-schedules allowed when delete event present. Will be ignored."); + } else { + postExecutionControl.withReSchedule(r); + } + }); } private PostExecutionControl

handleCleanup( - P resourceForExecution, P originalResource, Context

context) { + P resourceForExecution, + P originalResource, + Context

context, + ExecutionScope

executionScope) { if (log.isDebugEnabled()) { log.debug( "Executing delete for resource: {} with version: {}", @@ -274,7 +289,7 @@ private PostExecutionControl

handleCleanup( } DeleteControl deleteControl = controller.cleanup(resourceForExecution, context); final var useFinalizer = controller.useFinalizer(); - if (useFinalizer) { + if (useFinalizer && !configuration().isAllEventReconcileMode()) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); @@ -308,7 +323,7 @@ private PostExecutionControl

handleCleanup( deleteControl, useFinalizer); PostExecutionControl

postExecutionControl = PostExecutionControl.defaultDispatch(); - updatePostExecutionControlWithReschedule(postExecutionControl, deleteControl); + updatePostExecutionControlWithReschedule(postExecutionControl, deleteControl, executionScope); return postExecutionControl; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java index b9389ee9f7..6c0e729366 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java @@ -5,6 +5,7 @@ public class AbstractAllEventReconciler { public static final String FINALIZER = "all.event.mode/finalizer"; + public static final String ADDITIONAL_FINALIZER = "all.event.mode/finalizer2"; private boolean resourceEvent = false; private boolean deleteEvent = false; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java new file mode 100644 index 0000000000..02e7106b67 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; + +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("aeccs") +public class AllEventCleanerCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java index 64017329e5..17f7f90992 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -1,3 +1,56 @@ package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; -public class AllEventCleanerIT {} +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile.AllEventCustomResource; +import io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile.AllEventReconciler; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class AllEventCleanerIT { + + public static final String TEST = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new AllEventCleanerReconciler()).build(); + + @Test + void eventsPresent() { + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + extension.serverSideApply(testResource()); + + await() + .untilAsserted( + () -> { + assertThat(reconciler.isResourceEvent()).isTrue(); + assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); + }); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.isDeleteEvent()).isTrue(); + assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); + }); + } + + AllEventCustomResource getResource() { + return extension.get(AllEventCustomResource.class, TEST); + } + + AllEventCustomResource testResource() { + var res = new AllEventCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java new file mode 100644 index 0000000000..1f4d3bb3ac --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; + +import io.javaoperatorsdk.operator.api.config.ControllerMode; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; + +@ControllerConfiguration(mode = ControllerMode.RECONCILE_ALL_EVENT) +public class AllEventCleanerReconciler extends AbstractAllEventReconciler + implements Reconciler, Cleaner { + + @Override + public UpdateControl reconcile( + AllEventCleanerCustomResource resource, Context context) { + + increaseEventCount(); + if (!resource.isMarkedForDeletion()) { + setResourceEvent(true); + } + + if (!resource.hasFinalizer(FINALIZER)) { + resource.addFinalizer(FINALIZER); + context.getClient().resource(resource).update(); + return UpdateControl.noUpdate(); + } + + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup( + AllEventCleanerCustomResource resource, Context context) + throws Exception { + + increaseEventCount(); + if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { + setEventOnMarkedForDeletion(true); + if (resource.hasFinalizer(FINALIZER)) { + resource.removeFinalizer(FINALIZER); + context.getClient().resource(resource).update(); + } + } + + if (context.isDeleteEventPresent()) { + setDeleteEvent(true); + } + // todo handle this document + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java new file mode 100644 index 0000000000..5b38308940 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; + +public class AllEventCleanerSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java deleted file mode 100644 index 512822b97c..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleanerfinalizer/AllEventCleanerFinalizerIT.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.cleanerfinalizer; - -public class AllEventCleanerFinalizerIT {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java deleted file mode 100644 index 69d5cfc464..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/finalizer/AllEventFinalizerIT.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.finalizer; - -public class AllEventFinalizerIT {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index 5274348d95..d8d0200a02 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -18,11 +18,12 @@ public class AllEventIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder().withReconciler(new AllEventReconciler()).build(); + // todo additional finalizer + // todo retry @Test void eventsPresent() { var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); extension.serverSideApply(testResource()); - await() .untilAsserted( () -> { From f6283d725e9252dde7bf6c4c831c00ef6af68d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 15:26:59 +0200 Subject: [PATCH 11/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../cleaner/AllEventCleanerCustomResource.java | 2 +- .../alleventmode/cleaner/AllEventCleanerIT.java | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java index 02e7106b67..ff244cc371 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java @@ -8,6 +8,6 @@ @Group("sample.javaoperatorsdk") @Version("v1") -@ShortNames("aeccs") +@ShortNames("eccs") public class AllEventCleanerCustomResource extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java index 17f7f90992..07026064ad 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -4,8 +4,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile.AllEventCustomResource; -import io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile.AllEventReconciler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; @@ -22,7 +20,7 @@ public class AllEventCleanerIT { @Test void eventsPresent() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); extension.serverSideApply(testResource()); await() @@ -44,12 +42,12 @@ void eventsPresent() { }); } - AllEventCustomResource getResource() { - return extension.get(AllEventCustomResource.class, TEST); + AllEventCleanerCustomResource getResource() { + return extension.get(AllEventCleanerCustomResource.class, TEST); } - AllEventCustomResource testResource() { - var res = new AllEventCustomResource(); + AllEventCleanerCustomResource testResource() { + var res = new AllEventCleanerCustomResource(); res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); return res; } From 035861f6aade9cdde8244c8080a542771932b24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 16:06:40 +0200 Subject: [PATCH 12/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../baseapi/alleventmode/onlyreconcile/AllEventIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index d8d0200a02..8b5684df93 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -18,8 +18,8 @@ public class AllEventIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder().withReconciler(new AllEventReconciler()).build(); - // todo additional finalizer - // todo retry + // todo additional finalizer, events after that + // todo retry on delete event + event received meanwhile @Test void eventsPresent() { var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); From 7c0b0df60d819c50f28faf50cce52050cec804ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 16:09:04 +0200 Subject: [PATCH 13/41] delete notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- notes.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 notes.txt diff --git a/notes.txt b/notes.txt deleted file mode 100644 index 253d135578..0000000000 --- a/notes.txt +++ /dev/null @@ -1 +0,0 @@ -- check that Cleaner interface is not present From 044bfbef88f9558d85b0070e6678713630abc8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 16:30:53 +0200 Subject: [PATCH 14/41] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/ReconciliationDispatcher.java | 12 ++++++++---- .../mysql-schema/src/main/resources/log4j2.xml | 2 +- 2 files changed, 9 insertions(+), 5 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 69fa4101e2..81fdee1f8f 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 @@ -81,7 +81,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) originalResource.getMetadata().getNamespace()); final var markedForDeletion = originalResource.isMarkedForDeletion(); - if (!configuration().isAllEventReconcileMode() + if (!isAllEventMode() && markedForDeletion && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { log.debug( @@ -100,7 +100,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { executionScope.isDeleteFinalStateUnknown()); // checking the cleaner for all-event-mode - if (markedForDeletion && controller.isCleaner()) { + if ((!isAllEventMode() && markedForDeletion) || (isAllEventMode() && controller.isCleaner())) { return handleCleanup(resourceForExecution, originalResource, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); @@ -119,7 +119,7 @@ private PostExecutionControl

handleReconcile( P originalResource, Context

context) throws Exception { - if (!configuration().isAllEventReconcileMode() + if (!isAllEventMode() && controller.useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* @@ -289,7 +289,7 @@ private PostExecutionControl

handleCleanup( } DeleteControl deleteControl = controller.cleanup(resourceForExecution, context); final var useFinalizer = controller.useFinalizer(); - if (useFinalizer && !configuration().isAllEventReconcileMode()) { + if (useFinalizer && !isAllEventMode()) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); @@ -535,4 +535,8 @@ private Resource resource(R resource) { : resourceOperation.resource(resource); } } + + private boolean isAllEventMode() { + return configuration().isAllEventReconcileMode(); + } } diff --git a/sample-operators/mysql-schema/src/main/resources/log4j2.xml b/sample-operators/mysql-schema/src/main/resources/log4j2.xml index 01484221f9..5ab4735126 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 @@ - + From 0844e57610af698837ea74f64a80ba4ce9f4f702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 16:57:05 +0200 Subject: [PATCH 15/41] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/ReconciliationDispatcher.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 81fdee1f8f..26586fc6ac 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 @@ -100,7 +100,10 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { executionScope.isDeleteFinalStateUnknown()); // checking the cleaner for all-event-mode - if ((!isAllEventMode() && markedForDeletion) || (isAllEventMode() && controller.isCleaner())) { + if ((!isAllEventMode() && markedForDeletion) + || (isAllEventMode() + && controller.isCleaner() + && (markedForDeletion || executionScope.isDeleteEvent()))) { return handleCleanup(resourceForExecution, originalResource, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); From c93555b3560d1a974d8988cb658b5862d83a49a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 21 Aug 2025 17:13:59 +0200 Subject: [PATCH 16/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../AbstractAllEventReconciler.java | 51 +++++++++++++++---- .../cleaner/AllEventCleanerIT.java | 5 +- .../cleaner/AllEventCleanerReconciler.java | 4 +- .../onlyreconcile/AllEventIT.java | 46 ++++++++++++++++- .../onlyreconcile/AllEventReconciler.java | 15 ++++-- 5 files changed, 100 insertions(+), 21 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java index 6c0e729366..6eb9c01eba 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java @@ -7,25 +7,30 @@ public class AbstractAllEventReconciler { public static final String FINALIZER = "all.event.mode/finalizer"; public static final String ADDITIONAL_FINALIZER = "all.event.mode/finalizer2"; - private boolean resourceEvent = false; - private boolean deleteEvent = false; + protected volatile boolean useFinalizer = true; + protected volatile boolean throwExceptionOnFirstDeleteEvent = false; + protected volatile boolean isFirstDeleteEvent = true; + + private boolean resourceEventPresent = false; + private boolean deleteEventPresent = false; private boolean eventOnMarkedForDeletion = false; - private AtomicInteger eventCounter = new AtomicInteger(0); - public boolean isResourceEvent() { - return resourceEvent; + private final AtomicInteger eventCounter = new AtomicInteger(0); + + public boolean isResourceEventPresent() { + return resourceEventPresent; } - public void setResourceEvent(boolean resourceEvent) { - this.resourceEvent = resourceEvent; + public void setResourceEventPresent(boolean resourceEventPresent) { + this.resourceEventPresent = resourceEventPresent; } - public boolean isDeleteEvent() { - return deleteEvent; + public boolean isDeleteEventPresent() { + return deleteEventPresent; } - public void setDeleteEvent(boolean deleteEvent) { - this.deleteEvent = deleteEvent; + public void setDeleteEventPresent(boolean deleteEventPresent) { + this.deleteEventPresent = deleteEventPresent; } public boolean isEventOnMarkedForDeletion() { @@ -43,4 +48,28 @@ public int getEventCounter() { public void increaseEventCount() { eventCounter.incrementAndGet(); } + + public boolean getUseFinalizer() { + return useFinalizer; + } + + public void setUseFinalizer(boolean useFinalizer) { + this.useFinalizer = useFinalizer; + } + + public boolean isFirstDeleteEvent() { + return isFirstDeleteEvent; + } + + public void setFirstDeleteEvent(boolean firstDeleteEvent) { + isFirstDeleteEvent = firstDeleteEvent; + } + + public boolean isThrowExceptionOnFirstDeleteEvent() { + return throwExceptionOnFirstDeleteEvent; + } + + public void setThrowExceptionOnFirstDeleteEvent(boolean throwExceptionOnFirstDeleteEvent) { + this.throwExceptionOnFirstDeleteEvent = throwExceptionOnFirstDeleteEvent; + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java index 07026064ad..56aef50231 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -18,6 +18,7 @@ public class AllEventCleanerIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder().withReconciler(new AllEventCleanerReconciler()).build(); + // todo delete event without finalizer @Test void eventsPresent() { var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); @@ -26,7 +27,7 @@ void eventsPresent() { await() .untilAsserted( () -> { - assertThat(reconciler.isResourceEvent()).isTrue(); + assertThat(reconciler.isResourceEventPresent()).isTrue(); assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); }); @@ -37,7 +38,7 @@ void eventsPresent() { () -> { var r = getResource(); assertThat(r).isNull(); - assertThat(reconciler.isDeleteEvent()).isTrue(); + assertThat(reconciler.isDeleteEventPresent()).isTrue(); assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); }); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java index 1f4d3bb3ac..eb57972f64 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java @@ -19,7 +19,7 @@ public UpdateControl reconcile( increaseEventCount(); if (!resource.isMarkedForDeletion()) { - setResourceEvent(true); + setResourceEventPresent(true); } if (!resource.hasFinalizer(FINALIZER)) { @@ -46,7 +46,7 @@ public DeleteControl cleanup( } if (context.isDeleteEventPresent()) { - setDeleteEvent(true); + setDeleteEventPresent(true); } // todo handle this document return DeleteControl.defaultDelete(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index 8b5684df93..04a1d901d5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -27,7 +28,7 @@ void eventsPresent() { await() .untilAsserted( () -> { - assertThat(reconciler.isResourceEvent()).isTrue(); + assertThat(reconciler.isResourceEventPresent()).isTrue(); assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); }); @@ -38,11 +39,52 @@ void eventsPresent() { () -> { var r = getResource(); assertThat(r).isNull(); - assertThat(reconciler.isDeleteEvent()).isTrue(); + assertThat(reconciler.isDeleteEventPresent()).isTrue(); assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); }); } + @Test + void deleteEventPresentWithoutFinalizer() { + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + reconciler.setUseFinalizer(false); + extension.serverSideApply(testResource()); + + await().untilAsserted(() -> assertThat(reconciler.isResourceEventPresent()).isTrue()); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.isDeleteEventPresent()).isTrue(); + }); + } + + @Disabled("fix") + @Test + void retriesExceptionOnDeleteEvent() { + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + reconciler.setUseFinalizer(false); + reconciler.setThrowExceptionOnFirstDeleteEvent(true); + + extension.serverSideApply(testResource()); + + await().untilAsserted(() -> assertThat(reconciler.isResourceEventPresent()).isTrue()); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.isDeleteEventPresent()).isTrue(); + }); + } + AllEventCustomResource getResource() { return extension.get(AllEventCustomResource.class, TEST); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java index 76971beaa9..b24bfeab39 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java @@ -18,10 +18,10 @@ public UpdateControl reconcile( increaseEventCount(); if (!resource.isMarkedForDeletion()) { - setResourceEvent(true); + setResourceEventPresent(true); } - if (!resource.hasFinalizer(FINALIZER)) { + if (getUseFinalizer() && !resource.hasFinalizer(FINALIZER)) { resource.addFinalizer(FINALIZER); context.getClient().resource(resource).update(); return UpdateControl.noUpdate(); @@ -29,14 +29,21 @@ public UpdateControl reconcile( if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { setEventOnMarkedForDeletion(true); - if (resource.hasFinalizer(FINALIZER)) { + if (getUseFinalizer() && resource.hasFinalizer(FINALIZER)) { resource.removeFinalizer(FINALIZER); context.getClient().resource(resource).update(); } } + if (context.isDeleteEventPresent() + && isFirstDeleteEvent() + && isThrowExceptionOnFirstDeleteEvent()) { + isFirstDeleteEvent = false; + throw new RuntimeException("On purpose exception"); + } + if (context.isDeleteEventPresent()) { - setDeleteEvent(true); + setDeleteEventPresent(true); } return UpdateControl.noUpdate(); From 5e23e221138f5faadc83709cc1917a16990d1f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Sep 2025 13:05:59 +0200 Subject: [PATCH 17/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventProcessor.java | 6 +++--- .../operator/processing/event/ResourceState.java | 10 +++++++--- .../processing/event/ResourceStateManagerTest.java | 12 ++++++------ .../alleventmode/onlyreconcile/AllEventIT.java | 2 -- 4 files changed, 16 insertions(+), 14 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 62167a3993..55ed95d68b 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 @@ -217,10 +217,10 @@ private void handleEventMarking(Event event, ResourceState state) { // removed, but also the informers websocket is disconnected and later reconnected. So // meanwhile the resource could be deleted and recreated. In this case we just mark a new // event as below. - state.markEventReceived(); + state.markEventReceived(isAllEventMode()); } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { - state.markEventReceived(); + state.markEventReceived(isAllEventMode()); } else if (isAllEventMode() && state.deleteEventPresent()) { state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { @@ -344,7 +344,7 @@ private void handleRetryOnException(ExecutionScope

executionScope, Exception boolean eventPresent = state.eventPresent() || (isAllEventMode() && state.isAdditionalEventPresentAfterDeleteEvent()); - state.markEventReceived(); + state.markEventReceived(isAllEventMode()); retryAwareErrorLogging(state.getRetry(), eventPresent, exception, executionScope); if (eventPresent) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index c11d7a11dd..b7636818fa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -93,12 +93,16 @@ public boolean processedMarkForDeletionPresent() { return eventing == EventingState.PROCESSED_MARK_FOR_DELETION; } - public void markEventReceived() { - if (deleteEventPresent()) { + public void markEventReceived(boolean isAllEventMode) { + if (!isAllEventMode && deleteEventPresent()) { throw new IllegalStateException("Cannot receive event after a delete event received"); } log.debug("Marking event received for: {}", getId()); - eventing = EventingState.EVENT_PRESENT; + if (eventing == EventingState.DELETE_EVENT_PRESENT) { + eventing = EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; + } else { + eventing = EventingState.EVENT_PRESENT; + } } public void markAdditionalEventAfterDeleteEvent() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index 8bdf44705c..c0c7c7d92d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -36,7 +36,7 @@ public void returnsNoEventPresentIfNotMarkedYet() { @Test public void marksEvent() { - state.markEventReceived(); + state.markEventReceived(false); assertThat(state.eventPresent()).isTrue(); assertThat(state.deleteEventPresent()).isFalse(); @@ -52,7 +52,7 @@ public void marksDeleteEvent() { @Test public void afterDeleteEventMarkEventIsNotRelevant() { - state.markEventReceived(); + state.markEventReceived(false); state.markDeleteEventReceived(TestUtils.testCustomResource(), true); @@ -62,7 +62,7 @@ public void afterDeleteEventMarkEventIsNotRelevant() { @Test public void cleansUp() { - state.markEventReceived(); + state.markEventReceived(false); state.markDeleteEventReceived(TestUtils.testCustomResource(), true); manager.remove(sampleResourceID); @@ -78,14 +78,14 @@ public void cannotMarkEventAfterDeleteEventReceived() { IllegalStateException.class, () -> { state.markDeleteEventReceived(TestUtils.testCustomResource(), true); - state.markEventReceived(); + state.markEventReceived(false); }); } @Test public void listsResourceIDSWithEventsPresent() { - state.markEventReceived(); - state2.markEventReceived(); + state.markEventReceived(false); + state2.markEventReceived(false); state.unMarkEventReceived(false); var res = manager.resourcesWithEventPresent(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index 04a1d901d5..a87826e327 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -63,7 +62,6 @@ void deleteEventPresentWithoutFinalizer() { }); } - @Disabled("fix") @Test void retriesExceptionOnDeleteEvent() { var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); From 0aae1fb73121931a9d3db773dc9fc7768c128352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Sep 2025 13:09:33 +0200 Subject: [PATCH 18/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../baseapi/alleventmode/onlyreconcile/AllEventIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index a87826e327..e9616778e6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -83,6 +83,9 @@ void retriesExceptionOnDeleteEvent() { }); } + @Test + void eventReceivedOnDeleteEventRetry() {} + AllEventCustomResource getResource() { return extension.get(AllEventCustomResource.class, TEST); } From f87fd8c6dee5fe51292070dc56fb8f1b52d536e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Sep 2025 16:18:51 +0200 Subject: [PATCH 19/41] Finalizer utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/FinalizerUtils.java | 48 +++++++++++++++++++ .../onlyreconcile/AllEventReconciler.java | 7 ++- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java new file mode 100644 index 0000000000..5f40675d96 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class FinalizerUtils { + + private static final Logger log = LoggerFactory.getLogger(FinalizerUtils.class); + + // todo SSA + + public static

P patchFinalizer( + P resource, String finalizer, Context

context) { + return PrimaryUpdateAndCacheUtils.updateAndCacheResource( + resource, + context, + r -> r, + r -> + context + .getClient() + .resource(r) + .edit( + res -> { + res.addFinalizer(finalizer); + return res; + })); + } + + public static

P removeFinalizer( + P resource, String finalizer, Context

context) { + + return PrimaryUpdateAndCacheUtils.updateAndCacheResource( + resource, + context, + r -> r, + r -> + context + .getClient() + .resource(r) + .edit( + res -> { + res.removeFinalizer(finalizer); + return res; + })); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java index b24bfeab39..7588030ca0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java @@ -3,6 +3,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.FinalizerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; @@ -22,16 +23,14 @@ public UpdateControl reconcile( } if (getUseFinalizer() && !resource.hasFinalizer(FINALIZER)) { - resource.addFinalizer(FINALIZER); - context.getClient().resource(resource).update(); + FinalizerUtils.patchFinalizer(resource, FINALIZER, context); return UpdateControl.noUpdate(); } if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { setEventOnMarkedForDeletion(true); if (getUseFinalizer() && resource.hasFinalizer(FINALIZER)) { - resource.removeFinalizer(FINALIZER); - context.getClient().resource(resource).update(); + FinalizerUtils.removeFinalizer(resource, FINALIZER, context); } } From 281020b61c03da33d8ee3663c6166d6e2e47cd73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Sep 2025 13:10:05 +0200 Subject: [PATCH 20/41] tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/FinalizerUtils.java | 13 +- .../junit/AbstractOperatorExtension.java | 10 +- .../AbstractAllEventReconciler.java | 41 ++++- .../cleaner/AllEventCleanerIT.java | 1 - .../cleaner/AllEventCleanerReconciler.java | 14 +- .../onlyreconcile/AllEventIT.java | 151 +++++++++++++++++- .../onlyreconcile/AllEventReconciler.java | 45 ++++-- 7 files changed, 248 insertions(+), 27 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java index 5f40675d96..4b7ddc6c4c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java @@ -9,10 +9,16 @@ public class FinalizerUtils { private static final Logger log = LoggerFactory.getLogger(FinalizerUtils.class); - // todo SSA + // todo SSA, revisit if informer is ok for this public static

P patchFinalizer( P resource, String finalizer, Context

context) { + + if (resource.hasFinalizer(finalizer)) { + log.debug("Skipping adding finalizer, since already present."); + return resource; + } + return PrimaryUpdateAndCacheUtils.updateAndCacheResource( resource, context, @@ -30,7 +36,10 @@ public static

P patchFinalizer( public static

P removeFinalizer( P resource, String finalizer, Context

context) { - + if (!resource.hasFinalizer(finalizer)) { + log.debug("Skipping removing finalizer, since not present."); + return resource; + } return PrimaryUpdateAndCacheUtils.updateAndCacheResource( resource, context, diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 0ebcef2d5c..ee3509c2e5 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -20,6 +20,7 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.Utils; @@ -126,8 +127,15 @@ public T serverSideApply(T resource) { return kubernetesClient.resource(resource).inNamespace(namespace).serverSideApply(); } + public T update(T resource) { + return kubernetesClient.resource(resource).inNamespace(namespace).update(); + } + public T replace(T resource) { - return kubernetesClient.resource(resource).inNamespace(namespace).replace(); + return kubernetesClient + .resource(resource) + .inNamespace(namespace) + .createOr(NonDeletingOperation::update); } public boolean delete(T resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java index 6eb9c01eba..db5535f0df 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java @@ -6,9 +6,16 @@ public class AbstractAllEventReconciler { public static final String FINALIZER = "all.event.mode/finalizer"; public static final String ADDITIONAL_FINALIZER = "all.event.mode/finalizer2"; + public static final String NO_MORE_EXCEPTION_ANNOTATION_KEY = "no.more.exception"; protected volatile boolean useFinalizer = true; protected volatile boolean throwExceptionOnFirstDeleteEvent = false; + protected volatile boolean throwExceptionIfNoAnnotation = false; + + protected volatile boolean waitAfterFirstRetry = false; + protected volatile boolean continuerOnRetryWait = false; + protected volatile boolean waiting = false; + protected volatile boolean isFirstDeleteEvent = true; private boolean resourceEventPresent = false; @@ -41,7 +48,7 @@ public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; } - public int getEventCounter() { + public int getEventCount() { return eventCounter.get(); } @@ -72,4 +79,36 @@ public boolean isThrowExceptionOnFirstDeleteEvent() { public void setThrowExceptionOnFirstDeleteEvent(boolean throwExceptionOnFirstDeleteEvent) { this.throwExceptionOnFirstDeleteEvent = throwExceptionOnFirstDeleteEvent; } + + public boolean isThrowExceptionIfNoAnnotation() { + return throwExceptionIfNoAnnotation; + } + + public void setThrowExceptionIfNoAnnotation(boolean throwExceptionIfNoAnnotation) { + this.throwExceptionIfNoAnnotation = throwExceptionIfNoAnnotation; + } + + public boolean isWaitAfterFirstRetry() { + return waitAfterFirstRetry; + } + + public void setWaitAfterFirstRetry(boolean waitAfterFirstRetry) { + this.waitAfterFirstRetry = waitAfterFirstRetry; + } + + public boolean isContinuerOnRetryWait() { + return continuerOnRetryWait; + } + + public void setContinuerOnRetryWait(boolean continuerOnRetryWait) { + this.continuerOnRetryWait = continuerOnRetryWait; + } + + public boolean isWaiting() { + return waiting; + } + + public void setWaiting(boolean waiting) { + this.waiting = waiting; + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java index 56aef50231..989d2a6ddc 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -18,7 +18,6 @@ public class AllEventCleanerIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder().withReconciler(new AllEventCleanerReconciler()).build(); - // todo delete event without finalizer @Test void eventsPresent() { var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java index eb57972f64..4772e7c250 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java @@ -5,6 +5,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.FinalizerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; @@ -15,16 +16,15 @@ public class AllEventCleanerReconciler extends AbstractAllEventReconciler @Override public UpdateControl reconcile( - AllEventCleanerCustomResource resource, Context context) { + AllEventCleanerCustomResource primary, Context context) { increaseEventCount(); - if (!resource.isMarkedForDeletion()) { + if (!primary.isMarkedForDeletion()) { setResourceEventPresent(true); } - if (!resource.hasFinalizer(FINALIZER)) { - resource.addFinalizer(FINALIZER); - context.getClient().resource(resource).update(); + if (useFinalizer && !primary.hasFinalizer(FINALIZER)) { + FinalizerUtils.patchFinalizer(primary, FINALIZER, context); return UpdateControl.noUpdate(); } @@ -40,15 +40,13 @@ public DeleteControl cleanup( if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { setEventOnMarkedForDeletion(true); if (resource.hasFinalizer(FINALIZER)) { - resource.removeFinalizer(FINALIZER); - context.getClient().resource(resource).update(); + FinalizerUtils.removeFinalizer(resource, FINALIZER, context); } } if (context.isDeleteEventPresent()) { setDeleteEventPresent(true); } - // todo handle this document return DeleteControl.defaultDelete(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java index e9616778e6..0e8da2f8af 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java @@ -1,25 +1,38 @@ package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.ADDITIONAL_FINALIZER; import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; public class AllEventIT { public static final String TEST = "test1"; + public static final int MAX_RETRY_ATTEMPTS = 2; @RegisterExtension LocallyRunOperatorExtension extension = - LocallyRunOperatorExtension.builder().withReconciler(new AllEventReconciler()).build(); + LocallyRunOperatorExtension.builder() + .withReconciler( + new AllEventReconciler(), + o -> + o.withRetry( + new GenericRetry() + .setInitialInterval(800) + .setMaxAttempts(MAX_RETRY_ATTEMPTS) + .setIntervalMultiplier(1))) + .build(); - // todo additional finalizer, events after that - // todo retry on delete event + event received meanwhile @Test void eventsPresent() { var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); @@ -84,7 +97,137 @@ void retriesExceptionOnDeleteEvent() { } @Test - void eventReceivedOnDeleteEventRetry() {} + void additionalFinalizer() { + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + reconciler.setUseFinalizer(true); + var res = testResource(); + res.addFinalizer(ADDITIONAL_FINALIZER); + + extension.create(res); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNotNull(); + assertThat(r.getMetadata().getFinalizers()).containsExactly(ADDITIONAL_FINALIZER); + }); + var eventCount = reconciler.getEventCount(); + + res = getResource(); + res.removeFinalizer(ADDITIONAL_FINALIZER); + extension.update(res); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.getEventCount()).isEqualTo(eventCount + 1); + }); + } + + @Test + void additionalEventDuringRetryOnDeleteEvent() { + + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + reconciler.setThrowExceptionIfNoAnnotation(true); + reconciler.setWaitAfterFirstRetry(true); + var res = testResource(); + res.addFinalizer(ADDITIONAL_FINALIZER); + extension.create(res); + extension.delete(getResource()); + + await() + .pollDelay(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.getEventCount()).isGreaterThan(2); + }); + var eventCount = reconciler.getEventCount(); + + await() + .untilAsserted( + () -> { + assertThat(reconciler.isWaiting()); + }); + + res = getResource(); + res.getMetadata().getAnnotations().put("my-annotation", "true"); + extension.update(res); + reconciler.setContinuerOnRetryWait(true); + + await() + .pollDelay(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.getEventCount()).isEqualTo(eventCount + 1); + }); + + // second retry + await() + .pollDelay(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.getEventCount()).isEqualTo(eventCount + 2); + }); + + addNoMoreExceptionAnnotation(); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r.getMetadata().getFinalizers()).doesNotContain(FINALIZER); + }); + + removeAdditionalFinalizerWaitForResourceDeletion(); + } + + @Test + void additionalEventAfterExhaustedRetry() { + + var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + reconciler.setThrowExceptionIfNoAnnotation(true); + var res = testResource(); + res.addFinalizer(ADDITIONAL_FINALIZER); + extension.create(res); + extension.delete(getResource()); + + await() + .pollDelay(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.getEventCount()).isEqualTo(MAX_RETRY_ATTEMPTS + 1); + }); + + addNoMoreExceptionAnnotation(); + + await() + .pollDelay(Duration.ofMillis(30)) + .untilAsserted( + () -> { + assertThat(reconciler.getEventCount()).isGreaterThan(MAX_RETRY_ATTEMPTS + 1); + }); + + removeAdditionalFinalizerWaitForResourceDeletion(); + } + + private void removeAdditionalFinalizerWaitForResourceDeletion() { + var res = getResource(); + res.removeFinalizer(ADDITIONAL_FINALIZER); + extension.update(res); + await().untilAsserted(() -> assertThat(getResource()).isNull()); + } + + private void addNoMoreExceptionAnnotation() { + AllEventCustomResource res; + res = getResource(); + res.getMetadata().getAnnotations().put(NO_MORE_EXCEPTION_ANNOTATION_KEY, "true"); + extension.update(res); + } AllEventCustomResource getResource() { return extension.get(AllEventCustomResource.class, TEST); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java index 7588030ca0..6604b0993e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java @@ -1,5 +1,8 @@ package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -8,29 +11,51 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; -@ControllerConfiguration(mode = ControllerMode.RECONCILE_ALL_EVENT) +@ControllerConfiguration( + mode = ControllerMode.RECONCILE_ALL_EVENT, + generationAwareEventProcessing = false) public class AllEventReconciler extends AbstractAllEventReconciler implements Reconciler { + private static final Logger log = LoggerFactory.getLogger(AllEventReconciler.class); + @Override public UpdateControl reconcile( - AllEventCustomResource resource, Context context) { - + AllEventCustomResource primary, Context context) + throws InterruptedException { + log.info("Reconciling"); increaseEventCount(); - if (!resource.isMarkedForDeletion()) { + if (!primary.isMarkedForDeletion()) { setResourceEventPresent(true); } - if (getUseFinalizer() && !resource.hasFinalizer(FINALIZER)) { - FinalizerUtils.patchFinalizer(resource, FINALIZER, context); + if (!primary.isMarkedForDeletion() && getUseFinalizer() && !primary.hasFinalizer(FINALIZER)) { + log.info("Adding finalizer"); + FinalizerUtils.patchFinalizer(primary, FINALIZER, context); return UpdateControl.noUpdate(); } - if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { + if (waitAfterFirstRetry + && context.getRetryInfo().isPresent() + && context.getRetryInfo().orElseThrow().getAttemptCount() == 1) { + waiting = true; + while (!continuerOnRetryWait) { + Thread.sleep(50); + } + waiting = false; + } + + if (throwExceptionIfNoAnnotation + && !primary.getMetadata().getAnnotations().containsKey(NO_MORE_EXCEPTION_ANNOTATION_KEY)) { + throw new RuntimeException("On purpose exception for missing annotation"); + } + + if (primary.isMarkedForDeletion() && !context.isDeleteEventPresent()) { setEventOnMarkedForDeletion(true); - if (getUseFinalizer() && resource.hasFinalizer(FINALIZER)) { - FinalizerUtils.removeFinalizer(resource, FINALIZER, context); + if (getUseFinalizer() && primary.hasFinalizer(FINALIZER)) { + log.info("Removing finalizer"); + FinalizerUtils.removeFinalizer(primary, FINALIZER, context); } } @@ -44,7 +69,7 @@ && isThrowExceptionOnFirstDeleteEvent()) { if (context.isDeleteEventPresent()) { setDeleteEventPresent(true); } - + log.info("Reconciliation finished"); return UpdateControl.noUpdate(); } } From 29f4e8bf0663e9d720a74824c17eeafb0abfe640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Sep 2025 13:14:40 +0200 Subject: [PATCH 21/41] test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../cleaner/AllEventCleanerIT.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java index 989d2a6ddc..584b8296a3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java @@ -20,6 +20,31 @@ public class AllEventCleanerIT { @Test void eventsPresent() { + var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); + reconciler.setUseFinalizer(true); + extension.serverSideApply(testResource()); + + await() + .untilAsserted( + () -> { + assertThat(reconciler.isResourceEventPresent()).isTrue(); + assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); + }); + + extension.delete(getResource()); + + await() + .untilAsserted( + () -> { + var r = getResource(); + assertThat(r).isNull(); + assertThat(reconciler.isDeleteEventPresent()).isTrue(); + assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); + }); + } + + @Test + void deleteEventPresentWithoutFinalizer() { var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); extension.serverSideApply(testResource()); From b6c37543abfa501f17f45ce0ab62f59a039a56b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Sep 2025 13:22:50 +0200 Subject: [PATCH 22/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/ResourceStateManagerTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java index c0c7c7d92d..626b833c38 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ResourceStateManagerTest.java @@ -8,8 +8,6 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; -import io.javaoperatorsdk.operator.TestUtils; - import static org.assertj.core.api.Assertions.assertThat; class ResourceStateManagerTest { From 937222fd04daf6d850a8507a2a7c3c8543eae533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Sep 2025 13:29:45 +0200 Subject: [PATCH 23/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../AbstractAllEventReconciler.java | 84 ++++--------------- .../onlyreconcile/AllEventReconciler.java | 57 +++++++++++++ 2 files changed, 71 insertions(+), 70 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java index db5535f0df..df36e250ad 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java @@ -9,44 +9,12 @@ public class AbstractAllEventReconciler { public static final String NO_MORE_EXCEPTION_ANNOTATION_KEY = "no.more.exception"; protected volatile boolean useFinalizer = true; - protected volatile boolean throwExceptionOnFirstDeleteEvent = false; - protected volatile boolean throwExceptionIfNoAnnotation = false; - protected volatile boolean waitAfterFirstRetry = false; - protected volatile boolean continuerOnRetryWait = false; - protected volatile boolean waiting = false; - - protected volatile boolean isFirstDeleteEvent = true; + private final AtomicInteger eventCounter = new AtomicInteger(0); - private boolean resourceEventPresent = false; private boolean deleteEventPresent = false; private boolean eventOnMarkedForDeletion = false; - - private final AtomicInteger eventCounter = new AtomicInteger(0); - - public boolean isResourceEventPresent() { - return resourceEventPresent; - } - - public void setResourceEventPresent(boolean resourceEventPresent) { - this.resourceEventPresent = resourceEventPresent; - } - - public boolean isDeleteEventPresent() { - return deleteEventPresent; - } - - public void setDeleteEventPresent(boolean deleteEventPresent) { - this.deleteEventPresent = deleteEventPresent; - } - - public boolean isEventOnMarkedForDeletion() { - return eventOnMarkedForDeletion; - } - - public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { - this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; - } + private boolean resourceEventPresent = false; public int getEventCount() { return eventCounter.get(); @@ -64,51 +32,27 @@ public void setUseFinalizer(boolean useFinalizer) { this.useFinalizer = useFinalizer; } - public boolean isFirstDeleteEvent() { - return isFirstDeleteEvent; - } - - public void setFirstDeleteEvent(boolean firstDeleteEvent) { - isFirstDeleteEvent = firstDeleteEvent; - } - - public boolean isThrowExceptionOnFirstDeleteEvent() { - return throwExceptionOnFirstDeleteEvent; - } - - public void setThrowExceptionOnFirstDeleteEvent(boolean throwExceptionOnFirstDeleteEvent) { - this.throwExceptionOnFirstDeleteEvent = throwExceptionOnFirstDeleteEvent; - } - - public boolean isThrowExceptionIfNoAnnotation() { - return throwExceptionIfNoAnnotation; - } - - public void setThrowExceptionIfNoAnnotation(boolean throwExceptionIfNoAnnotation) { - this.throwExceptionIfNoAnnotation = throwExceptionIfNoAnnotation; - } - - public boolean isWaitAfterFirstRetry() { - return waitAfterFirstRetry; + public boolean isDeleteEventPresent() { + return deleteEventPresent; } - public void setWaitAfterFirstRetry(boolean waitAfterFirstRetry) { - this.waitAfterFirstRetry = waitAfterFirstRetry; + public void setDeleteEventPresent(boolean deleteEventPresent) { + this.deleteEventPresent = deleteEventPresent; } - public boolean isContinuerOnRetryWait() { - return continuerOnRetryWait; + public boolean isEventOnMarkedForDeletion() { + return eventOnMarkedForDeletion; } - public void setContinuerOnRetryWait(boolean continuerOnRetryWait) { - this.continuerOnRetryWait = continuerOnRetryWait; + public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { + this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; } - public boolean isWaiting() { - return waiting; + public boolean isResourceEventPresent() { + return resourceEventPresent; } - public void setWaiting(boolean waiting) { - this.waiting = waiting; + public void setResourceEventPresent(boolean resourceEventPresent) { + this.resourceEventPresent = resourceEventPresent; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java index 6604b0993e..166589e401 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java @@ -19,6 +19,15 @@ public class AllEventReconciler extends AbstractAllEventReconciler private static final Logger log = LoggerFactory.getLogger(AllEventReconciler.class); + private volatile boolean throwExceptionOnFirstDeleteEvent = false; + private volatile boolean throwExceptionIfNoAnnotation = false; + + private volatile boolean waitAfterFirstRetry = false; + private volatile boolean continuerOnRetryWait = false; + private volatile boolean waiting = false; + + private volatile boolean isFirstDeleteEvent = true; + @Override public UpdateControl reconcile( AllEventCustomResource primary, Context context) @@ -72,4 +81,52 @@ && isThrowExceptionOnFirstDeleteEvent()) { log.info("Reconciliation finished"); return UpdateControl.noUpdate(); } + + public boolean isFirstDeleteEvent() { + return isFirstDeleteEvent; + } + + public void setFirstDeleteEvent(boolean firstDeleteEvent) { + isFirstDeleteEvent = firstDeleteEvent; + } + + public boolean isThrowExceptionOnFirstDeleteEvent() { + return throwExceptionOnFirstDeleteEvent; + } + + public void setThrowExceptionOnFirstDeleteEvent(boolean throwExceptionOnFirstDeleteEvent) { + this.throwExceptionOnFirstDeleteEvent = throwExceptionOnFirstDeleteEvent; + } + + public boolean isThrowExceptionIfNoAnnotation() { + return throwExceptionIfNoAnnotation; + } + + public void setThrowExceptionIfNoAnnotation(boolean throwExceptionIfNoAnnotation) { + this.throwExceptionIfNoAnnotation = throwExceptionIfNoAnnotation; + } + + public boolean isWaitAfterFirstRetry() { + return waitAfterFirstRetry; + } + + public void setWaitAfterFirstRetry(boolean waitAfterFirstRetry) { + this.waitAfterFirstRetry = waitAfterFirstRetry; + } + + public boolean isContinuerOnRetryWait() { + return continuerOnRetryWait; + } + + public void setContinuerOnRetryWait(boolean continuerOnRetryWait) { + this.continuerOnRetryWait = continuerOnRetryWait; + } + + public boolean isWaiting() { + return waiting; + } + + public void setWaiting(boolean waiting) { + this.waiting = waiting; + } } From 77f2894fe613727ff9ab277ab0f41bcd1ec3b166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Sep 2025 15:25:57 +0200 Subject: [PATCH 24/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/EventProcessor.java | 2 ++ .../event/ReconciliationDispatcher.java | 1 - .../processing/event/EventProcessorTest.java | 22 +++++++++++++++++++ .../event/ReconciliationDispatcherTest.java | 9 ++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) 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 55ed95d68b..0619824cd0 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 @@ -504,6 +504,8 @@ public void run() { return; } executionScope.setDeleteEvent(true); + executionScope.setDeleteFinalStateUnknown( + state.orElseThrow().isDeleteFinalStateUnknown()); } else { log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); return; 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 26586fc6ac..fa7e65738f 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 @@ -262,7 +262,6 @@ private PostExecutionControl

createPostExecutionControl( return postExecutionControl; } - // todo test private void updatePostExecutionControlWithReschedule( PostExecutionControl

postExecutionControl, BaseControl baseControl, 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 f5060a3492..ebf12f6f68 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 @@ -491,6 +491,28 @@ void cleansUpForDeleteEventEvenIfProcessorNotStarted() { // no exception thrown } + @Test + void allEventModeProcessesDeleteEvent() {} + + @Test + void allEventModeRetriesDeleteEventError() {} + + @Test + void processesAdditionalEventWhileInDeleteModeRetry() {} + + @Test + void allEventModeIfNoRetryInCleanupOnError() { + } + + @Test + void onAllEventModeIfRetryExhaustedCleansUpState() { + } + + @Test + void passesResourceFromStateToDispatcher() { + // check also last state unknown + } + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( 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 89f3655356..d462e83ec0 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 @@ -703,6 +703,15 @@ void reconcilerContextUsesTheSameInstanceOfResourceAsParam() { .isNotSameAs(testCustomResource); } + @Test + void allEventModeNoReSchedulesAllowedForDeleteEvent() {} + + @Test + void allEventModeCallsCleanupOnDeleteEvent() {} + + @Test + void allEventModeCallsCleanupOnMarkedForDeletion() {} + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); From a044ab7894566f4044ead1920b49327e72b68354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 3 Sep 2025 15:35:51 +0200 Subject: [PATCH 25/41] Changes to processAllEventInReconciler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/BaseConfigurationService.java | 5 +- .../api/config/ControllerConfiguration.java | 8 +- .../ControllerConfigurationOverrider.java | 11 +-- .../operator/api/config/ControllerMode.java | 6 -- .../ResolvedControllerConfiguration.java | 18 ++--- .../reconciler/ControllerConfiguration.java | 3 +- .../processing/event/EventProcessor.java | 32 ++++---- .../event/ReconciliationDispatcher.java | 15 ++-- .../processing/event/EventProcessorTest.java | 6 +- .../event/ReconciliationDispatcherTest.java | 6 -- .../controller/ControllerEventSourceTest.java | 2 +- .../AbstractAllEventReconciler.java | 58 -------------- .../AllEventCleanerCustomResource.java | 13 --- .../cleaner/AllEventCleanerIT.java | 79 ------------------- .../cleaner/AllEventCleanerReconciler.java | 52 ------------ .../onlyreconcile/AllEventSpec.java | 14 ---- .../PropagateAllEventCustomResource.java} | 4 +- .../onlyreconcile/PropagateAllEventIT.java} | 34 ++++---- .../onlyreconcile/PropagateAllEventSpec.java} | 4 +- .../PropagateEventReconciler.java} | 69 +++++++++++++--- 20 files changed, 127 insertions(+), 312 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{alleventmode/onlyreconcile/AllEventCustomResource.java => propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java} (67%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{alleventmode/onlyreconcile/AllEventIT.java => propagateallevent/onlyreconcile/PropagateAllEventIT.java} (82%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{alleventmode/cleaner/AllEventCleanerSpec.java => propagateallevent/onlyreconcile/PropagateAllEventSpec.java} (56%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{alleventmode/onlyreconcile/AllEventReconciler.java => propagateallevent/onlyreconcile/PropagateEventReconciler.java} (65%) 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 2d2db3e954..6a11294848 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 @@ -304,7 +304,8 @@ private

ResolvedControllerConfiguration

controllerCon final var dependentFieldManager = fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager; - var controllerMode = annotation == null ? ControllerMode.DEFAULT : annotation.mode(); + var propagateAllEventToReconciler = + annotation != null && annotation.propagateAllEventToReconciler(); InformerConfiguration

informerConfig = InformerConfiguration.builder(resourceClass) @@ -326,7 +327,7 @@ private

ResolvedControllerConfiguration

controllerCon dependentFieldManager, this, informerConfig, - controllerMode); + propagateAllEventToReconciler); } /** 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 ac34318439..d38be6ae6f 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 @@ -93,11 +93,7 @@ default String fieldManager() { C getConfigurationFor(DependentResourceSpec spec); - default ControllerMode mode() { - return ControllerMode.DEFAULT; - } - - default boolean isAllEventReconcileMode() { - return mode() == ControllerMode.RECONCILE_ALL_EVENT; + default boolean propagateAllEventToReconciler() { + return false; } } 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 bf6a1265ac..35d7d2dafc 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 @@ -30,7 +30,7 @@ public class ControllerConfigurationOverrider { private Duration reconciliationMaxInterval; private Map configurations; private final InformerConfiguration.Builder config; - private ControllerMode mode; + private boolean propagateAllEventToReconciler; private ControllerConfigurationOverrider(ControllerConfiguration original) { this.finalizer = original.getFinalizerName(); @@ -43,7 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { this.rateLimiter = original.getRateLimiter(); this.name = original.getName(); this.fieldManager = original.fieldManager(); - this.mode = original.mode(); + this.propagateAllEventToReconciler = original.propagateAllEventToReconciler(); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -156,8 +156,9 @@ public ControllerConfigurationOverrider withFieldManager(String dependentFiel return this; } - public ControllerConfigurationOverrider withMode(ControllerMode controllerMode) { - this.mode = controllerMode; + public ControllerConfigurationOverrider withPropagateAllEventToReconciler( + boolean propagateAllEventToReconciler) { + this.propagateAllEventToReconciler = propagateAllEventToReconciler; return this; } @@ -205,7 +206,7 @@ public ControllerConfiguration build() { fieldManager, original.getConfigurationService(), config.buildForController(), - mode, + propagateAllEventToReconciler, original.getWorkflowSpec().orElse(null)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java deleted file mode 100644 index 536cefc7cf..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerMode.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -public enum ControllerMode { - DEFAULT, - RECONCILE_ALL_EVENT -} 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 index 6ca03ae91b..d6c8b839c5 100644 --- 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 @@ -29,8 +29,8 @@ public class ResolvedControllerConfiguration

private final Map configurations; private final ConfigurationService configurationService; private final String fieldManager; + private final boolean propagateAllEventToReconciler; private WorkflowSpec workflowSpec; - private ControllerMode controllerMode; public ResolvedControllerConfiguration(ControllerConfiguration

other) { this( @@ -45,7 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration

other) { other.fieldManager(), other.getConfigurationService(), other.getInformerConfig(), - other.mode(), + other.propagateAllEventToReconciler(), other.getWorkflowSpec().orElse(null)); } @@ -61,7 +61,7 @@ public ResolvedControllerConfiguration( String fieldManager, ConfigurationService configurationService, InformerConfiguration

informerConfig, - ControllerMode controllerMode, + boolean propagateAllEventToReconciler, WorkflowSpec workflowSpec) { this( name, @@ -75,7 +75,7 @@ public ResolvedControllerConfiguration( fieldManager, configurationService, informerConfig, - controllerMode); + propagateAllEventToReconciler); setWorkflowSpec(workflowSpec); } @@ -91,7 +91,7 @@ protected ResolvedControllerConfiguration( String fieldManager, ConfigurationService configurationService, InformerConfiguration

informerConfig, - ControllerMode controllerMode) { + boolean propagateAllEventToReconciler) { this.informerConfig = informerConfig; this.configurationService = configurationService; this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName); @@ -104,7 +104,7 @@ protected ResolvedControllerConfiguration( this.finalizer = ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName()); this.fieldManager = fieldManager; - this.controllerMode = controllerMode; + this.propagateAllEventToReconciler = propagateAllEventToReconciler; } protected ResolvedControllerConfiguration( @@ -124,7 +124,7 @@ protected ResolvedControllerConfiguration( null, configurationService, InformerConfiguration.builder(resourceClass).buildForController(), - null); + false); } @Override @@ -216,7 +216,7 @@ public String fieldManager() { } @Override - public ControllerMode mode() { - return controllerMode; + public boolean propagateAllEventToReconciler() { + return ControllerConfiguration.super.propagateAllEventToReconciler(); } } 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 a2afceca2a..2fa4ff763b 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 @@ -6,7 +6,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -79,5 +78,5 @@ MaxReconciliationInterval maxReconciliationInterval() default */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; - ControllerMode mode() default ControllerMode.DEFAULT; + boolean propagateAllEventToReconciler() default false; } 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 0619824cd0..b2952a3eed 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 @@ -131,7 +131,7 @@ public synchronized void handleEvent(Event event) { } private void handleMarkedEventForResource(ResourceState state) { - if (state.deleteEventPresent() && !isAllEventMode()) { + if (state.deleteEventPresent() && !propagateAllEvent()) { cleanupForDeletedEvent(state.getId()); } else if (!state.processedMarkForDeletionPresent()) { submitReconciliationExecution(state); @@ -145,7 +145,7 @@ private void submitReconciliationExecution(ResourceState state) { Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution - && (maybeLatest.isPresent() || (isAllEventMode() && state.deleteEventPresent()))) { + && (maybeLatest.isPresent() || (propagateAllEvent() && state.deleteEventPresent()))) { var rateLimit = state.getRateLimit(); if (rateLimit == null) { rateLimit = rateLimiter.initState(); @@ -159,7 +159,7 @@ private void submitReconciliationExecution(ResourceState state) { state.setUnderProcessing(true); final var latest = maybeLatest.orElseGet(() -> getResourceFromState(state)); ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); - state.unMarkEventReceived(isAllEventMode()); + state.unMarkEventReceived(propagateAllEvent()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ReconcilerExecutor(resourceID, executionScope)); @@ -186,7 +186,7 @@ private void submitReconciliationExecution(ResourceState state) { @SuppressWarnings("unchecked") private P getResourceFromState(ResourceState state) { - if (isAllEventMode()) { + if (propagateAllEvent()) { log.debug("Getting resource from state for {}", state.getId()); return (P) state.getLastKnownResource(); } else { @@ -217,11 +217,11 @@ private void handleEventMarking(Event event, ResourceState state) { // removed, but also the informers websocket is disconnected and later reconnected. So // meanwhile the resource could be deleted and recreated. In this case we just mark a new // event as below. - state.markEventReceived(isAllEventMode()); + state.markEventReceived(propagateAllEvent()); } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { - state.markEventReceived(isAllEventMode()); - } else if (isAllEventMode() && state.deleteEventPresent()) { + state.markEventReceived(propagateAllEvent()); + } else if (propagateAllEvent() && state.deleteEventPresent()) { state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { log.debug( @@ -264,21 +264,21 @@ synchronized void eventProcessingFinished( // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() - && (!state.deleteEventPresent() || isAllEventMode())) { + && (!state.deleteEventPresent() || propagateAllEvent())) { handleRetryOnException( executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); - if ((isAllEventMode() && executionScope.isDeleteEvent()) - || (!isAllEventMode() && state.deleteEventPresent())) { + if ((propagateAllEvent() && executionScope.isDeleteEvent()) + || (!propagateAllEvent() && state.deleteEventPresent())) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { state.markProcessedMarkForDeletion(); metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { - if (state.eventPresent() || (isAllEventMode() && state.deleteEventPresent())) { + if (state.eventPresent() || (propagateAllEvent() && state.deleteEventPresent())) { submitReconciliationExecution(state); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); @@ -343,8 +343,8 @@ private void handleRetryOnException(ExecutionScope

executionScope, Exception var resourceID = state.getId(); boolean eventPresent = state.eventPresent() - || (isAllEventMode() && state.isAdditionalEventPresentAfterDeleteEvent()); - state.markEventReceived(isAllEventMode()); + || (propagateAllEvent() && state.isAdditionalEventPresentAfterDeleteEvent()); + state.markEventReceived(propagateAllEvent()); retryAwareErrorLogging(state.getRetry(), eventPresent, exception, executionScope); if (eventPresent) { @@ -488,7 +488,7 @@ public void run() { try { var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { - if (isAllEventMode()) { + if (propagateAllEvent()) { log.debug( "Resource not found in the cache, checking for delete event resource: {}", resourceID); @@ -547,7 +547,7 @@ public synchronized boolean isRunning() { } // shortening - private boolean isAllEventMode() { - return controllerConfiguration.isAllEventReconcileMode(); + private boolean propagateAllEvent() { + return controllerConfiguration.propagateAllEventToReconciler(); } } 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 fa7e65738f..817145ad4d 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 @@ -81,7 +81,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) originalResource.getMetadata().getNamespace()); final var markedForDeletion = originalResource.isMarkedForDeletion(); - if (!isAllEventMode() + if (!propagateAllEvent() && markedForDeletion && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { log.debug( @@ -100,10 +100,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { executionScope.isDeleteFinalStateUnknown()); // checking the cleaner for all-event-mode - if ((!isAllEventMode() && markedForDeletion) - || (isAllEventMode() - && controller.isCleaner() - && (markedForDeletion || executionScope.isDeleteEvent()))) { + if (!propagateAllEvent() && markedForDeletion) { return handleCleanup(resourceForExecution, originalResource, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); @@ -122,7 +119,7 @@ private PostExecutionControl

handleReconcile( P originalResource, Context

context) throws Exception { - if (!isAllEventMode() + if (!propagateAllEvent() && controller.useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* @@ -291,7 +288,7 @@ private PostExecutionControl

handleCleanup( } DeleteControl deleteControl = controller.cleanup(resourceForExecution, context); final var useFinalizer = controller.useFinalizer(); - if (useFinalizer && !isAllEventMode()) { + if (useFinalizer && !propagateAllEvent()) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); @@ -538,7 +535,7 @@ private Resource resource(R resource) { } } - private boolean isAllEventMode() { - return configuration().isAllEventReconcileMode(); + private boolean propagateAllEvent() { + return configuration().propagateAllEventToReconciler(); } } 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 ebf12f6f68..3592234311 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 @@ -501,12 +501,10 @@ void allEventModeRetriesDeleteEventError() {} void processesAdditionalEventWhileInDeleteModeRetry() {} @Test - void allEventModeIfNoRetryInCleanupOnError() { - } + void allEventModeIfNoRetryInCleanupOnError() {} @Test - void onAllEventModeIfRetryExhaustedCleansUpState() { - } + void onAllEventModeIfRetryExhaustedCleansUpState() {} @Test void passesResourceFromStateToDispatcher() { 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 d462e83ec0..ce0970e273 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 @@ -706,12 +706,6 @@ void reconcilerContextUsesTheSameInstanceOfResourceAsParam() { @Test void allEventModeNoReSchedulesAllowedForDeleteEvent() {} - @Test - void allEventModeCallsCleanupOnDeleteEvent() {} - - @Test - void allEventModeCallsCleanupOnMarkedForDeletion() {} - 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/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index e4891d0456..f6e4be31c9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -209,7 +209,7 @@ public TestConfiguration( .withOnUpdateFilter(onUpdateFilter) .withGenericFilter(genericFilter) .buildForController(), - null); + false); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java deleted file mode 100644 index df36e250ad..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/AbstractAllEventReconciler.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode; - -import java.util.concurrent.atomic.AtomicInteger; - -public class AbstractAllEventReconciler { - - public static final String FINALIZER = "all.event.mode/finalizer"; - public static final String ADDITIONAL_FINALIZER = "all.event.mode/finalizer2"; - public static final String NO_MORE_EXCEPTION_ANNOTATION_KEY = "no.more.exception"; - - protected volatile boolean useFinalizer = true; - - private final AtomicInteger eventCounter = new AtomicInteger(0); - - private boolean deleteEventPresent = false; - private boolean eventOnMarkedForDeletion = false; - private boolean resourceEventPresent = false; - - public int getEventCount() { - return eventCounter.get(); - } - - public void increaseEventCount() { - eventCounter.incrementAndGet(); - } - - public boolean getUseFinalizer() { - return useFinalizer; - } - - public void setUseFinalizer(boolean useFinalizer) { - this.useFinalizer = useFinalizer; - } - - public boolean isDeleteEventPresent() { - return deleteEventPresent; - } - - public void setDeleteEventPresent(boolean deleteEventPresent) { - this.deleteEventPresent = deleteEventPresent; - } - - public boolean isEventOnMarkedForDeletion() { - return eventOnMarkedForDeletion; - } - - public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { - this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; - } - - public boolean isResourceEventPresent() { - return resourceEventPresent; - } - - public void setResourceEventPresent(boolean resourceEventPresent) { - this.resourceEventPresent = resourceEventPresent; - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java deleted file mode 100644 index ff244cc371..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerCustomResource.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; - -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("eccs") -public class AllEventCleanerCustomResource extends CustomResource - implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java deleted file mode 100644 index 584b8296a3..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerIT.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; - -import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -public class AllEventCleanerIT { - - public static final String TEST = "test1"; - - @RegisterExtension - LocallyRunOperatorExtension extension = - LocallyRunOperatorExtension.builder().withReconciler(new AllEventCleanerReconciler()).build(); - - @Test - void eventsPresent() { - var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); - reconciler.setUseFinalizer(true); - extension.serverSideApply(testResource()); - - await() - .untilAsserted( - () -> { - assertThat(reconciler.isResourceEventPresent()).isTrue(); - assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); - }); - - extension.delete(getResource()); - - await() - .untilAsserted( - () -> { - var r = getResource(); - assertThat(r).isNull(); - assertThat(reconciler.isDeleteEventPresent()).isTrue(); - assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); - }); - } - - @Test - void deleteEventPresentWithoutFinalizer() { - var reconciler = extension.getReconcilerOfType(AllEventCleanerReconciler.class); - extension.serverSideApply(testResource()); - - await() - .untilAsserted( - () -> { - assertThat(reconciler.isResourceEventPresent()).isTrue(); - assertThat(getResource().hasFinalizer(FINALIZER)).isTrue(); - }); - - extension.delete(getResource()); - - await() - .untilAsserted( - () -> { - var r = getResource(); - assertThat(r).isNull(); - assertThat(reconciler.isDeleteEventPresent()).isTrue(); - assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); - }); - } - - AllEventCleanerCustomResource getResource() { - return extension.get(AllEventCleanerCustomResource.class, TEST); - } - - AllEventCleanerCustomResource testResource() { - var res = new AllEventCleanerCustomResource(); - res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); - return res; - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java deleted file mode 100644 index 4772e7c250..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerReconciler.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; - -import io.javaoperatorsdk.operator.api.config.ControllerMode; -import io.javaoperatorsdk.operator.api.reconciler.Cleaner; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.FinalizerUtils; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; - -@ControllerConfiguration(mode = ControllerMode.RECONCILE_ALL_EVENT) -public class AllEventCleanerReconciler extends AbstractAllEventReconciler - implements Reconciler, Cleaner { - - @Override - public UpdateControl reconcile( - AllEventCleanerCustomResource primary, Context context) { - - increaseEventCount(); - if (!primary.isMarkedForDeletion()) { - setResourceEventPresent(true); - } - - if (useFinalizer && !primary.hasFinalizer(FINALIZER)) { - FinalizerUtils.patchFinalizer(primary, FINALIZER, context); - return UpdateControl.noUpdate(); - } - - return UpdateControl.noUpdate(); - } - - @Override - public DeleteControl cleanup( - AllEventCleanerCustomResource resource, Context context) - throws Exception { - - increaseEventCount(); - if (resource.isMarkedForDeletion() && !context.isDeleteEventPresent()) { - setEventOnMarkedForDeletion(true); - if (resource.hasFinalizer(FINALIZER)) { - FinalizerUtils.removeFinalizer(resource, FINALIZER, context); - } - } - - if (context.isDeleteEventPresent()) { - setDeleteEventPresent(true); - } - return DeleteControl.defaultDelete(); - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java deleted file mode 100644 index 0b14423606..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventSpec.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; - -public class AllEventSpec { - - private String value; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java similarity index 67% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java index b12f879d51..d51bd8fdaf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; @@ -9,5 +9,5 @@ @Group("sample.javaoperatorsdk") @Version("v1") @ShortNames("aecs") -public class AllEventCustomResource extends CustomResource +public class PropagateAllEventCustomResource extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java similarity index 82% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java index 0e8da2f8af..208b50f64c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; import java.time.Duration; @@ -9,13 +9,13 @@ import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.ADDITIONAL_FINALIZER; -import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.FINALIZER; -import static io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; +import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.ADDITIONAL_FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class AllEventIT { +public class PropagateAllEventIT { public static final String TEST = "test1"; public static final int MAX_RETRY_ATTEMPTS = 2; @@ -24,7 +24,7 @@ public class AllEventIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withReconciler( - new AllEventReconciler(), + new PropagateEventReconciler(), o -> o.withRetry( new GenericRetry() @@ -35,7 +35,7 @@ public class AllEventIT { @Test void eventsPresent() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); extension.serverSideApply(testResource()); await() .untilAsserted( @@ -58,7 +58,7 @@ void eventsPresent() { @Test void deleteEventPresentWithoutFinalizer() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); reconciler.setUseFinalizer(false); extension.serverSideApply(testResource()); @@ -77,7 +77,7 @@ void deleteEventPresentWithoutFinalizer() { @Test void retriesExceptionOnDeleteEvent() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); reconciler.setUseFinalizer(false); reconciler.setThrowExceptionOnFirstDeleteEvent(true); @@ -98,7 +98,7 @@ void retriesExceptionOnDeleteEvent() { @Test void additionalFinalizer() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); reconciler.setUseFinalizer(true); var res = testResource(); res.addFinalizer(ADDITIONAL_FINALIZER); @@ -132,7 +132,7 @@ void additionalFinalizer() { @Test void additionalEventDuringRetryOnDeleteEvent() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); reconciler.setThrowExceptionIfNoAnnotation(true); reconciler.setWaitAfterFirstRetry(true); var res = testResource(); @@ -189,7 +189,7 @@ void additionalEventDuringRetryOnDeleteEvent() { @Test void additionalEventAfterExhaustedRetry() { - var reconciler = extension.getReconcilerOfType(AllEventReconciler.class); + var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); reconciler.setThrowExceptionIfNoAnnotation(true); var res = testResource(); res.addFinalizer(ADDITIONAL_FINALIZER); @@ -223,18 +223,18 @@ private void removeAdditionalFinalizerWaitForResourceDeletion() { } private void addNoMoreExceptionAnnotation() { - AllEventCustomResource res; + PropagateAllEventCustomResource res; res = getResource(); res.getMetadata().getAnnotations().put(NO_MORE_EXCEPTION_ANNOTATION_KEY, "true"); extension.update(res); } - AllEventCustomResource getResource() { - return extension.get(AllEventCustomResource.class, TEST); + PropagateAllEventCustomResource getResource() { + return extension.get(PropagateAllEventCustomResource.class, TEST); } - AllEventCustomResource testResource() { - var res = new AllEventCustomResource(); + PropagateAllEventCustomResource testResource() { + var res = new PropagateAllEventCustomResource(); res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); return res; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java similarity index 56% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java index 5b38308940..e1c8742fd8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/cleaner/AllEventCleanerSpec.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java @@ -1,6 +1,6 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.cleaner; +package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; -public class AllEventCleanerSpec { +public class PropagateAllEventSpec { private String value; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java similarity index 65% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java index 166589e401..bde5c96af7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/alleventmode/onlyreconcile/AllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java @@ -1,23 +1,22 @@ -package io.javaoperatorsdk.operator.baseapi.alleventmode.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; + +import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.api.config.ControllerMode; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.FinalizerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.baseapi.alleventmode.AbstractAllEventReconciler; @ControllerConfiguration( - mode = ControllerMode.RECONCILE_ALL_EVENT, + propagateAllEventToReconciler = true, generationAwareEventProcessing = false) -public class AllEventReconciler extends AbstractAllEventReconciler - implements Reconciler { +public class PropagateEventReconciler implements Reconciler { - private static final Logger log = LoggerFactory.getLogger(AllEventReconciler.class); + private static final Logger log = LoggerFactory.getLogger(PropagateEventReconciler.class); private volatile boolean throwExceptionOnFirstDeleteEvent = false; private volatile boolean throwExceptionIfNoAnnotation = false; @@ -28,9 +27,21 @@ public class AllEventReconciler extends AbstractAllEventReconciler private volatile boolean isFirstDeleteEvent = true; + public static final String FINALIZER = "all.event.mode/finalizer"; + public static final String ADDITIONAL_FINALIZER = "all.event.mode/finalizer2"; + public static final String NO_MORE_EXCEPTION_ANNOTATION_KEY = "no.more.exception"; + + protected volatile boolean useFinalizer = true; + + private final AtomicInteger eventCounter = new AtomicInteger(0); + + private boolean deleteEventPresent = false; + private boolean eventOnMarkedForDeletion = false; + private boolean resourceEventPresent = false; + @Override - public UpdateControl reconcile( - AllEventCustomResource primary, Context context) + public UpdateControl reconcile( + PropagateAllEventCustomResource primary, Context context) throws InterruptedException { log.info("Reconciling"); increaseEventCount(); @@ -129,4 +140,44 @@ public boolean isWaiting() { public void setWaiting(boolean waiting) { this.waiting = waiting; } + + public int getEventCount() { + return eventCounter.get(); + } + + public void increaseEventCount() { + eventCounter.incrementAndGet(); + } + + public boolean getUseFinalizer() { + return useFinalizer; + } + + public void setUseFinalizer(boolean useFinalizer) { + this.useFinalizer = useFinalizer; + } + + public boolean isDeleteEventPresent() { + return deleteEventPresent; + } + + public void setDeleteEventPresent(boolean deleteEventPresent) { + this.deleteEventPresent = deleteEventPresent; + } + + public boolean isEventOnMarkedForDeletion() { + return eventOnMarkedForDeletion; + } + + public void setEventOnMarkedForDeletion(boolean eventOnMarkedForDeletion) { + this.eventOnMarkedForDeletion = eventOnMarkedForDeletion; + } + + public boolean isResourceEventPresent() { + return resourceEventPresent; + } + + public void setResourceEventPresent(boolean resourceEventPresent) { + this.resourceEventPresent = resourceEventPresent; + } } From ae4ef854242f2956e0177fa131194883e1eb0e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 10:35:19 +0200 Subject: [PATCH 26/41] naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/Context.java | 4 ++-- .../api/reconciler/DefaultContext.java | 20 +++++++++---------- .../PropagateEventReconciler.java | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index b063dfedaf..f7e9c3763c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -73,7 +73,7 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { */ boolean isNextReconciliationImminent(); - boolean isDeleteEventPresent(); + boolean isPrimaryResourceDeleted(); - boolean isDeleteFinalStateUnknown(); + boolean isPrimaryResourceFinalStateUnknown(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index d6d454bd3f..5a9166d047 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -24,21 +24,21 @@ public class DefaultContext

implements Context

{ private final ControllerConfiguration

controllerConfiguration; private final DefaultManagedWorkflowAndDependentResourceContext

defaultManagedDependentResourceContext; - private final boolean isDeleteEventPresent; - private final boolean isDeleteFinalStateUnknown; + private final boolean primaryResourceDeleted; + private final boolean primaryResourceFinalStateUnknown; public DefaultContext( RetryInfo retryInfo, Controller

controller, P primaryResource, - boolean isDeleteEventPresent, - boolean isDeleteFinalStateUnknown) { + boolean primaryResourceDeleted, + boolean primaryResourceFinalStateUnknown) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; this.controllerConfiguration = controller.getConfiguration(); - this.isDeleteEventPresent = isDeleteEventPresent; - this.isDeleteFinalStateUnknown = isDeleteFinalStateUnknown; + this.primaryResourceDeleted = primaryResourceDeleted; + this.primaryResourceFinalStateUnknown = primaryResourceFinalStateUnknown; this.defaultManagedDependentResourceContext = new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this); } @@ -129,13 +129,13 @@ public boolean isNextReconciliationImminent() { } @Override - public boolean isDeleteEventPresent() { - return isDeleteEventPresent; + public boolean isPrimaryResourceDeleted() { + return primaryResourceDeleted; } @Override - public boolean isDeleteFinalStateUnknown() { - return isDeleteFinalStateUnknown; + public boolean isPrimaryResourceFinalStateUnknown() { + return primaryResourceFinalStateUnknown; } public DefaultContext

setRetryInfo(RetryInfo retryInfo) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java index bde5c96af7..b1ecc27872 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java @@ -71,7 +71,7 @@ public UpdateControl reconcile( throw new RuntimeException("On purpose exception for missing annotation"); } - if (primary.isMarkedForDeletion() && !context.isDeleteEventPresent()) { + if (primary.isMarkedForDeletion() && !context.isPrimaryResourceDeleted()) { setEventOnMarkedForDeletion(true); if (getUseFinalizer() && primary.hasFinalizer(FINALIZER)) { log.info("Removing finalizer"); @@ -79,14 +79,14 @@ public UpdateControl reconcile( } } - if (context.isDeleteEventPresent() + if (context.isPrimaryResourceDeleted() && isFirstDeleteEvent() && isThrowExceptionOnFirstDeleteEvent()) { isFirstDeleteEvent = false; throw new RuntimeException("On purpose exception"); } - if (context.isDeleteEventPresent()) { + if (context.isPrimaryResourceDeleted()) { setDeleteEventPresent(true); } log.info("Reconciliation finished"); From 561ac4cb658d189809f1e0c5a5b43ca926f79b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 10:48:29 +0200 Subject: [PATCH 27/41] naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/BaseConfigurationService.java | 6 +++--- .../api/config/ControllerConfiguration.java | 2 +- .../config/ControllerConfigurationOverrider.java | 12 ++++++------ .../config/ResolvedControllerConfiguration.java | 16 ++++++++-------- .../api/reconciler/ControllerConfiguration.java | 2 +- .../processing/event/EventProcessor.java | 2 +- .../event/ReconciliationDispatcher.java | 2 +- .../onlyreconcile/PropagateEventReconciler.java | 4 +--- 8 files changed, 22 insertions(+), 24 deletions(-) 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 6a11294848..1c5a79d79e 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 @@ -304,8 +304,8 @@ private

ResolvedControllerConfiguration

controllerCon final var dependentFieldManager = fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager; - var propagateAllEventToReconciler = - annotation != null && annotation.propagateAllEventToReconciler(); + var triggerReconcilerOnAllEvent = + annotation != null && annotation.triggerReconcilerOnAllEvent(); InformerConfiguration

informerConfig = InformerConfiguration.builder(resourceClass) @@ -327,7 +327,7 @@ private

ResolvedControllerConfiguration

controllerCon dependentFieldManager, this, informerConfig, - propagateAllEventToReconciler); + triggerReconcilerOnAllEvent); } /** 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 d38be6ae6f..9c9f43485d 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 @@ -93,7 +93,7 @@ default String fieldManager() { C getConfigurationFor(DependentResourceSpec spec); - default boolean propagateAllEventToReconciler() { + default boolean triggerReconcilerOnAllEvent() { return false; } } 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 35d7d2dafc..c7fb6f7f6d 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 @@ -30,7 +30,7 @@ public class ControllerConfigurationOverrider { private Duration reconciliationMaxInterval; private Map configurations; private final InformerConfiguration.Builder config; - private boolean propagateAllEventToReconciler; + private boolean triggerReconcilerOnAllEvent; private ControllerConfigurationOverrider(ControllerConfiguration original) { this.finalizer = original.getFinalizerName(); @@ -43,7 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { this.rateLimiter = original.getRateLimiter(); this.name = original.getName(); this.fieldManager = original.fieldManager(); - this.propagateAllEventToReconciler = original.propagateAllEventToReconciler(); + this.triggerReconcilerOnAllEvent = original.triggerReconcilerOnAllEvent(); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -156,9 +156,9 @@ public ControllerConfigurationOverrider withFieldManager(String dependentFiel return this; } - public ControllerConfigurationOverrider withPropagateAllEventToReconciler( - boolean propagateAllEventToReconciler) { - this.propagateAllEventToReconciler = propagateAllEventToReconciler; + public ControllerConfigurationOverrider withTriggerReconcilerOnAllEvent( + boolean triggerReconcilerOnAllEvent) { + this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent; return this; } @@ -206,7 +206,7 @@ public ControllerConfiguration build() { fieldManager, original.getConfigurationService(), config.buildForController(), - propagateAllEventToReconciler, + triggerReconcilerOnAllEvent, original.getWorkflowSpec().orElse(null)); } 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 index d6c8b839c5..ac0ded6b11 100644 --- 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 @@ -29,7 +29,7 @@ public class ResolvedControllerConfiguration

private final Map configurations; private final ConfigurationService configurationService; private final String fieldManager; - private final boolean propagateAllEventToReconciler; + private final boolean triggerReconcilerOnAllEvent; private WorkflowSpec workflowSpec; public ResolvedControllerConfiguration(ControllerConfiguration

other) { @@ -45,7 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration

other) { other.fieldManager(), other.getConfigurationService(), other.getInformerConfig(), - other.propagateAllEventToReconciler(), + other.triggerReconcilerOnAllEvent(), other.getWorkflowSpec().orElse(null)); } @@ -61,7 +61,7 @@ public ResolvedControllerConfiguration( String fieldManager, ConfigurationService configurationService, InformerConfiguration

informerConfig, - boolean propagateAllEventToReconciler, + boolean triggerReconcilerOnAllEvent, WorkflowSpec workflowSpec) { this( name, @@ -75,7 +75,7 @@ public ResolvedControllerConfiguration( fieldManager, configurationService, informerConfig, - propagateAllEventToReconciler); + triggerReconcilerOnAllEvent); setWorkflowSpec(workflowSpec); } @@ -91,7 +91,7 @@ protected ResolvedControllerConfiguration( String fieldManager, ConfigurationService configurationService, InformerConfiguration

informerConfig, - boolean propagateAllEventToReconciler) { + boolean triggerReconcilerOnAllEvent) { this.informerConfig = informerConfig; this.configurationService = configurationService; this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName); @@ -104,7 +104,7 @@ protected ResolvedControllerConfiguration( this.finalizer = ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName()); this.fieldManager = fieldManager; - this.propagateAllEventToReconciler = propagateAllEventToReconciler; + this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent; } protected ResolvedControllerConfiguration( @@ -216,7 +216,7 @@ public String fieldManager() { } @Override - public boolean propagateAllEventToReconciler() { - return ControllerConfiguration.super.propagateAllEventToReconciler(); + public boolean triggerReconcilerOnAllEvent() { + return triggerReconcilerOnAllEvent; } } 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 2fa4ff763b..21b4c16b11 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 @@ -78,5 +78,5 @@ MaxReconciliationInterval maxReconciliationInterval() default */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; - boolean propagateAllEventToReconciler() default false; + boolean triggerReconcilerOnAllEvent() default false; } 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 b2952a3eed..97365d1913 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 @@ -548,6 +548,6 @@ public synchronized boolean isRunning() { // shortening private boolean propagateAllEvent() { - return controllerConfiguration.propagateAllEventToReconciler(); + return controllerConfiguration.triggerReconcilerOnAllEvent(); } } 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 817145ad4d..8e7170c6f8 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 @@ -536,6 +536,6 @@ private Resource resource(R resource) { } private boolean propagateAllEvent() { - return configuration().propagateAllEventToReconciler(); + return configuration().triggerReconcilerOnAllEvent(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java index b1ecc27872..146d8283c6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java @@ -11,9 +11,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -@ControllerConfiguration( - propagateAllEventToReconciler = true, - generationAwareEventProcessing = false) +@ControllerConfiguration(triggerReconcilerOnAllEvent = true, generationAwareEventProcessing = false) public class PropagateEventReconciler implements Reconciler { private static final Logger log = LoggerFactory.getLogger(PropagateEventReconciler.class); From c51666232c4dcfbf942d8853acc5518f0d2d3c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 10:53:22 +0200 Subject: [PATCH 28/41] =?UTF-8?q?wip=20Signed-off-by:=20Attila=20M=C3=A9sz?= =?UTF-8?q?=C3=A1ros=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...erReconcilerOnAllEventCustomResource.java} | 6 ++-- .../TriggerReconcilerOnAllEventIT.java} | 34 +++++++++---------- ...riggerReconcilerOnAllEventReconciler.java} | 13 ++++--- .../TriggerReconcilerOnAllEventSpec.java} | 4 +-- 4 files changed, 30 insertions(+), 27 deletions(-) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java => triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java} (60%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{propagateallevent/onlyreconcile/PropagateAllEventIT.java => triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java} (80%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{propagateallevent/onlyreconcile/PropagateEventReconciler.java => triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java} (91%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/{propagateallevent/onlyreconcile/PropagateAllEventSpec.java => triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java} (54%) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java similarity index 60% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java index d51bd8fdaf..59cb1e3d02 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; @@ -9,5 +9,5 @@ @Group("sample.javaoperatorsdk") @Version("v1") @ShortNames("aecs") -public class PropagateAllEventCustomResource extends CustomResource - implements Namespaced {} +public class TriggerReconcilerOnAllEventCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java similarity index 80% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java index 208b50f64c..659b02c4c3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; import java.time.Duration; @@ -9,13 +9,13 @@ import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.ADDITIONAL_FINALIZER; -import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.FINALIZER; -import static io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile.PropagateEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; +import static io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile.TriggerReconcilerOnAllEventReconciler.ADDITIONAL_FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile.TriggerReconcilerOnAllEventReconciler.FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile.TriggerReconcilerOnAllEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class PropagateAllEventIT { +public class TriggerReconcilerOnAllEventIT { public static final String TEST = "test1"; public static final int MAX_RETRY_ATTEMPTS = 2; @@ -24,7 +24,7 @@ public class PropagateAllEventIT { LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withReconciler( - new PropagateEventReconciler(), + new TriggerReconcilerOnAllEventReconciler(), o -> o.withRetry( new GenericRetry() @@ -35,7 +35,7 @@ public class PropagateAllEventIT { @Test void eventsPresent() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); extension.serverSideApply(testResource()); await() .untilAsserted( @@ -58,7 +58,7 @@ void eventsPresent() { @Test void deleteEventPresentWithoutFinalizer() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setUseFinalizer(false); extension.serverSideApply(testResource()); @@ -77,7 +77,7 @@ void deleteEventPresentWithoutFinalizer() { @Test void retriesExceptionOnDeleteEvent() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setUseFinalizer(false); reconciler.setThrowExceptionOnFirstDeleteEvent(true); @@ -98,7 +98,7 @@ void retriesExceptionOnDeleteEvent() { @Test void additionalFinalizer() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setUseFinalizer(true); var res = testResource(); res.addFinalizer(ADDITIONAL_FINALIZER); @@ -132,7 +132,7 @@ void additionalFinalizer() { @Test void additionalEventDuringRetryOnDeleteEvent() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setThrowExceptionIfNoAnnotation(true); reconciler.setWaitAfterFirstRetry(true); var res = testResource(); @@ -189,7 +189,7 @@ void additionalEventDuringRetryOnDeleteEvent() { @Test void additionalEventAfterExhaustedRetry() { - var reconciler = extension.getReconcilerOfType(PropagateEventReconciler.class); + var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setThrowExceptionIfNoAnnotation(true); var res = testResource(); res.addFinalizer(ADDITIONAL_FINALIZER); @@ -223,18 +223,18 @@ private void removeAdditionalFinalizerWaitForResourceDeletion() { } private void addNoMoreExceptionAnnotation() { - PropagateAllEventCustomResource res; + TriggerReconcilerOnAllEventCustomResource res; res = getResource(); res.getMetadata().getAnnotations().put(NO_MORE_EXCEPTION_ANNOTATION_KEY, "true"); extension.update(res); } - PropagateAllEventCustomResource getResource() { - return extension.get(PropagateAllEventCustomResource.class, TEST); + TriggerReconcilerOnAllEventCustomResource getResource() { + return extension.get(TriggerReconcilerOnAllEventCustomResource.class, TEST); } - PropagateAllEventCustomResource testResource() { - var res = new PropagateAllEventCustomResource(); + TriggerReconcilerOnAllEventCustomResource testResource() { + var res = new TriggerReconcilerOnAllEventCustomResource(); res.setMetadata(new ObjectMetaBuilder().withName(TEST).build()); return res; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java similarity index 91% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java index 146d8283c6..b3ffec2ef2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; import java.util.concurrent.atomic.AtomicInteger; @@ -12,9 +12,11 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @ControllerConfiguration(triggerReconcilerOnAllEvent = true, generationAwareEventProcessing = false) -public class PropagateEventReconciler implements Reconciler { +public class TriggerReconcilerOnAllEventReconciler + implements Reconciler { - private static final Logger log = LoggerFactory.getLogger(PropagateEventReconciler.class); + private static final Logger log = + LoggerFactory.getLogger(TriggerReconcilerOnAllEventReconciler.class); private volatile boolean throwExceptionOnFirstDeleteEvent = false; private volatile boolean throwExceptionIfNoAnnotation = false; @@ -38,8 +40,9 @@ public class PropagateEventReconciler implements Reconciler reconcile( - PropagateAllEventCustomResource primary, Context context) + public UpdateControl reconcile( + TriggerReconcilerOnAllEventCustomResource primary, + Context context) throws InterruptedException { log.info("Reconciling"); increaseEventCount(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java similarity index 54% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java index e1c8742fd8..9254be3b25 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/propagateallevent/onlyreconcile/PropagateAllEventSpec.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java @@ -1,6 +1,6 @@ -package io.javaoperatorsdk.operator.baseapi.propagateallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; -public class PropagateAllEventSpec { +public class TriggerReconcilerOnAllEventSpec { private String value; From ec33d7e6f2c8bb9d7dbf3bf6acea5cc362d7374a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 14:17:25 +0200 Subject: [PATCH 29/41] =?UTF-8?q?wip=20Signed-off-by:=20Attila=20M=C3=A9sz?= =?UTF-8?q?=C3=A1ros=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/event/EventProcessor.java | 37 +++-- .../processing/event/ExecutionScope.java | 8 +- .../event/ReconciliationDispatcher.java | 10 +- .../javaoperatorsdk/operator/TestUtils.java | 4 + .../processing/event/EventProcessorTest.java | 127 +++++++++++++++--- .../event/ReconciliationDispatcherTest.java | 22 +-- 6 files changed, 156 insertions(+), 52 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 97365d1913..fd3744f341 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 @@ -131,7 +131,7 @@ public synchronized void handleEvent(Event event) { } private void handleMarkedEventForResource(ResourceState state) { - if (state.deleteEventPresent() && !propagateAllEvent()) { + if (state.deleteEventPresent() && !triggerOnAllEvent()) { cleanupForDeletedEvent(state.getId()); } else if (!state.processedMarkForDeletionPresent()) { submitReconciliationExecution(state); @@ -145,7 +145,7 @@ private void submitReconciliationExecution(ResourceState state) { Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution - && (maybeLatest.isPresent() || (propagateAllEvent() && state.deleteEventPresent()))) { + && (maybeLatest.isPresent() || (triggerOnAllEvent() && state.deleteEventPresent()))) { var rateLimit = state.getRateLimit(); if (rateLimit == null) { rateLimit = rateLimiter.initState(); @@ -158,8 +158,10 @@ private void submitReconciliationExecution(ResourceState state) { } state.setUnderProcessing(true); final var latest = maybeLatest.orElseGet(() -> getResourceFromState(state)); - ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); - state.unMarkEventReceived(propagateAllEvent()); + ExecutionScope

executionScope = + new ExecutionScope<>( + state.getRetry(), state.deleteEventPresent(), state.isDeleteFinalStateUnknown()); + state.unMarkEventReceived(triggerOnAllEvent()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ReconcilerExecutor(resourceID, executionScope)); @@ -186,7 +188,7 @@ private void submitReconciliationExecution(ResourceState state) { @SuppressWarnings("unchecked") private P getResourceFromState(ResourceState state) { - if (propagateAllEvent()) { + if (triggerOnAllEvent()) { log.debug("Getting resource from state for {}", state.getId()); return (P) state.getLastKnownResource(); } else { @@ -217,11 +219,11 @@ private void handleEventMarking(Event event, ResourceState state) { // removed, but also the informers websocket is disconnected and later reconnected. So // meanwhile the resource could be deleted and recreated. In this case we just mark a new // event as below. - state.markEventReceived(propagateAllEvent()); + state.markEventReceived(triggerOnAllEvent()); } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { - state.markEventReceived(propagateAllEvent()); - } else if (propagateAllEvent() && state.deleteEventPresent()) { + state.markEventReceived(triggerOnAllEvent()); + } else if (triggerOnAllEvent() && state.deleteEventPresent()) { state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { log.debug( @@ -264,21 +266,21 @@ synchronized void eventProcessingFinished( // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() - && (!state.deleteEventPresent() || propagateAllEvent())) { + && (!state.deleteEventPresent() || triggerOnAllEvent())) { handleRetryOnException( executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); - if ((propagateAllEvent() && executionScope.isDeleteEvent()) - || (!propagateAllEvent() && state.deleteEventPresent())) { + if ((triggerOnAllEvent() && executionScope.isDeleteEvent()) + || (!triggerOnAllEvent() && state.deleteEventPresent())) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { state.markProcessedMarkForDeletion(); metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { - if (state.eventPresent() || (propagateAllEvent() && state.deleteEventPresent())) { + if (state.eventPresent() || (triggerOnAllEvent() && state.deleteEventPresent())) { submitReconciliationExecution(state); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); @@ -343,8 +345,8 @@ private void handleRetryOnException(ExecutionScope

executionScope, Exception var resourceID = state.getId(); boolean eventPresent = state.eventPresent() - || (propagateAllEvent() && state.isAdditionalEventPresentAfterDeleteEvent()); - state.markEventReceived(propagateAllEvent()); + || (triggerOnAllEvent() && state.isAdditionalEventPresentAfterDeleteEvent()); + state.markEventReceived(triggerOnAllEvent()); retryAwareErrorLogging(state.getRetry(), eventPresent, exception, executionScope); if (eventPresent) { @@ -488,7 +490,7 @@ public void run() { try { var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { - if (propagateAllEvent()) { + if (triggerOnAllEvent() && executionScope.isDeleteEvent()) { log.debug( "Resource not found in the cache, checking for delete event resource: {}", resourceID); @@ -503,9 +505,6 @@ public void run() { "Skipping execution; delete event resource not found in state: {}", resourceID); return; } - executionScope.setDeleteEvent(true); - executionScope.setDeleteFinalStateUnknown( - state.orElseThrow().isDeleteFinalStateUnknown()); } else { log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); return; @@ -547,7 +546,7 @@ public synchronized boolean isRunning() { } // shortening - private boolean propagateAllEvent() { + private boolean triggerOnAllEvent() { return controllerConfiguration.triggerReconcilerOnAllEvent(); } } 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 b7a253faa1..a2d80dc829 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 @@ -8,11 +8,13 @@ class ExecutionScope { // the latest custom resource from cache private R resource; private final RetryInfo retryInfo; - private boolean deleteEvent = false; - private boolean isDeleteFinalStateUnknown = false; + private boolean deleteEvent; + private boolean isDeleteFinalStateUnknown; - ExecutionScope(RetryInfo retryInfo) { + ExecutionScope(RetryInfo retryInfo, boolean deleteEvent, boolean isDeleteFinalStateUnknown) { this.retryInfo = retryInfo; + this.deleteEvent = deleteEvent; + this.isDeleteFinalStateUnknown = isDeleteFinalStateUnknown; } public ExecutionScope setResource(R resource) { 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 8e7170c6f8..70b69f5ad2 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 @@ -81,7 +81,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) originalResource.getMetadata().getNamespace()); final var markedForDeletion = originalResource.isMarkedForDeletion(); - if (!propagateAllEvent() + if (!triggerOnAllEvent() && markedForDeletion && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { log.debug( @@ -100,7 +100,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { executionScope.isDeleteFinalStateUnknown()); // checking the cleaner for all-event-mode - if (!propagateAllEvent() && markedForDeletion) { + if (!triggerOnAllEvent() && markedForDeletion) { return handleCleanup(resourceForExecution, originalResource, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); @@ -119,7 +119,7 @@ private PostExecutionControl

handleReconcile( P originalResource, Context

context) throws Exception { - if (!propagateAllEvent() + if (!triggerOnAllEvent() && controller.useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* @@ -288,7 +288,7 @@ private PostExecutionControl

handleCleanup( } DeleteControl deleteControl = controller.cleanup(resourceForExecution, context); final var useFinalizer = controller.useFinalizer(); - if (useFinalizer && !propagateAllEvent()) { + if (useFinalizer && !triggerOnAllEvent()) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); @@ -535,7 +535,7 @@ private Resource resource(R resource) { } } - private boolean propagateAllEvent() { + private boolean triggerOnAllEvent() { return configuration().triggerReconcilerOnAllEvent(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index afef9e6703..d3f0fbac68 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -32,6 +32,10 @@ public static TestCustomResource testCustomResource1() { return testCustomResource(new ResourceID("test1", "default")); } + public static ResourceID testCustomResource1Id() { + return new ResourceID("test1", "default"); + } + public static TestCustomResource testCustomResource(ResourceID id) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata( 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 3592234311..9000c651b7 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 @@ -36,6 +36,7 @@ import static io.javaoperatorsdk.operator.TestUtils.markForDeletion; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; +import static io.javaoperatorsdk.operator.TestUtils.testCustomResource1; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.eq; @@ -131,7 +132,7 @@ void ifExecutionInProgressWaitsUntilItsFinished() { void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(null); + ExecutionScope executionScope = new ExecutionScope(null, false, false); executionScope.setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -273,7 +274,8 @@ void cancelScheduleOnceEventsOnSuccessfulExecution() { var cr = testCustomResource(crID); eventProcessor.eventProcessingFinished( - new ExecutionScope(null).setResource(cr), PostExecutionControl.defaultDispatch()); + new ExecutionScope(null, false, false).setResource(cr), + PostExecutionControl.defaultDispatch()); verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); } @@ -326,7 +328,8 @@ void startProcessedMarkedEventReceivedBefore() { @Test void notUpdatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(null).setResource(customResource); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceStatusPatched(customResource); @@ -339,7 +342,8 @@ void notUpdatesEventSourceHandlerIfResourceUpdated() { void notReschedulesAfterTheFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(null).setResource(customResource); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -352,7 +356,8 @@ void notReschedulesAfterTheFinalizerRemoveProcessed() { void skipEventProcessingIfFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(null).setResource(customResource); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -369,7 +374,8 @@ void skipEventProcessingIfFinalizerRemoveProcessed() { void newResourceAfterMissedDeleteEvent() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(null).setResource(customResource); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); var newResource = testCustomResource(); @@ -405,7 +411,8 @@ void rateLimitsReconciliationSubmission() { @Test void schedulesRetryForMarReconciliationInterval() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(null).setResource(customResource); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); @@ -427,7 +434,8 @@ void schedulesRetryForMarReconciliationIntervalIfRetryExhausted() { eventSourceManagerMock, metricsMock)); eventProcessorWithRetry.start(); - ExecutionScope executionScope = new ExecutionScope(null).setResource(testCustomResource()); + ExecutionScope executionScope = + new ExecutionScope(null, false, false).setResource(testCustomResource()); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException()); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); @@ -456,7 +464,7 @@ void executionOfReconciliationShouldNotStartIfProcessorStopped() throws Interrup eventProcessor = spy( new EventProcessor( - controllerConfiguration(null, rateLimiterMock, configurationService), + controllerConfiguration(null, rateLimiterMock, configurationService, false), reconciliationDispatcherMock, eventSourceManagerMock, null)); @@ -492,19 +500,84 @@ void cleansUpForDeleteEventEvenIfProcessorNotStarted() { } @Test - void allEventModeProcessesDeleteEvent() {} + void triggerOnAllEventProcessesDeleteEvent() { + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent(null, rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + + verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); + } @Test - void allEventModeRetriesDeleteEventError() {} + void triggerOnAllEventDeleteEventInstantlyAfterEvent() { + var reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent(null, rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); + } @Test - void processesAdditionalEventWhileInDeleteModeRetry() {} + void triggerOnAllEventRetriesDeleteEventError() { + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent( + GenericRetry.defaultLimitedExponentialRetry(), rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()) + .thenReturn(PostExecutionControl.exceptionDuringExecution(new RuntimeException())) + .thenReturn(PostExecutionControl.defaultDispatch()); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + // retry event + eventProcessor.handleEvent(new Event(TestUtils.testCustomResource1Id())); + + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + + verify(reconciliationDispatcherMock, times(3)).handleExecution(any()); + } @Test - void allEventModeIfNoRetryInCleanupOnError() {} + void processesAdditionalEventWhileInDeleteModeRetry() {} @Test - void onAllEventModeIfRetryExhaustedCleansUpState() {} + void triggerOnAllEventIfNoRetryInCleanupOnError() {} @Test void passesResourceFromStateToDispatcher() { @@ -528,6 +601,20 @@ private ResourceEvent prepareCREvent() { return prepareCREvent(new ResourceID(UUID.randomUUID().toString(), TEST_NAMESPACE)); } + private ResourceEvent prepareCREvent1() { + return prepareCREvent(testCustomResource1()); + } + + private ResourceEvent prepareCRDeleteEvent1() { + when(controllerEventSourceMock.get(eq(TestUtils.testCustomResource1Id()))) + .thenReturn(Optional.empty()); + return new ResourceDeleteEvent( + ResourceAction.DELETED, + ResourceID.fromResource(TestUtils.testCustomResource1()), + TestUtils.testCustomResource1(), + true); + } + private ResourceEvent prepareCREvent(HasMetadata hasMetadata) { when(controllerEventSourceMock.get(eq(ResourceID.fromResource(hasMetadata)))) .thenReturn(Optional.of(hasMetadata)); @@ -552,17 +639,25 @@ private void overrideData(ResourceID id, HasMetadata applyTo) { } ControllerConfiguration controllerConfiguration(Retry retry, RateLimiter rateLimiter) { - return controllerConfiguration(retry, rateLimiter, new BaseConfigurationService()); + return controllerConfiguration(retry, rateLimiter, new BaseConfigurationService(), false); + } + + ControllerConfiguration controllerConfigTriggerAllEvent(Retry retry, RateLimiter rateLimiter) { + return controllerConfiguration(retry, rateLimiter, new BaseConfigurationService(), true); } ControllerConfiguration controllerConfiguration( - Retry retry, RateLimiter rateLimiter, ConfigurationService configurationService) { + Retry retry, + RateLimiter rateLimiter, + ConfigurationService configurationService, + boolean triggerOnAllEvent) { ControllerConfiguration res = mock(ControllerConfiguration.class); when(res.getName()).thenReturn("Test"); when(res.getRetry()).thenReturn(retry); when(res.getRateLimiter()).thenReturn(rateLimiter); when(res.maxReconciliationInterval()).thenReturn(Optional.of(Duration.ofMillis(1000))); when(res.getConfigurationService()).thenReturn(configurationService); + when(res.triggerReconcilerOnAllEvent()).thenReturn(triggerOnAllEvent); return res; } } 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 ce0970e273..97c197731a 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 @@ -404,7 +404,9 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - }) + }, + false, + false) .setResource(testCustomResource)); ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); @@ -505,7 +507,9 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - }) + }, + false, + false) .setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); @@ -528,7 +532,7 @@ void callErrorStatusHandlerEvenOnFirstError() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); assertThat(postExecControl.exceptionDuringExecution()).isTrue(); @@ -549,7 +553,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); @@ -571,7 +575,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); verify(customResourceFacade, times(0)).patchStatus(eq(testCustomResource), any()); @@ -588,7 +592,7 @@ void errorStatusHandlerCanPatchResource() { reconciler.errorHandler = () -> ErrorStatusUpdateControl.patchStatus(testCustomResource); reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -611,7 +615,7 @@ void ifRetryLimitedToZeroMaxAttemptsErrorHandlerGetsCorrectLastAttempt() { reconciler.errorHandler = () -> ErrorStatusUpdateControl.noStatusUpdate(); reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)) .updateErrorStatus( @@ -675,7 +679,7 @@ void reSchedulesFromErrorHandler() { var res = reconciliationDispatcher.handleExecution( - new ExecutionScope(null).setResource(testCustomResource)); + new ExecutionScope(null, false, false).setResource(testCustomResource)); assertThat(res.getReScheduleDelay()).contains(delay); assertThat(res.getRuntimeException()).isEmpty(); @@ -726,7 +730,7 @@ private void removeFinalizers(CustomResource customResource) { } public ExecutionScope executionScopeWithCREvent(T resource) { - return (ExecutionScope) new ExecutionScope<>(null).setResource(resource); + return (ExecutionScope) new ExecutionScope<>(null, false, false).setResource(resource); } private class TestReconciler From 81c049df6ac90ce1302c69a1b778da259fe1ccfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 14:20:41 +0200 Subject: [PATCH 30/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventProcessor.java | 7 +------ 1 file changed, 1 insertion(+), 6 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 fd3744f341..82395ab5ee 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 @@ -501,13 +501,8 @@ public void run() { .filter(ResourceState::deleteEventPresent) .map(ResourceState::getLastKnownResource); if (actualResource.isEmpty()) { - log.debug( - "Skipping execution; delete event resource not found in state: {}", resourceID); - return; + throw new IllegalStateException("This should not happen"); } - } else { - log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); - return; } } actualResource.ifPresent(executionScope::setResource); From 1faf2a1e468e1ecf1f6596a2a24fb6e15ef4b3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Sep 2025 15:28:27 +0200 Subject: [PATCH 31/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/EventProcessor.java | 35 +++++++++++---- .../processing/event/ExecutionScope.java | 23 +++++----- .../processing/event/EventProcessorTest.java | 43 +++++++++++++++---- .../event/ReconciliationDispatcherTest.java | 16 ++++--- 4 files changed, 83 insertions(+), 34 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 82395ab5ee..4469cbc80e 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 @@ -158,9 +158,14 @@ private void submitReconciliationExecution(ResourceState state) { } state.setUnderProcessing(true); final var latest = maybeLatest.orElseGet(() -> getResourceFromState(state)); + // passing the latest resources for a corner case when delete event received + // during processing an event ExecutionScope

executionScope = new ExecutionScope<>( - state.getRetry(), state.deleteEventPresent(), state.isDeleteFinalStateUnknown()); + latest, + state.getRetry(), + state.deleteEventPresent(), + state.isDeleteFinalStateUnknown()); state.unMarkEventReceived(triggerOnAllEvent()); metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); @@ -281,6 +286,7 @@ synchronized void eventProcessingFinished( metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { if (state.eventPresent() || (triggerOnAllEvent() && state.deleteEventPresent())) { + log.debug("Submitting for reconciliation."); submitReconciliationExecution(state); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); @@ -484,25 +490,36 @@ public void run() { log.debug("Event processor not running skipping resource processing: {}", resourceID); return; } + log.debug("Running reconcile executor for: {}", executionScope); // change thread name for easier debugging final var thread = Thread.currentThread(); final var name = thread.getName(); try { + // we try to get the most up-to-date resource from cache var actualResource = cache.get(resourceID); if (actualResource.isEmpty()) { - if (triggerOnAllEvent() && executionScope.isDeleteEvent()) { + if (triggerOnAllEvent()) { log.debug( "Resource not found in the cache, checking for delete event resource: {}", resourceID); var state = resourceStateManager.get(resourceID); - actualResource = - (Optional

) - state - .filter(ResourceState::deleteEventPresent) - .map(ResourceState::getLastKnownResource); - if (actualResource.isEmpty()) { - throw new IllegalStateException("This should not happen"); + if (executionScope.isDeleteEvent()) { + actualResource = + (Optional

) + state + .filter(ResourceState::deleteEventPresent) + .map(ResourceState::getLastKnownResource); + if (actualResource.isEmpty()) { + throw new IllegalStateException("this should not happen"); + } + } else { + log.debug("Skipping execution since delete event received meanwhile"); + eventProcessingFinished(executionScope, PostExecutionControl.defaultDispatch()); + return; } + } else { + log.debug("Skipping execution; primary resource missing from cache: {}", resourceID); + return; } } actualResource.ifPresent(executionScope::setResource); 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 a2d80dc829..bef40c0317 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 @@ -11,10 +11,12 @@ class ExecutionScope { private boolean deleteEvent; private boolean isDeleteFinalStateUnknown; - ExecutionScope(RetryInfo retryInfo, boolean deleteEvent, boolean isDeleteFinalStateUnknown) { + ExecutionScope( + R resource, RetryInfo retryInfo, boolean deleteEvent, boolean isDeleteFinalStateUnknown) { this.retryInfo = retryInfo; this.deleteEvent = deleteEvent; this.isDeleteFinalStateUnknown = isDeleteFinalStateUnknown; + this.resource = resource; } public ExecutionScope setResource(R resource) { @@ -48,15 +50,16 @@ public void setDeleteFinalStateUnknown(boolean deleteFinalStateUnknown) { @Override public String toString() { - if (resource == null) { - return "ExecutionScope{resource: null}"; - } else - return "ExecutionScope{" - + " resource id: " - + ResourceID.fromResource(resource) - + ", version: " - + resource.getMetadata().getResourceVersion() - + '}'; + return "ExecutionScope{" + + "resource=" + + ResourceID.fromResource(resource) + + ", retryInfo=" + + retryInfo + + ", deleteEvent=" + + deleteEvent + + ", isDeleteFinalStateUnknown=" + + isDeleteFinalStateUnknown + + '}'; } public RetryInfo getRetryInfo() { 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 9000c651b7..eb4f9c9afe 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 @@ -132,7 +132,7 @@ void ifExecutionInProgressWaitsUntilItsFinished() { void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(null, false, false); + ExecutionScope executionScope = new ExecutionScope(null, null, false, false); executionScope.setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -274,7 +274,7 @@ void cancelScheduleOnceEventsOnSuccessfulExecution() { var cr = testCustomResource(crID); eventProcessor.eventProcessingFinished( - new ExecutionScope(null, false, false).setResource(cr), + new ExecutionScope(null, null, false, false).setResource(cr), PostExecutionControl.defaultDispatch()); verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); @@ -329,7 +329,7 @@ void startProcessedMarkedEventReceivedBefore() { void notUpdatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(customResource); + new ExecutionScope(null, null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceStatusPatched(customResource); @@ -343,7 +343,7 @@ void notReschedulesAfterTheFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(customResource); + new ExecutionScope(null, null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -357,7 +357,7 @@ void skipEventProcessingIfFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(customResource); + new ExecutionScope(null, null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -375,7 +375,7 @@ void newResourceAfterMissedDeleteEvent() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(customResource); + new ExecutionScope(null, null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); var newResource = testCustomResource(); @@ -412,7 +412,7 @@ void rateLimitsReconciliationSubmission() { void schedulesRetryForMarReconciliationInterval() { TestCustomResource customResource = testCustomResource(); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(customResource); + new ExecutionScope(null, null, false, false).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); @@ -435,7 +435,7 @@ void schedulesRetryForMarReconciliationIntervalIfRetryExhausted() { metricsMock)); eventProcessorWithRetry.start(); ExecutionScope executionScope = - new ExecutionScope(null, false, false).setResource(testCustomResource()); + new ExecutionScope(null, null, false, false).setResource(testCustomResource()); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException()); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); @@ -522,6 +522,7 @@ void triggerOnAllEventProcessesDeleteEvent() { verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); } + // this is a special corner case that needs special care @Test void triggerOnAllEventDeleteEventInstantlyAfterEvent() { var reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); @@ -540,6 +541,29 @@ void triggerOnAllEventDeleteEventInstantlyAfterEvent() { eventProcessor.handleEvent(prepareCREvent1()); eventProcessor.handleEvent(prepareCRDeleteEvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + verify(reconciliationDispatcherMock, times(1)).handleExecution(any()); + } + + @Test + void triggerOnAllEventDeleteEventAfterEventProcessed() { + var reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent(null, rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + + eventProcessor.handleEvent(prepareCRDeleteEvent1()); waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); } @@ -584,6 +608,9 @@ void passesResourceFromStateToDispatcher() { // check also last state unknown } + @Test + void onAllEventRateLimiting() {} + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( 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 97c197731a..d89b250cf1 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 @@ -394,6 +394,7 @@ void propagatesRetryInfoToContextIfFinalizerSet() { reconciliationDispatcher.handleExecution( new ExecutionScope( + null, new RetryInfo() { @Override public int getAttemptCount() { @@ -497,6 +498,7 @@ void callErrorStatusHandlerIfImplemented() { reconciliationDispatcher.handleExecution( new ExecutionScope( + null, new RetryInfo() { @Override public int getAttemptCount() { @@ -532,7 +534,7 @@ void callErrorStatusHandlerEvenOnFirstError() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); assertThat(postExecControl.exceptionDuringExecution()).isTrue(); @@ -553,7 +555,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); @@ -575,7 +577,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); verify(customResourceFacade, times(0)).patchStatus(eq(testCustomResource), any()); @@ -592,7 +594,7 @@ void errorStatusHandlerCanPatchResource() { reconciler.errorHandler = () -> ErrorStatusUpdateControl.patchStatus(testCustomResource); reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -615,7 +617,7 @@ void ifRetryLimitedToZeroMaxAttemptsErrorHandlerGetsCorrectLastAttempt() { reconciler.errorHandler = () -> ErrorStatusUpdateControl.noStatusUpdate(); reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)) .updateErrorStatus( @@ -679,7 +681,7 @@ void reSchedulesFromErrorHandler() { var res = reconciliationDispatcher.handleExecution( - new ExecutionScope(null, false, false).setResource(testCustomResource)); + new ExecutionScope(null, null, false, false).setResource(testCustomResource)); assertThat(res.getReScheduleDelay()).contains(delay); assertThat(res.getRuntimeException()).isEmpty(); @@ -730,7 +732,7 @@ private void removeFinalizers(CustomResource customResource) { } public ExecutionScope executionScopeWithCREvent(T resource) { - return (ExecutionScope) new ExecutionScope<>(null, false, false).setResource(resource); + return (ExecutionScope) new ExecutionScope<>(null, null, false, false).setResource(resource); } private class TestReconciler From 377df30d748d2dc167d24cb4c1144396753b9fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 5 Sep 2025 17:31:55 +0200 Subject: [PATCH 32/41] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/junit/AbstractOperatorExtension.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index ee3509c2e5..1f96fb4b87 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -20,7 +20,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; -import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.Utils; @@ -132,10 +131,7 @@ public T update(T resource) { } public T replace(T resource) { - return kubernetesClient - .resource(resource) - .inNamespace(namespace) - .createOr(NonDeletingOperation::update); + return kubernetesClient.resource(resource).inNamespace(namespace).replace(resource); } public boolean delete(T resource) { From 9aad556079dc9324843881e774bb2748e2df0092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 5 Sep 2025 19:01:51 +0200 Subject: [PATCH 33/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/EventProcessorTest.java | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) 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 eb4f9c9afe..bde016a848 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 @@ -598,10 +598,66 @@ void triggerOnAllEventRetriesDeleteEventError() { } @Test - void processesAdditionalEventWhileInDeleteModeRetry() {} + void processesAdditionalEventWhileInDeleteModeRetry() { + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent( + GenericRetry.DEFAULT.setInitialInterval(1000).setMaxAttempts(-1), + rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()) + .thenReturn(PostExecutionControl.exceptionDuringExecution(new RuntimeException())); + + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + // retry event + eventProcessor.handleEvent(new Event(TestUtils.testCustomResource1Id())); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + verify(reconciliationDispatcherMock, times(3)).handleExecution(any()); + } @Test - void triggerOnAllEventIfNoRetryInCleanupOnError() {} + void afterRetryExhaustedAdditionalEventTriggerReconciliationWhenDeleteEventPresent() { + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent( + GenericRetry.DEFAULT + .setInitialInterval(100) + .setIntervalMultiplier(1) + .setMaxAttempts(1), + rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()) + .thenReturn(PostExecutionControl.exceptionDuringExecution(new RuntimeException())); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + eventProcessor.handleEvent(new Event(TestUtils.testCustomResource1Id())); + await() + .untilAsserted(() -> verify(reconciliationDispatcherMock, times(3)).handleExecution(any())); + + eventProcessor.handleEvent(new Event(TestUtils.testCustomResource1Id())); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + verify(reconciliationDispatcherMock, times(4)).handleExecution(any()); + } @Test void passesResourceFromStateToDispatcher() { From 41116b45d74bb86754b1fd73539639434234df0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 5 Sep 2025 19:32:54 +0200 Subject: [PATCH 34/41] test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/EventProcessorTest.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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 bde016a848..441280c570 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 @@ -44,6 +44,7 @@ import static org.mockito.Mockito.after; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.atMostOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -618,7 +619,7 @@ void processesAdditionalEventWhileInDeleteModeRetry() { eventProcessor.handleEvent(prepareCREvent1()); waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); eventProcessor.handleEvent(prepareCRDeleteEvent1()); - verify(reconciliationDispatcherMock, times(2)).handleExecution(any()); + verify(reconciliationDispatcherMock, atMost(2)).handleExecution(any()); waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); // retry event eventProcessor.handleEvent(new Event(TestUtils.testCustomResource1Id())); @@ -661,7 +662,26 @@ void afterRetryExhaustedAdditionalEventTriggerReconciliationWhenDeleteEventPrese @Test void passesResourceFromStateToDispatcher() { - // check also last state unknown + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent(null, rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); + when(eventSourceManagerMock.retryEventSource()).thenReturn(mock(TimerEventSource.class)); + eventProcessor.start(); + + eventProcessor.handleEvent(prepareCREvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + + eventProcessor.handleEvent(prepareCRDeleteEvent1()); + waitUntilProcessingFinished(eventProcessor, TestUtils.testCustomResource1Id()); + var captor = ArgumentCaptor.forClass(ExecutionScope.class); + verify(reconciliationDispatcherMock, times(2)).handleExecution(captor.capture()); + assertThat(captor.getAllValues().get(1).getResource()).isNotNull(); } @Test From 3e71e678a5721531016be030c6637e2b4a34d5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 5 Sep 2025 19:39:58 +0200 Subject: [PATCH 35/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventSources.java | 4 ++++ .../event/source/timer/TimerEventSource.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) 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 6a8b290c4f..5d711b6f4d 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 @@ -26,6 +26,10 @@ class EventSources

{ new TimerEventSource<>("RetryAndRescheduleTimerEventSource"); private ControllerEventSource

controllerEventSource; + public EventSources(boolean triggerReconcilerOnAllEvent) { + this.controllerEventSource = controllerEventSource; + } + public void add(EventSource eventSource) { final var name = eventSource.name(); var existing = sourceByName.get(name); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index 53c0d328a8..3f4d136f6c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -21,13 +21,15 @@ public class TimerEventSource extends AbstractEventSource private Timer timer; private final Map onceTasks = new ConcurrentHashMap<>(); + private boolean triggerReconcilerOnAllEvent; public TimerEventSource() { super(Void.class); } - public TimerEventSource(String name) { + public TimerEventSource(String name, boolean triggerReconcilerOnAllEvent) { super(Void.class, name); + this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent; } @SuppressWarnings("unused") @@ -50,7 +52,11 @@ public void scheduleOnce(ResourceID resourceID, long delay) { @Override public void onResourceDeleted(R resource) { - cancelOnceSchedule(ResourceID.fromResource(resource)); + // for triggerReconcilerOnAllEvent the cancelOnceSchedule will be called on + // successful delete event processing + if (!triggerReconcilerOnAllEvent) { + cancelOnceSchedule(ResourceID.fromResource(resource)); + } } public void cancelOnceSchedule(ResourceID customResourceUid) { From e52956728b7ebe1d1edf2b58dfcef91a78564612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 5 Sep 2025 19:48:29 +0200 Subject: [PATCH 36/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventSourceManager.java | 4 +++- .../operator/processing/event/EventSources.java | 10 +++++++--- .../processing/event/ReconciliationDispatcherTest.java | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) 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 8b07bf110b..c96d5b1ea2 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 @@ -38,7 +38,9 @@ public class EventSourceManager

private final ExecutorServiceManager executorServiceManager; public EventSourceManager(Controller

controller) { - this(controller, new EventSources<>()); + this( + controller, + new EventSources<>(controller.getConfiguration().triggerReconcilerOnAllEvent())); } EventSourceManager(Controller

controller, EventSources

eventSources) { 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 5d711b6f4d..f870b63962 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 @@ -22,12 +22,16 @@ class EventSources

{ new ConcurrentSkipListMap<>(); private final Map sourceByName = new HashMap<>(); - private final TimerEventSource

retryAndRescheduleTimerEventSource = - new TimerEventSource<>("RetryAndRescheduleTimerEventSource"); + private final TimerEventSource

retryAndRescheduleTimerEventSource; private ControllerEventSource

controllerEventSource; public EventSources(boolean triggerReconcilerOnAllEvent) { - this.controllerEventSource = controllerEventSource; + retryAndRescheduleTimerEventSource = + new TimerEventSource<>("RetryAndRescheduleTimerEventSource", triggerReconcilerOnAllEvent); + } + + EventSources() { + this(false); } public void add(EventSource eventSource) { 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 d89b250cf1..4b13f2e29d 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 @@ -710,7 +710,7 @@ void reconcilerContextUsesTheSameInstanceOfResourceAsParam() { } @Test - void allEventModeNoReSchedulesAllowedForDeleteEvent() {} + void procAllEventModeNoReSchedulesAllowedForDeleteEvent() {} private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); From fd682446d1380e0c9f3bbed0e90d689f1e0ef227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 16 Sep 2025 10:29:58 +0200 Subject: [PATCH 37/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/EventProcessorTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 441280c570..860f843261 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 @@ -605,7 +605,9 @@ void processesAdditionalEventWhileInDeleteModeRetry() { spy( new EventProcessor( controllerConfigTriggerAllEvent( - GenericRetry.DEFAULT.setInitialInterval(1000).setMaxAttempts(-1), + GenericRetry.defaultLimitedExponentialRetry() + .setInitialInterval(1000) + .setMaxAttempts(-1), rateLimiterMock), reconciliationDispatcherMock, eventSourceManagerMock, @@ -634,7 +636,7 @@ void afterRetryExhaustedAdditionalEventTriggerReconciliationWhenDeleteEventPrese spy( new EventProcessor( controllerConfigTriggerAllEvent( - GenericRetry.DEFAULT + GenericRetry.defaultLimitedExponentialRetry() .setInitialInterval(100) .setIntervalMultiplier(1) .setMaxAttempts(1), From 7f61bbc2e5f1162ef2b69419861eb3ddfb7f116e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 16 Sep 2025 11:39:40 +0200 Subject: [PATCH 38/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/Controller.java | 15 ++++++ .../operator/processing/ControllerTest.java | 53 +++++++++++++++++++ 2 files changed, 68 insertions(+) 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 80af4fc61a..851c6b011d 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 @@ -61,6 +61,11 @@ public class Controller

private static final String RESOURCE = "resource"; private static final String STATUS = "status"; private static final String BOTH = "both"; + public static final String CLEANER_NOT_SUPPORTED_ON_ALL_EVENT_ERROR_MESSAGE = + "Cleaner is not supported when triggerReconcilerOnAllEvent enabled."; + public static final String + MANAGED_WORKFLOWS_NOT_SUPPORTED_TRIGGER_RECONCILER_ON_ALL_EVENT_ERROR_MESSAGE = + "Managed workflows are not supported when triggerReconcilerOnAllEvent enabled."; private final Reconciler

reconciler; private final ControllerConfiguration

configuration; @@ -97,6 +102,16 @@ public Controller( explicitWorkflowInvocation = configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation).orElse(false); + if (configuration.triggerReconcilerOnAllEvent()) { + if (isCleaner) { + throw new OperatorException(CLEANER_NOT_SUPPORTED_ON_ALL_EVENT_ERROR_MESSAGE); + } + if (managedWorkflow != null && !managedWorkflow.isEmpty()) { + throw new OperatorException( + MANAGED_WORKFLOWS_NOT_SUPPORTED_TRIGGER_RECONCILER_ON_ALL_EVENT_ERROR_MESSAGE); + } + } + eventSourceManager = new EventSourceManager<>(this); eventProcessor = new EventProcessor<>(eventSourceManager, configurationService); eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index ca14fbc76b..10ddc79dfd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; @@ -23,7 +24,10 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.api.monitoring.Metrics.NOOP; +import static io.javaoperatorsdk.operator.processing.Controller.CLEANER_NOT_SUPPORTED_ON_ALL_EVENT_ERROR_MESSAGE; +import static io.javaoperatorsdk.operator.processing.Controller.MANAGED_WORKFLOWS_NOT_SUPPORTED_TRIGGER_RECONCILER_ON_ALL_EVENT_ERROR_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -76,6 +80,54 @@ void usesFinalizerIfThereIfReconcilerImplementsCleaner() { assertThat(controller.useFinalizer()).isTrue(); } + @Test + void cleanerNotAllowedWithTriggerOnAllEventEnabled() { + Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); + final var configuration = MockControllerConfiguration.forResource(Secret.class); + when(configuration.getConfigurationService()).thenReturn(new BaseConfigurationService()); + when(configuration.triggerReconcilerOnAllEvent()).thenReturn(true); + + var exception = + assertThrows( + OperatorException.class, + () -> + new Controller( + reconciler, configuration, MockKubernetesClient.client(Secret.class))); + + assertThat(exception.getMessage()).isEqualTo(CLEANER_NOT_SUPPORTED_ON_ALL_EVENT_ERROR_MESSAGE); + } + + @Test + void managedWorkflowNotAllowedWithOnAllEventEnabled() { + Reconciler reconciler = mock(Reconciler.class); + final var configuration = MockControllerConfiguration.forResource(Secret.class); + + var configurationService = mock(ConfigurationService.class); + var mockWorkflowFactory = mock(ManagedWorkflowFactory.class); + var mockManagedWorkflow = mock(ManagedWorkflow.class); + + when(configuration.getConfigurationService()).thenReturn(configurationService); + var workflowSpec = mock(WorkflowSpec.class); + when(configuration.getWorkflowSpec()).thenReturn(Optional.of(workflowSpec)); + when(configurationService.getMetrics()).thenReturn(NOOP); + when(configurationService.getWorkflowFactory()).thenReturn(mockWorkflowFactory); + when(mockWorkflowFactory.workflowFor(any())).thenReturn(mockManagedWorkflow); + var managedWorkflowMock = workflow(true); + when(mockManagedWorkflow.resolve(any(), any())).thenReturn(managedWorkflowMock); + + when(configuration.triggerReconcilerOnAllEvent()).thenReturn(true); + + var exception = + assertThrows( + OperatorException.class, + () -> + new Controller( + reconciler, configuration, MockKubernetesClient.client(Secret.class))); + + assertThat(exception.getMessage()) + .isEqualTo(MANAGED_WORKFLOWS_NOT_SUPPORTED_TRIGGER_RECONCILER_ON_ALL_EVENT_ERROR_MESSAGE); + } + @ParameterizedTest @CsvSource({ "true, true, true, false", @@ -132,6 +184,7 @@ private Workflow workflow(boolean hasCleaner) { var workflow = mock(Workflow.class); when(workflow.cleanup(any(), any())).thenReturn(mock(WorkflowCleanupResult.class)); when(workflow.hasCleaner()).thenReturn(hasCleaner); + when(workflow.isEmpty()).thenReturn(false); return workflow; } } From 71da180a88f5424f4051d2467ad56a473feb6a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 16 Sep 2025 11:42:53 +0200 Subject: [PATCH 39/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../{FinalizerUtils.java => ControllerUtils.java} | 4 ++-- .../TriggerReconcilerOnAllEventReconciler.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/{FinalizerUtils.java => ControllerUtils.java} (92%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java index 4b7ddc6c4c..722bde3ce2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/FinalizerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java @@ -5,9 +5,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; -public class FinalizerUtils { +public class ControllerUtils { - private static final Logger log = LoggerFactory.getLogger(FinalizerUtils.class); + private static final Logger log = LoggerFactory.getLogger(ControllerUtils.class); // todo SSA, revisit if informer is ok for this diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java index b3ffec2ef2..873bc77e6b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.FinalizerUtils; +import io.javaoperatorsdk.operator.api.reconciler.ControllerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -53,7 +53,7 @@ public UpdateControl reconcile( if (!primary.isMarkedForDeletion() && getUseFinalizer() && !primary.hasFinalizer(FINALIZER)) { log.info("Adding finalizer"); - FinalizerUtils.patchFinalizer(primary, FINALIZER, context); + ControllerUtils.patchFinalizer(primary, FINALIZER, context); return UpdateControl.noUpdate(); } @@ -76,7 +76,7 @@ public UpdateControl reconcile( setEventOnMarkedForDeletion(true); if (getUseFinalizer() && primary.hasFinalizer(FINALIZER)) { log.info("Removing finalizer"); - FinalizerUtils.removeFinalizer(primary, FINALIZER, context); + ControllerUtils.removeFinalizer(primary, FINALIZER, context); } } From ed8e901a292f936ce721c48a26147417d09d8702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 16 Sep 2025 14:47:05 +0200 Subject: [PATCH 40/41] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/ControllerUtils.java | 57 ------ .../PrimaryUpdateAndCacheUtils.java | 167 ++++++++++++++++++ ...TriggerReconcilerOnAllEventReconciler.java | 6 +- 3 files changed, 170 insertions(+), 60 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java deleted file mode 100644 index 722bde3ce2..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.HasMetadata; - -public class ControllerUtils { - - private static final Logger log = LoggerFactory.getLogger(ControllerUtils.class); - - // todo SSA, revisit if informer is ok for this - - public static

P patchFinalizer( - P resource, String finalizer, Context

context) { - - if (resource.hasFinalizer(finalizer)) { - log.debug("Skipping adding finalizer, since already present."); - return resource; - } - - return PrimaryUpdateAndCacheUtils.updateAndCacheResource( - resource, - context, - r -> r, - r -> - context - .getClient() - .resource(r) - .edit( - res -> { - res.addFinalizer(finalizer); - return res; - })); - } - - public static

P removeFinalizer( - P resource, String finalizer, Context

context) { - if (!resource.hasFinalizer(finalizer)) { - log.debug("Skipping removing finalizer, since not present."); - return resource; - } - return PrimaryUpdateAndCacheUtils.updateAndCacheResource( - resource, - context, - r -> r, - r -> - context - .getClient() - .resource(r) - .edit( - res -> { - res.removeFinalizer(finalizer); - return res; - })); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java index c61cc837c1..28bd08b110 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.lang.reflect.InvocationTargetException; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.function.UnaryOperator; @@ -8,12 +9,17 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; 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.processing.event.ResourceID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + /** * Utility methods to patch the primary resource state and store it to the related cache, to make * sure that the latest version of the resource is present for the next reconciliation. The main use @@ -229,4 +235,165 @@ private static

P pollLocalCache( throw new OperatorException(e); } } + + /** Adds finalizer using JSON Patch. Retries conflicts and unprocessable content (HTTP 422) */ + @SuppressWarnings("unchecked") + public static

P addFinalizer( + Context

context, P resource, String finalizerName) { + if (log.isDebugEnabled()) { + log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); + } + int retryIndex = 0; + while (true) { + try { + if (resource.hasFinalizer(finalizerName)) { + return resource; + } + return context + .getClient() + .resource(resource) + .edit( + r -> { + r.addFinalizer(finalizerName); + return r; + }); + } catch (KubernetesClientException e) { + log.trace("Exception during patch for resource: {}", resource); + retryIndex++; + // only retry on conflict (409) and unprocessable content (422) which + // can happen if JSON Patch is not a valid request since there was + // a concurrent request which already removed another finalizer: + // List element removal from a list is by index in JSON Patch + // so if addressing a second finalizer but first is meanwhile removed + // it is a wrong request. + if (e.getCode() != 409 && e.getCode() != 422) { + throw e; + } + if (retryIndex >= DEFAULT_MAX_RETRY) { + throw new OperatorException( + "Exceeded maximum (" + + DEFAULT_MAX_RETRY + + ") retry attempts to patch resource: " + + ResourceID.fromResource(resource)); + } + log.debug( + "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", + resource.getMetadata().getName(), + resource.getMetadata().getNamespace(), + e.getCode()); + var operation = context.getClient().resources(resource.getClass()); + if (resource.getMetadata().getNamespace() != null) { + resource = + (P) + operation + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getMetadata().getName()) + .get(); + } else { + resource = (P) operation.withName(resource.getMetadata().getName()).get(); + } + } + } + } + + /** Adds finalizer using Server-Side Apply. */ + public static

P addFinalizerWithSSA( + Context

context, P originalResource, String finalizerName) { + return addFinalizerWithSSA( + context.getClient(), + originalResource, + finalizerName, + context.getControllerConfiguration().fieldManager()); + } + + /** Adds finalizer using Server-Side Apply. */ + public static

P addFinalizerWithSSA( + KubernetesClient client, P originalResource, String finalizerName, String fieldManager) { + log.debug( + "Adding finalizer (using SSA) for resource: {} version: {}", + getUID(originalResource), + getVersion(originalResource)); + try { + P resource = (P) originalResource.getClass().getConstructor().newInstance(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName(originalResource.getMetadata().getName()); + objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); + resource.setMetadata(objectMeta); + resource.addFinalizer(finalizerName); + return client + .resource(resource) + .patch( + new PatchContext.Builder() + .withFieldManager(fieldManager) + .withForce(true) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException( + "Issue with creating custom resource instance with reflection." + + " Custom Resources must provide a no-arg constructor. Class: " + + originalResource.getClass().getName(), + e); + } + } + + // todo + public static

P removeFinalizer() { + return null; + } + + /** + * Experimental. Patches finalizer. For retry uses informer cache to get the fresh resources. + * Therefore makes less Kubernetes API Calls. + */ + public static

P addFinalizer( + P resource, String finalizer, Context

context) { + + if (resource.hasFinalizer(finalizer)) { + log.debug("Skipping adding finalizer, since already present."); + return resource; + } + + return updateAndCacheResource( + resource, + context, + r -> r, + r -> + context + .getClient() + .resource(r) + .edit( + res -> { + res.addFinalizer(finalizer); + return res; + })); + } + + /** + * Experimental. Removes finalizer, for retry uses informer cache to get the fresh resources. + * Therefore makes less Kubernetes API Calls. + */ + public static

P removeFinalizer( + P resource, String finalizer, Context

context) { + if (!resource.hasFinalizer(finalizer)) { + log.debug("Skipping removing finalizer, since not present."); + return resource; + } + return updateAndCacheResource( + resource, + context, + r -> r, + r -> + context + .getClient() + .resource(r) + .edit( + res -> { + res.removeFinalizer(finalizer); + return res; + })); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java index 873bc77e6b..eb9b7759dd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ControllerUtils; +import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -53,7 +53,7 @@ public UpdateControl reconcile( if (!primary.isMarkedForDeletion() && getUseFinalizer() && !primary.hasFinalizer(FINALIZER)) { log.info("Adding finalizer"); - ControllerUtils.patchFinalizer(primary, FINALIZER, context); + PrimaryUpdateAndCacheUtils.addFinalizer(primary, FINALIZER, context); return UpdateControl.noUpdate(); } @@ -76,7 +76,7 @@ public UpdateControl reconcile( setEventOnMarkedForDeletion(true); if (getUseFinalizer() && primary.hasFinalizer(FINALIZER)) { log.info("Removing finalizer"); - ControllerUtils.removeFinalizer(primary, FINALIZER, context); + PrimaryUpdateAndCacheUtils.removeFinalizer(primary, FINALIZER, context); } } From fd38e9ed8c035264dff7583d41906acc5279e8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 10:45:59 +0200 Subject: [PATCH 41/41] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../en/docs/documentation/reconciler.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/content/en/docs/documentation/reconciler.md b/docs/content/en/docs/documentation/reconciler.md index 3ea09cf167..061ca340a2 100644 --- a/docs/content/en/docs/documentation/reconciler.md +++ b/docs/content/en/docs/documentation/reconciler.md @@ -210,3 +210,43 @@ called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or updated via `PrimaryUpdateAndCacheUtils`. See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache). + +### Trigger reconciliation on all events + +TLDR; We provide an execution mode where `reconcile` method is called on every event from event source. + +The framework optimizes execution for generic use cases, which in almost all cases falls into two categories: + +1. The controller does not use finalizers; thus when the primary resource is deleted, all the managed secondary + resources are cleaned up using the Kubernetes garbage collection mechanism, a.k.a., using owner references. +2. When finalizers are used (using `Cleaner` interface), thus when controller requires some explicit cleanup logic, typically for external + resources and when secondary resources are in different namespace than the primary resources (owner references + cannot be used in this case). + +Note that for example framework neither of those cases triggers the reconciler on the `Delete` event of the primary resource. +When finalizer is used, it calls `cleaner(..)` method when resource is marked for deletion and our (not other) finalizer +is present. When there is no finalizer, does not make sense to call the `reconciel(..)` method on a `Delete` event +since all the cleanup will be done by the garbage collector. This way we spare reconciliation cycles. + +However, there are cases when controllers do not strictly follow those patterns, typically when: +- Only some of the primary resources use finalizers, e.g., for some of the primary resources you need + to create an external resource for others not. +- You maintain some additional in memory caches (so not all the caches are encapsulated by an `EventSource`) + and you don't want to use finalizers. For those cases, you typically want to clean up your caches when the primary + resource is deleted. + +For such use cases you can set [`triggerReconcilerOnAllEvent`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L81) +to `true`, as a result, `reconcile` method will be triggered on ALL events (so also `Delete` events), and you +are free to optimize you reconciliation for the use cases above and possibly others. + +In this mode: +- you cannot use `Cleaner` interface. The framework assumes you will explicitly manage the finalizers. To + add finalizer you can use [`PrimeUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java#L308). +- even if the primary resource is already deleted from the Informer's cache, we will still pass the last known state + as the parameter for the reconciler. You can check if the resource is deleted using `Context.isPrimaryResourceDeleted()`. +- The retry, rate limiting, re-schedule mechanisms work normally. (The internal caches related to the resource + are cleaned up only when there was a successful reconiliation after `Delete` event received for the primary resource + and reconciliation was not re-scheduled. +- you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal + execution mode. + \ No newline at end of file