From 3fa213676f115a6795492296650fe591a7981167 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/61] 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 10c931b529aaecf3a451a14c37f9a7ea2395f5d0 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/61] 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 f73fbc78f74bf4d7fcc0756366ddaded9c945233 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/61] 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 ae9ab80b0b2ebc5b3b0f0bcb432e80f0d33c60ca 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/61] 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 27f009b74916d90e5f6da48782555e81dfb4a8e0 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/61] 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 dda1311fb2395fdfafb03bdda13b359a33480ee1 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/61] 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 7471d3ba21e064e723653afbf648dfd7418b13b6 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/61] 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 9433cf86ff1bf8cc795e4fa3d84a4acb4b8b7e11 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/61] 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 02711c3df8491cb0c69cf0075c55e58e136593d8 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/61] 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 cc10c4d0b3821cf0fa56a3e21faf7862a5b48093 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/61] 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 9b13134ad1b502ef46f6482800262205551078bd 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/61] 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 6d1b37412d51d50baea8d09d02a117ecbeb939ba 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/61] 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 0fb71d4f9d87ff55a9cdd2c28f13d3034a3256d6 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/61] 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 8ed16a7dca4ac919de6bdfc97de3449823d560df 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/61] 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 6cdfd1db68975933478e78c8c59221f81a834c61 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/61] 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 9e1b28ff18d14b07bb5ef022e3c91e062de0af68 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/61] 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 bdffb647cb77d81b1524726bf9a9c85f23608c75 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/61] 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 ad8c37c5574d16f715929a79702f4e4eabd6dbde 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/61] 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 962ea4f7403dd15c843fd3bc628f98df0621405d 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/61] 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 ff1379127ad3d5810ce40330d33f136b88cb920f 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/61] 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 ac01a98f42540a6fe203156ef32da368a61034b4 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/61] 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 b62dfcb2ee7c0c8aa7415a266fe2eb9f42c39dc7 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/61] 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 3c9ed9d601d0b591004110c6314af4ddca449ac7 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/61] 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 b8d7daeaa5c93aa6ca60c4d7f744a57af6f7be83 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/61] 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 11e08c18a03fbaa467247e9bc44566c70bbd690b 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/61] 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 14255c6a58b49b8493889444c9c2c88fdd8e6f3d 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/61] 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 e210e6498c422ed5f426dc2fc6b8bda4be2c3a83 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/61] 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 fa28ca3bbc778d4384831dd15fb22ef4381b72cb 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/61] =?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 98e8a8c14053f76662ce1565e3550003f9d36d9a 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/61] =?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 8b00785fd9643a0f5e5530807230e6cb02811413 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/61] 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 1a633fc5f15a2ee2e45fd55173ca24dfe330b819 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/61] 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 7c4201cf6e51038188e6b2c64c9a612664d2c07f 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/61] 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 ddbb1cbc13f46062c2defc844056ad9c71230220 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/61] 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 b50c3807ea58fb7dc655668bd3a43674068c9474 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/61] 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 8ae1e59e6acdfb9fea60b8690b5a2dfd82b9fdb9 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/61] 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 0b93bdc9d4ef42f5049d776d7bf28d0bb44c0dcb 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/61] 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 51614794f148b57783e2ecf7fd36d3e0d0758836 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/61] 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 8c10ddc5fc54d6dbb033b6bfffe187a758005b05 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/61] 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 c2b91bc8312accc75c27d7c136bfeb9541b454ec 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/61] 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 adb35710758c17b4ec876a4de52bc63e25b05747 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/61] 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 3d1895da3e3a61dacaa8a7d76c3d5ed6eb99e4f4 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/61] 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 From 94c4c74dde8a96f21970eb40734c38eaa0817311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 12:42:43 +0200 Subject: [PATCH 42/61] javadoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- docs/content/en/docs/documentation/reconciler.md | 7 ++++--- .../operator/api/reconciler/Context.java | 13 +++++++++++++ .../api/reconciler/ControllerConfiguration.java | 5 +++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/content/en/docs/documentation/reconciler.md b/docs/content/en/docs/documentation/reconciler.md index 061ca340a2..c46172eb04 100644 --- a/docs/content/en/docs/documentation/reconciler.md +++ b/docs/content/en/docs/documentation/reconciler.md @@ -240,13 +240,14 @@ to `true`, as a result, `reconcile` method will be triggered on ALL events (so a 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 +- The retry, rate limiting, re-schedule, filters 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 `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). - 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 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 f7e9c3763c..36df3666e6 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,20 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { */ boolean isNextReconciliationImminent(); + /** + * To check if the primary resource is already deleted. This value can be true only if you turn on + * {@link + * io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration#triggerReconcilerOnAllEvent()} + * + * @return true Delete event received for primary resource + */ boolean isPrimaryResourceDeleted(); + /** + * Check this only if {@link #isPrimaryResourceDeleted()} is true. + * + * @return true if the primary resource is deleted, but the last known state is only available + * from the caches of the underlying Informer, not from Delete event. + */ boolean isPrimaryResourceFinalStateUnknown(); } 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 21b4c16b11..db0f799172 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,10 @@ MaxReconciliationInterval maxReconciliationInterval() default */ String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER; + /** + * By settings to true, reconcile method will be triggered on every event, thus even for Delete + * event. You cannot use {@link Cleaner} or managed dependent resources in that case. See + * documentation for further details. + */ boolean triggerReconcilerOnAllEvent() default false; } From c5ff462ac107ed75525eb8f289307588a7a8ca68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 12:59:44 +0200 Subject: [PATCH 43/61] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../PrimaryUpdateAndCacheUtils.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) 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 28bd08b110..e8fcc7d644 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 @@ -239,7 +239,7 @@ private static

P pollLocalCache( /** Adds finalizer using JSON Patch. Retries conflicts and unprocessable content (HTTP 422) */ @SuppressWarnings("unchecked") public static

P addFinalizer( - Context

context, P resource, String finalizerName) { + KubernetesClient client, P resource, String finalizerName) { if (log.isDebugEnabled()) { log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); } @@ -249,8 +249,7 @@ public static

P addFinalizer( if (resource.hasFinalizer(finalizerName)) { return resource; } - return context - .getClient() + return client .resource(resource) .edit( r -> { @@ -281,7 +280,7 @@ public static

P addFinalizer( resource.getMetadata().getName(), resource.getMetadata().getNamespace(), e.getCode()); - var operation = context.getClient().resources(resource.getClass()); + var operation = client.resources(resource.getClass()); if (resource.getMetadata().getNamespace() != null) { resource = (P) @@ -307,12 +306,15 @@ public static

P addFinalizerWithSSA( } /** Adds finalizer using Server-Side Apply. */ + @SuppressWarnings("unchecked") 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)); + if (log.isDebugEnabled()) { + 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(); @@ -340,14 +342,13 @@ public static

P addFinalizerWithSSA( } } - // 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. + * 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) { @@ -373,8 +374,8 @@ public static

P addFinalizer( } /** - * Experimental. Removes finalizer, for retry uses informer cache to get the fresh resources. - * Therefore makes less Kubernetes API Calls. + * 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) { From b89015310ff8d83edf0e121c90cc965185ac5d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 13:14:24 +0200 Subject: [PATCH 44/61] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../PrimaryUpdateAndCacheUtils.java | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) 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 e8fcc7d644..43c1523782 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 @@ -3,6 +3,7 @@ import java.lang.reflect.InvocationTargetException; import java.time.LocalTime; import java.time.temporal.ChronoUnit; +import java.util.function.Predicate; import java.util.function.UnaryOperator; import org.slf4j.Logger; @@ -158,7 +159,7 @@ public static

P updateAndCacheResource( long cachePollPeriodMillis) { if (log.isDebugEnabled()) { - log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resourceToUpdate)); + log.debug("Update and cache: {}", ResourceID.fromResource(resourceToUpdate)); } P modified = null; int retryIndex = 0; @@ -240,22 +241,44 @@ private static

P pollLocalCache( @SuppressWarnings("unchecked") public static

P addFinalizer( KubernetesClient client, P resource, String finalizerName) { + return conflictRetryingPatch( + client, + resource, + r -> { + r.addFinalizer(finalizerName); + return r; + }, + r -> !r.hasFinalizer(finalizerName)); + } + + public static

P removeFinalizer( + KubernetesClient client, P resource, String finalizerName) { + return conflictRetryingPatch( + client, + resource, + r -> { + r.removeFinalizer(finalizerName); + return r; + }, + r -> r.hasFinalizer(finalizerName)); + } + + @SuppressWarnings("unchecked") + public static

P conflictRetryingPatch( + KubernetesClient client, + P resource, + UnaryOperator

unaryOperator, + Predicate

preCondition) { if (log.isDebugEnabled()) { log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); } int retryIndex = 0; while (true) { try { - if (resource.hasFinalizer(finalizerName)) { + if (!preCondition.test(resource)) { return resource; } - return client - .resource(resource) - .edit( - r -> { - r.addFinalizer(finalizerName); - return r; - }); + return client.resource(resource).edit(unaryOperator); } catch (KubernetesClientException e) { log.trace("Exception during patch for resource: {}", resource); retryIndex++; @@ -342,14 +365,13 @@ public static

P addFinalizerWithSSA( } } - 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. */ + @Experimental( + "Not used internally for now. Therefor we don't consider it well tested. But the intention is" + + " to have it as default in the future.") public static

P addFinalizer( P resource, String finalizer, Context

context) { @@ -377,6 +399,9 @@ public static

P addFinalizer( * Experimental. Removes finalizer, for retry uses informer cache to get the fresh resources, * therefore makes less Kubernetes API Calls. */ + @Experimental( + "Not used internally for now. Therefor we don't consider it well tested. But the intention is" + + " to have it as default in the future.") public static

P removeFinalizer( P resource, String finalizer, Context

context) { if (!resource.hasFinalizer(finalizer)) { From 3930a379912ae16d6b7a2b40c4fe145c0280fda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 13:25:40 +0200 Subject: [PATCH 45/61] 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/PrimaryUpdateAndCacheUtils.java | 11 +++++++++++ .../TriggerReconcilerOnAllEventReconciler.java | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) 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 43c1523782..99f2f8d829 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 @@ -237,6 +237,12 @@ private static

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

P addFinalizer(Context

context, String finalizer) { + return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer); + } + /** Adds finalizer using JSON Patch. Retries conflicts and unprocessable content (HTTP 422) */ @SuppressWarnings("unchecked") public static

P addFinalizer( @@ -251,6 +257,11 @@ public static

P addFinalizer( r -> !r.hasFinalizer(finalizerName)); } + public static

P removeFinalizer( + Context

context, String finalizerName) { + return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName); + } + public static

P removeFinalizer( KubernetesClient client, P resource, String finalizerName) { return conflictRetryingPatch( 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 eb9b7759dd..5dee44a300 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 @@ -53,7 +53,7 @@ public UpdateControl reconcile( if (!primary.isMarkedForDeletion() && getUseFinalizer() && !primary.hasFinalizer(FINALIZER)) { log.info("Adding finalizer"); - PrimaryUpdateAndCacheUtils.addFinalizer(primary, FINALIZER, context); + PrimaryUpdateAndCacheUtils.addFinalizer(context, FINALIZER); return UpdateControl.noUpdate(); } @@ -76,7 +76,7 @@ public UpdateControl reconcile( setEventOnMarkedForDeletion(true); if (getUseFinalizer() && primary.hasFinalizer(FINALIZER)) { log.info("Removing finalizer"); - PrimaryUpdateAndCacheUtils.removeFinalizer(primary, FINALIZER, context); + PrimaryUpdateAndCacheUtils.removeFinalizer(context, FINALIZER); } } From cb1baf53c939424d625c117081880b05c5a02659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 13:26:09 +0200 Subject: [PATCH 46/61] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../PrimaryUpdateAndCacheUtils.java | 58 ------------------- 1 file changed, 58 deletions(-) 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 99f2f8d829..18664e606c 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 @@ -375,62 +375,4 @@ public static

P addFinalizerWithSSA( e); } } - - /** - * Experimental. Patches finalizer. For retry uses informer cache to get the fresh resources, - * therefore makes less Kubernetes API Calls. - */ - @Experimental( - "Not used internally for now. Therefor we don't consider it well tested. But the intention is" - + " to have it as default in the future.") - 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. - */ - @Experimental( - "Not used internally for now. Therefor we don't consider it well tested. But the intention is" - + " to have it as default in the future.") - 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; - })); - } } From 38004975373ddb2a25a00e3c3a7977ca7a7feea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 13:58:02 +0200 Subject: [PATCH 47/61] utils integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- operator-framework-core/pom.xml | 5 +++ ...aryUpdateAndCacheUtilsIntegrationTest.java | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index c99b609113..c5c2f54986 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -79,6 +79,11 @@ awaitility test + + io.fabric8 + kube-api-test-client-inject + test + diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java new file mode 100644 index 0000000000..1229df3760 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubeapitest.junit.EnableKubeAPIServer; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@EnableKubeAPIServer +class PrimaryUpdateAndCacheUtilsIntegrationTest { + + public static final String DEFAULT_NS = "default"; + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String FINALIZER = "int.test/finalizer"; + static KubernetesClient client; + + @Test + void testFinalizerAddAndRemoval() { + var cm = + client + .resource( + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withNamespace(DEFAULT_NS) + .build()) + .build()) + .create(); + + PrimaryUpdateAndCacheUtils.addFinalizer(client, cm, FINALIZER); + + cm = client.configMaps().inNamespace(DEFAULT_NS).withName(TEST_RESOURCE_NAME).get(); + assertThat(cm.getMetadata().getFinalizers()).containsExactly(FINALIZER); + + PrimaryUpdateAndCacheUtils.removeFinalizer(client, cm, FINALIZER); + + cm = client.configMaps().inNamespace(DEFAULT_NS).withName(TEST_RESOURCE_NAME).get(); + assertThat(cm.getMetadata().getFinalizers()).isEmpty(); + client.resource(cm).delete(); + } +} From 18e6a40afa51e6c9820ba4a3e3238336df21cf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 17 Sep 2025 14:05:49 +0200 Subject: [PATCH 48/61] test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- ...aryUpdateAndCacheUtilsIntegrationTest.java | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java index 1229df3760..86cf6d2b22 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsIntegrationTest.java @@ -1,8 +1,11 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Map; + import org.junit.jupiter.api.Test; import io.fabric8.kubeapitest.junit.EnableKubeAPIServer; +import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; @@ -19,27 +22,55 @@ class PrimaryUpdateAndCacheUtilsIntegrationTest { @Test void testFinalizerAddAndRemoval() { - var cm = - client - .resource( - new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(TEST_RESOURCE_NAME) - .withNamespace(DEFAULT_NS) - .build()) - .build()) - .create(); - + var cm = createConfigMap(); PrimaryUpdateAndCacheUtils.addFinalizer(client, cm, FINALIZER); - cm = client.configMaps().inNamespace(DEFAULT_NS).withName(TEST_RESOURCE_NAME).get(); + cm = getTestConfigMap(); assertThat(cm.getMetadata().getFinalizers()).containsExactly(FINALIZER); PrimaryUpdateAndCacheUtils.removeFinalizer(client, cm, FINALIZER); - cm = client.configMaps().inNamespace(DEFAULT_NS).withName(TEST_RESOURCE_NAME).get(); + cm = getTestConfigMap(); assertThat(cm.getMetadata().getFinalizers()).isEmpty(); client.resource(cm).delete(); } + + private static ConfigMap getTestConfigMap() { + return client.configMaps().inNamespace(DEFAULT_NS).withName(TEST_RESOURCE_NAME).get(); + } + + @Test + void testFinalizerAddRetryOnOptimisticLockFailure() { + var cm = createConfigMap(); + // update resource, so it has a new version on the server + cm.setData(Map.of("k", "v")); + client.resource(cm).update(); + + PrimaryUpdateAndCacheUtils.addFinalizer(client, cm, FINALIZER); + + cm = getTestConfigMap(); + assertThat(cm.getMetadata().getFinalizers()).containsExactly(FINALIZER); + + cm.setData(Map.of("k2", "v2")); + client.resource(cm).update(); + + PrimaryUpdateAndCacheUtils.removeFinalizer(client, cm, FINALIZER); + cm = getTestConfigMap(); + assertThat(cm.getMetadata().getFinalizers()).isEmpty(); + + client.resource(cm).delete(); + } + + private static ConfigMap createConfigMap() { + return client + .resource( + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withNamespace(DEFAULT_NS) + .build()) + .build()) + .create(); + } } From c4b5371fc50ea1771c2c59cc6d87ca6ba84d1aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 24 Sep 2025 11:39:34 +0200 Subject: [PATCH 49/61] fixes for code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../PrimaryUpdateAndCacheUtils.java | 13 ++++++++++-- .../processing/event/EventProcessor.java | 15 ++++++++----- .../event/ReconciliationDispatcher.java | 11 +++++++++- .../processing/event/ResourceState.java | 2 +- .../controller/ControllerEventSource.java | 6 +++--- .../controller/ResourceDeleteEvent.java | 20 ++++++++++++++++++ .../event/ReconciliationDispatcherTest.java | 21 ++++++++++++++++--- 7 files changed, 73 insertions(+), 15 deletions(-) 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 18664e606c..4ae59837fe 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 @@ -274,11 +274,20 @@ public static

P removeFinalizer( r -> r.hasFinalizer(finalizerName)); } + /** + * @param client KubernetesClient + * @param resource to update + * @param resourceChangesOperator changes to be done on the resource before update + * @param preCondition condition to check if the patch operation still needs to be performed or + * not. + * @return updated resource or unchanged if the precondition does not hold. + * @param

resource type + */ @SuppressWarnings("unchecked") public static

P conflictRetryingPatch( KubernetesClient client, P resource, - UnaryOperator

unaryOperator, + UnaryOperator

resourceChangesOperator, Predicate

preCondition) { if (log.isDebugEnabled()) { log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); @@ -289,7 +298,7 @@ public static

P conflictRetryingPatch( if (!preCondition.test(resource)) { return resource; } - return client.resource(resource).edit(unaryOperator); + return client.resource(resource).edit(resourceChangesOperator); } catch (KubernetesClientException e) { log.trace("Exception during patch for resource: {}", resource); retryIndex++; 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 4469cbc80e..2c583a0862 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 @@ -145,7 +145,7 @@ private void submitReconciliationExecution(ResourceState state) { Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution - && (maybeLatest.isPresent() || (triggerOnAllEvent() && state.deleteEventPresent()))) { + && (maybeLatest.isPresent() || isTriggerOnAllEventAndDeleteEventPresent(state))) { var rateLimit = state.getRateLimit(); if (rateLimit == null) { rateLimit = rateLimiter.initState(); @@ -228,7 +228,7 @@ private void handleEventMarking(Event event, ResourceState state) { } } else if (!state.deleteEventPresent() && !state.processedMarkForDeletionPresent()) { state.markEventReceived(triggerOnAllEvent()); - } else if (triggerOnAllEvent() && state.deleteEventPresent()) { + } else if (isTriggerOnAllEventAndDeleteEventPresent(state)) { state.markAdditionalEventAfterDeleteEvent(); } else if (log.isDebugEnabled()) { log.debug( @@ -285,7 +285,7 @@ synchronized void eventProcessingFinished( state.markProcessedMarkForDeletion(); metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { - if (state.eventPresent() || (triggerOnAllEvent() && state.deleteEventPresent())) { + if (state.eventPresent() || isTriggerOnAllEventAndDeleteEventPresent(state)) { log.debug("Submitting for reconciliation."); submitReconciliationExecution(state); } else { @@ -294,6 +294,10 @@ synchronized void eventProcessingFinished( } } + private boolean isTriggerOnAllEventAndDeleteEventPresent(ResourceState state) { + return triggerOnAllEvent() && state.deleteEventPresent(); + } + /** * In case retry is configured more complex error logging takes place, see handleRetryOnException */ @@ -502,15 +506,16 @@ public void run() { log.debug( "Resource not found in the cache, checking for delete event resource: {}", resourceID); - var state = resourceStateManager.get(resourceID); if (executionScope.isDeleteEvent()) { + var state = resourceStateManager.get(resourceID); actualResource = (Optional

) state .filter(ResourceState::deleteEventPresent) .map(ResourceState::getLastKnownResource); if (actualResource.isEmpty()) { - throw new IllegalStateException("this should not happen"); + throw new IllegalStateException( + "ActualResource should be always present, either from cache or delete event."); } } else { log.debug("Skipping execution since delete event received meanwhile"); 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 70b69f5ad2..0b1e210ce8 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 @@ -64,6 +64,7 @@ public ReconciliationDispatcher(Controller

controller) { } public PostExecutionControl

handleExecution(ExecutionScope

executionScope) { + validateExecutionScope(executionScope); try { return handleDispatch(executionScope); } catch (Exception e) { @@ -90,7 +91,6 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { originalResource.getMetadata().getFinalizers()); return PostExecutionControl.defaultDispatch(); } - Context

context = new DefaultContext<>( executionScope.getRetryInfo(), @@ -435,6 +435,15 @@ public P conflictRetryingPatch( } } + private void validateExecutionScope(ExecutionScope

executionScope) { + if (!triggerOnAllEvent() + && (executionScope.isDeleteEvent() || executionScope.isDeleteFinalStateUnknown())) { + throw new OperatorException( + "isDeleteEvent or isDeleteFinalStateUnknown cannot be true if not triggerOnAllEvent." + + " This indicates an issue with the implementation."); + } + } + // created to support unit testing static class CustomResourceFacade { 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 b7636818fa..2913749590 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 @@ -38,7 +38,7 @@ private enum EventingState { private EventingState eventing; private RateLimitState rateLimit; private HasMetadata lastKnownResource; - private boolean isDeleteFinalStateUnknown; + private boolean isDeleteFinalStateUnknown = false; public ResourceState(ResourceID id) { this.id = id; 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 a505a97702..76000b0d45 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 @@ -124,9 +124,9 @@ public void onUpdate(T oldCustomResource, T newCustomResource) { } @Override - public void onDelete(T resource, boolean b) { - super.onDelete(resource, b); - eventReceived(ResourceAction.DELETED, resource, null, b); + public void onDelete(T resource, boolean deletedFinalStateUnknown) { + super.onDelete(resource, deletedFinalStateUnknown); + eventReceived(ResourceAction.DELETED, resource, null, deletedFinalStateUnknown); } @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 index 73d856e922..1a25061a0e 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 @@ -1,8 +1,14 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; +import java.util.Objects; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; +/** + * Extends ResourceEvent for informer Delete events, it holds also information if the final stat is + * unknown for the deleted resource. + */ public class ResourceDeleteEvent extends ResourceEvent { private final boolean deletedFinalStateUnknown; @@ -19,4 +25,18 @@ public ResourceDeleteEvent( public boolean isDeletedFinalStateUnknown() { return deletedFinalStateUnknown; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ResourceDeleteEvent that = (ResourceDeleteEvent) o; + return deletedFinalStateUnknown == that.deletedFinalStateUnknown; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), deletedFinalStateUnknown); + } } 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 4b13f2e29d..8f1b087dd1 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 @@ -687,6 +687,24 @@ void reSchedulesFromErrorHandler() { assertThat(res.getRuntimeException()).isEmpty(); } + @Test + void isDeleteEventCannotBeTrueIfNotTriggerOnAllEvent() { + assertThrows( + OperatorException.class, + () -> + reconciliationDispatcher.handleExecution( + new ExecutionScope(null, null, true, false).setResource(testCustomResource))); + } + + @Test + void isDeleteFinalStateUnknownEventCannotBeTrueIfNotTriggerOnAllEvent() { + assertThrows( + OperatorException.class, + () -> + reconciliationDispatcher.handleExecution( + new ExecutionScope(null, null, false, true).setResource(testCustomResource))); + } + @Test void reconcilerContextUsesTheSameInstanceOfResourceAsParam() { initConfigService(false, false); @@ -709,9 +727,6 @@ void reconcilerContextUsesTheSameInstanceOfResourceAsParam() { .isNotSameAs(testCustomResource); } - @Test - void procAllEventModeNoReSchedulesAllowedForDeleteEvent() {} - private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); From 03410ae062227c83488e6c6830f836711a09e533 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 2 Oct 2025 14:28:01 +0200 Subject: [PATCH 50/61] docs: improve wording --- .../en/docs/documentation/reconciler.md | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/docs/content/en/docs/documentation/reconciler.md b/docs/content/en/docs/documentation/reconciler.md index c46172eb04..2ebca7aeb0 100644 --- a/docs/content/en/docs/documentation/reconciler.md +++ b/docs/content/en/docs/documentation/reconciler.md @@ -211,43 +211,50 @@ 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 +### Trigger reconciliation even for `Delete` 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: +The framework optimizes execution for generic use cases, which, in almost all cases, fall 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). +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. This + mechanism, however, only works when all secondary resources are Kubernetes resources in the same namespace as the + primary resource. +2. The controller uses finalizers (the controller implements the `Cleaner` interface), when explicit cleanup logic is + required, 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. +Note that neither of those cases trigger the `reconcile` method of the controller on the `Delete` event of the primary +resource. When a finalizer is used, the SDK calls the `cleanup` method of the `Cleaner` implementation when the resource +is marked for deletion and the finalizer specified by the controller is present on the primary resource. When there is +no finalizer, there is no need to call the `reconcile` method on a `Delete` event since all the cleanup will be done by +the garbage collector. This avoids 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 + +- 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. +to `true`, as a result, the `reconcile` method will be triggered on ALL events (so also `Delete` events), making it +possible to support the above use cases. In this mode: + - 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, filters 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 `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). + as the parameter for the reconciler. You can check if the resource is deleted using + `Context.isPrimaryResourceDeleted()`. +- The retry, rate limiting, re-schedule, filters mechanisms work normally. The internal caches related to the resource + are cleaned up only when there is a successful reconciliation after a `Delete` event was received for the primary + resource + and reconciliation is not re-scheduled. +- you cannot use the `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). - 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 From c787ce336ad334b82f6702d6f30d458257ab192a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 16:15:22 +0200 Subject: [PATCH 51/61] Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java Co-authored-by: Martin Stefanko --- .../operator/processing/event/ResourceState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2913749590..7b899a2711 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 @@ -108,7 +108,7 @@ public void markEventReceived(boolean isAllEventMode) { public void markAdditionalEventAfterDeleteEvent() { if (!deleteEventPresent()) { throw new IllegalStateException( - "Cannot mark additional event after delete event, if in current state not delete event" + "Cannot mark additional event after delete event, if in current state there is not delete event" + " present"); } log.debug("Marking additional event after delete event: {}", getId()); From 6a084c1a7e9cc7b5e9ddac734aca8e0da1a4bd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 16:15:33 +0200 Subject: [PATCH 52/61] Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceDeleteEvent.java Co-authored-by: Martin Stefanko --- .../processing/event/source/controller/ResourceDeleteEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1a25061a0e..e6e6d2ffa0 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 @@ -6,7 +6,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; /** - * Extends ResourceEvent for informer Delete events, it holds also information if the final stat is + * Extends ResourceEvent for informer Delete events, it holds also information if the final state is * unknown for the deleted resource. */ public class ResourceDeleteEvent extends ResourceEvent { From 7b050f0a8d18c7b8d7dfd259038548deb3458db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 16:19:18 +0200 Subject: [PATCH 53/61] format 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 7b899a2711..5496c37a00 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 @@ -108,8 +108,8 @@ public void markEventReceived(boolean isAllEventMode) { public void markAdditionalEventAfterDeleteEvent() { if (!deleteEventPresent()) { throw new IllegalStateException( - "Cannot mark additional event after delete event, if in current state there is not delete event" - + " present"); + "Cannot mark additional event after delete event, if in current state there is not delete" + + " event present"); } log.debug("Marking additional event after delete event: {}", getId()); eventing = EventingState.ADDITIONAL_EVENT_PRESENT_AFTER_DELETE_EVENT; From e8e6a3a3dcd3d4c4a17be110a8dbea0e62016008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 16:21:19 +0200 Subject: [PATCH 54/61] 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/EventProcessorTest.java | 3 --- 1 file changed, 3 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 860f843261..923cc2fee5 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 @@ -686,9 +686,6 @@ void passesResourceFromStateToDispatcher() { assertThat(captor.getAllValues().get(1).getResource()).isNotNull(); } - @Test - void onAllEventRateLimiting() {} - private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( From 4b80885900e1cf42a7a32d211cc7f9bd369182ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 16:31:30 +0200 Subject: [PATCH 55/61] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../PrimaryUpdateAndCacheUtils.java | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) 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 4ae59837fe..c119cf96c5 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 @@ -237,13 +237,26 @@ private static

P pollLocalCache( } } - /** Adds finalizer using JSON Patch. Retries conflicts and unprocessable content (HTTP 422) */ + /** + * Adds finalizer to the primary resource from the context using JSON Patch. Retries conflicts and + * unprocessable content (HTTP 422), see {@link + * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, + * Predicate)} for details on retry. + * + * @return updated resource from the server response + */ @SuppressWarnings("unchecked") public static

P addFinalizer(Context

context, String finalizer) { return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer); } - /** Adds finalizer using JSON Patch. Retries conflicts and unprocessable content (HTTP 422) */ + /** + * Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content + * (HTTP 422), see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, + * HasMetadata, UnaryOperator, Predicate)} for details on retry. + * + * @return updated resource from the server response + */ @SuppressWarnings("unchecked") public static

P addFinalizer( KubernetesClient client, P resource, String finalizerName) { @@ -257,6 +270,14 @@ public static

P addFinalizer( r -> !r.hasFinalizer(finalizerName)); } + /** + * Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and + * reties the operation if failed, see {@link + * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, + * Predicate)} for details. + * + * @return updated resource from the response from the server + */ public static

P removeFinalizer( Context

context, String finalizerName) { return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName); @@ -275,12 +296,16 @@ public static

P removeFinalizer( } /** + * Parches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or + * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in + * {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}. + * * @param client KubernetesClient * @param resource to update * @param resourceChangesOperator changes to be done on the resource before update * @param preCondition condition to check if the patch operation still needs to be performed or * not. - * @return updated resource or unchanged if the precondition does not hold. + * @return updated resource from the server or unchanged if the precondition does not hold. * @param

resource type */ @SuppressWarnings("unchecked") @@ -338,7 +363,13 @@ public static

P conflictRetryingPatch( } } - /** Adds finalizer using Server-Side Apply. */ + /** + * Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of + * the target resource, setting only name, namespace and finalizer. Does not use optimistic + * locking for the patch. + * + * @return the patched resource from the server response + */ public static

P addFinalizerWithSSA( Context

context, P originalResource, String finalizerName) { return addFinalizerWithSSA( @@ -348,7 +379,13 @@ public static

P addFinalizerWithSSA( context.getControllerConfiguration().fieldManager()); } - /** Adds finalizer using Server-Side Apply. */ + /** + * Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of + * the target resource, setting only name, namespace and finalizer. Does not use optimistic + * locking for the patch. + * + * @return the patched resource from the server response + */ @SuppressWarnings("unchecked") public static

P addFinalizerWithSSA( KubernetesClient client, P originalResource, String finalizerName, String fieldManager) { From 3a840a7c223a513dd9865fdeb3ed95a24d2a39ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 3 Oct 2025 09:08:51 +0200 Subject: [PATCH 56/61] comments on integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../TriggerReconcilerOnAllEventIT.java | 14 ++++++++++- ...TriggerReconcilerOnAllEventReconciler.java | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java index 659b02c4c3..eb14821698 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java @@ -51,7 +51,9 @@ void eventsPresent() { () -> { var r = getResource(); assertThat(r).isNull(); + // check if reconciler also triggered when delete event received assertThat(reconciler.isDeleteEventPresent()).isTrue(); + // also when it was marked for deletion assertThat(reconciler.isEventOnMarkedForDeletion()).isTrue(); }); } @@ -96,6 +98,10 @@ void retriesExceptionOnDeleteEvent() { }); } + /** + * Checks if we receive event even after our finalizer is removed but there is an additional + * finalizer + */ @Test void additionalFinalizer() { var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); @@ -129,6 +135,10 @@ void additionalFinalizer() { }); } + // tests the situation where we received a delete event, but there is an exception during + // reconiliation + // what is retried, but during retry we receive an additional event, that should instantly + // trigger the reconciliation again when current finished. @Test void additionalEventDuringRetryOnDeleteEvent() { @@ -154,9 +164,11 @@ void additionalEventDuringRetryOnDeleteEvent() { assertThat(reconciler.isWaiting()); }); + // trigger reconciliation while waiting in reconciler res = getResource(); res.getMetadata().getAnnotations().put("my-annotation", "true"); extension.update(res); + // continue reconciliation reconciler.setContinuerOnRetryWait(true); await() @@ -188,7 +200,6 @@ void additionalEventDuringRetryOnDeleteEvent() { @Test void additionalEventAfterExhaustedRetry() { - var reconciler = extension.getReconcilerOfType(TriggerReconcilerOnAllEventReconciler.class); reconciler.setThrowExceptionIfNoAnnotation(true); var res = testResource(); @@ -203,6 +214,7 @@ void additionalEventAfterExhaustedRetry() { assertThat(reconciler.getEventCount()).isEqualTo(MAX_RETRY_ATTEMPTS + 1); }); + // this also triggers the reconciliation addNoMoreExceptionAnnotation(); await() 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 5dee44a300..cd0cd6ade9 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 @@ -15,29 +15,34 @@ public class TriggerReconcilerOnAllEventReconciler implements Reconciler { + 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"; + private static final Logger log = LoggerFactory.getLogger(TriggerReconcilerOnAllEventReconciler.class); + // control flags to throw exceptions in certain situations private volatile boolean throwExceptionOnFirstDeleteEvent = false; private volatile boolean throwExceptionIfNoAnnotation = false; + // flags for managing wait within reconciliation, + // so we can send an update while reconciliation is in progress private volatile boolean waitAfterFirstRetry = false; private volatile boolean continuerOnRetryWait = false; private volatile boolean waiting = false; + // control flag to throw an exception on first delete event private volatile boolean isFirstDeleteEvent = true; + // control if the reconciler should add / remove the finalizer + private volatile boolean useFinalizer = 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); - + // flags to flip if reconciled primary resource in certain state private boolean deleteEventPresent = false; private boolean eventOnMarkedForDeletion = false; private boolean resourceEventPresent = false; + // counter for how many times the reconciler has been called + private final AtomicInteger reconciliationCounter = new AtomicInteger(0); @Override public UpdateControl reconcile( @@ -143,11 +148,11 @@ public void setWaiting(boolean waiting) { } public int getEventCount() { - return eventCounter.get(); + return reconciliationCounter.get(); } public void increaseEventCount() { - eventCounter.incrementAndGet(); + reconciliationCounter.incrementAndGet(); } public boolean getUseFinalizer() { From 64c92122cd8ca66f472fa3778ca90f0aae7d4a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 3 Oct 2025 09:35:15 +0200 Subject: [PATCH 57/61] improve and add unit test for event processor 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 | 47 ++++++++++++++++++- 1 file changed, 45 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 923cc2fee5..090b5d829a 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 @@ -390,6 +390,8 @@ void newResourceAfterMissedDeleteEvent() { @Test void rateLimitsReconciliationSubmission() { + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); // the refresh defaultPollingPeriod value does not matter here var refreshPeriod = Duration.ofMillis(100); var event = prepareCREvent(); @@ -403,10 +405,12 @@ void rateLimitsReconciliationSubmission() { eventProcessor.handleEvent(event); verify(reconciliationDispatcherMock, after(FAKE_CONTROLLER_EXECUTION_DURATION).times(1)) .handleExecution(any()); - verify(retryTimerEventSourceMock, times(0)).scheduleOnce((ResourceID) any(), anyLong()); + verify(retryTimerEventSourceMock, times(0)) + .scheduleOnce((ResourceID) any(), eq(refreshPeriod.toMillis())); eventProcessor.handleEvent(event); - verify(retryTimerEventSourceMock, times(1)).scheduleOnce((ResourceID) any(), anyLong()); + verify(retryTimerEventSourceMock, times(1)) + .scheduleOnce((ResourceID) any(), eq(refreshPeriod.toMillis())); } @Test @@ -662,6 +666,45 @@ void afterRetryExhaustedAdditionalEventTriggerReconciliationWhenDeleteEventPrese verify(reconciliationDispatcherMock, times(4)).handleExecution(any()); } + @Test + void rateLimitsDeleteEventInAllEventMode() { + when(eventSourceManagerMock.retryEventSource()).thenReturn(retryTimerEventSourceMock); + when(reconciliationDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch()); + eventProcessor = + spy( + new EventProcessor( + controllerConfigTriggerAllEvent( + GenericRetry.defaultLimitedExponentialRetry() + .setInitialInterval(100) + .setIntervalMultiplier(1) + .setMaxAttempts(1), + rateLimiterMock), + reconciliationDispatcherMock, + eventSourceManagerMock, + null)); + eventProcessor.start(); + // the refresh defaultPollingPeriod value does not matter here + var refreshPeriod = Duration.ofMillis(100); + var event = prepareCREvent(); + + final var rateLimit = new RateLimitState() {}; + when(rateLimiterMock.initState()).thenReturn(rateLimit); + when(rateLimiterMock.isLimited(rateLimit)) + .thenReturn(Optional.empty()) + .thenReturn(Optional.of(refreshPeriod)); + + eventProcessor.handleEvent(event); + verify(reconciliationDispatcherMock, after(FAKE_CONTROLLER_EXECUTION_DURATION).times(1)) + .handleExecution(any()); + verify(retryTimerEventSourceMock, times(0)) + .scheduleOnce((ResourceID) any(), eq(refreshPeriod.toMillis())); + + eventProcessor.handleEvent(event); + verify(retryTimerEventSourceMock, times(1)) + .scheduleOnce((ResourceID) any(), eq(refreshPeriod.toMillis())); + } + @Test void passesResourceFromStateToDispatcher() { eventProcessor = From 53d003eaa7576cd170725620a9ee86e4cb87d77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 3 Oct 2025 12:06:32 +0200 Subject: [PATCH 58/61] Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java Co-authored-by: Martin Stefanko --- .../operator/api/reconciler/PrimaryUpdateAndCacheUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c119cf96c5..c6c41219c8 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 @@ -296,7 +296,7 @@ public static

P removeFinalizer( } /** - * Parches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or + * Patches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in * {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}. * From cdd6e4275b612de3bf522f731d4c4db8bd53269a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 3 Oct 2025 12:10:43 +0200 Subject: [PATCH 59/61] missing javadoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/PrimaryUpdateAndCacheUtils.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 c6c41219c8..9e2e0a43d2 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 @@ -272,17 +272,23 @@ public static

P addFinalizer( /** * Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and - * reties the operation if failed, see {@link - * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, - * Predicate)} for details. + * handles retries, see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, + * HasMetadata, UnaryOperator, Predicate)} for details. * - * @return updated resource from the response from the server + * @return updated resource from the server response */ public static

P removeFinalizer( Context

context, String finalizerName) { return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName); } + /** + * Removes the target finalizer from target resource. Uses JSON Patch and handles retries, see + * {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, + * UnaryOperator, Predicate)} for details. + * + * @return updated resource from the server response + */ public static

P removeFinalizer( KubernetesClient client, P resource, String finalizerName) { return conflictRetryingPatch( From f5fbfbc14c3ab932e011d25725b51c7300684db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Oct 2025 09:16:24 +0200 Subject: [PATCH 60/61] javadoc improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../io/javaoperatorsdk/operator/api/reconciler/Context.java | 2 ++ .../onlyreconcile/TriggerReconcilerOnAllEventIT.java | 4 ++++ 2 files changed, 6 insertions(+) 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 36df3666e6..f30fb13b25 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 @@ -79,6 +79,7 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { * io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration#triggerReconcilerOnAllEvent()} * * @return true Delete event received for primary resource + * @since 5.2.0 */ boolean isPrimaryResourceDeleted(); @@ -87,6 +88,7 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { * * @return true if the primary resource is deleted, but the last known state is only available * from the caches of the underlying Informer, not from Delete event. + * @since 5.2.0 */ boolean isPrimaryResourceFinalStateUnknown(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java index eb14821698..6cb4f30ad4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java @@ -15,6 +15,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +/** + * This is quite a technical test to check if we receive events properly for various lifecycle + * events, and situations in the reconciler. + */ public class TriggerReconcilerOnAllEventIT { public static final String TEST = "test1"; From 525413e5cf3f21d3fb5085d00330134890fd4f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Oct 2025 09:56:26 +0200 Subject: [PATCH 61/61] additional sample 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 | 3 +- .../PrimaryUpdateAndCacheUtils.java | 20 +++-- ...gerReconcilerOnAllEventCustomResource.java | 2 +- .../TriggerReconcilerOnAllEventIT.java | 8 +- ...TriggerReconcilerOnAllEventReconciler.java | 2 +- .../TriggerReconcilerOnAllEventSpec.java | 2 +- .../SelectiveFinalizerHandlingIT.java | 76 +++++++++++++++++++ .../SelectiveFinalizerHandlingReconciler.java | 34 +++++++++ ...lizerHandlingReconcilerCustomResource.java | 13 ++++ ...ectiveFinalizerHandlingReconcilerSpec.java | 14 ++++ 10 files changed, 160 insertions(+), 14 deletions(-) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/{onlyreconcile => eventing}/TriggerReconcilerOnAllEventCustomResource.java (86%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/{onlyreconcile => eventing}/TriggerReconcilerOnAllEventIT.java (96%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/{onlyreconcile => eventing}/TriggerReconcilerOnAllEventReconciler.java (98%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/{onlyreconcile => eventing}/TriggerReconcilerOnAllEventSpec.java (72%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerSpec.java diff --git a/docs/content/en/docs/documentation/reconciler.md b/docs/content/en/docs/documentation/reconciler.md index 2ebca7aeb0..7af6527422 100644 --- a/docs/content/en/docs/documentation/reconciler.md +++ b/docs/content/en/docs/documentation/reconciler.md @@ -211,7 +211,7 @@ 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 even for `Delete` events +### Trigger reconciliation for all events TLDR; We provide an execution mode where `reconcile` method is called on every event from event source. @@ -258,3 +258,4 @@ In this mode: - you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal execution mode. +See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling) for selectively adding finalizers for resources; \ No newline at end of file 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 9e2e0a43d2..844d8a65a5 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 @@ -241,11 +241,11 @@ private static

P pollLocalCache( * Adds finalizer to the primary resource from the context using JSON Patch. Retries conflicts and * unprocessable content (HTTP 422), see {@link * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, - * Predicate)} for details on retry. + * Predicate)} for details on retry. It does not add finalizer if there is already a finalizer or + * resource is marked for deletion. * * @return updated resource from the server response */ - @SuppressWarnings("unchecked") public static

P addFinalizer(Context

context, String finalizer) { return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer); } @@ -253,13 +253,16 @@ public static

P addFinalizer(Context

context, String /** * Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content * (HTTP 422), see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, - * HasMetadata, UnaryOperator, Predicate)} for details on retry. + * HasMetadata, UnaryOperator, Predicate)} for details on retry. It does not try to add finalizer + * if there is already a finalizer or resource is marked for deletion. * * @return updated resource from the server response */ - @SuppressWarnings("unchecked") public static

P addFinalizer( KubernetesClient client, P resource, String finalizerName) { + if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) { + return resource; + } return conflictRetryingPatch( client, resource, @@ -273,7 +276,8 @@ public static

P addFinalizer( /** * Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and * handles retries, see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, - * HasMetadata, UnaryOperator, Predicate)} for details. + * HasMetadata, UnaryOperator, Predicate)} for details. It does not try to remove finalizer if + * finalizer is not present on the resource. * * @return updated resource from the server response */ @@ -285,12 +289,16 @@ public static

P removeFinalizer( /** * Removes the target finalizer from target resource. Uses JSON Patch and handles retries, see * {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, - * UnaryOperator, Predicate)} for details. + * UnaryOperator, Predicate)} for details. It does not try to remove finalizer if finalizer is not + * present on the resource. * * @return updated resource from the server response */ public static

P removeFinalizer( KubernetesClient client, P resource, String finalizerName) { + if (!resource.hasFinalizer(finalizerName)) { + return resource; + } return conflictRetryingPatch( client, resource, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventCustomResource.java similarity index 86% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventCustomResource.java index 59cb1e3d02..849d97a20a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventCustomResource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventIT.java similarity index 96% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventIT.java index 6cb4f30ad4..2161e99af6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing; import java.time.Duration; @@ -9,9 +9,9 @@ import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -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 io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing.TriggerReconcilerOnAllEventReconciler.ADDITIONAL_FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing.TriggerReconcilerOnAllEventReconciler.FINALIZER; +import static io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing.TriggerReconcilerOnAllEventReconciler.NO_MORE_EXCEPTION_ANNOTATION_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; 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/eventing/TriggerReconcilerOnAllEventReconciler.java similarity index 98% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventReconciler.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventReconciler.java index cd0cd6ade9..6757b15eb3 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/eventing/TriggerReconcilerOnAllEventReconciler.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing; import java.util.concurrent.atomic.AtomicInteger; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventSpec.java similarity index 72% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventSpec.java index 9254be3b25..df93f517b3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/onlyreconcile/TriggerReconcilerOnAllEventSpec.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/eventing/TriggerReconcilerOnAllEventSpec.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.baseapi.triggerallevent.onlyreconcile; +package io.javaoperatorsdk.operator.baseapi.triggerallevent.eventing; public class TriggerReconcilerOnAllEventSpec { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingIT.java new file mode 100644 index 0000000000..30dd8b0ead --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingIT.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator.baseapi.triggerallevent.finalizerhandling; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * The test showcases how manage finalizers only on some of the custom resources using + * `triggerReconcilerOnAllEvent` mode. + */ +public class SelectiveFinalizerHandlingIT { + + public static final String TEST_RESOURCE2 = "test2"; + public static final String TEST_RESOURCE1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(SelectiveFinalizerHandlingReconciler.class) + .build(); + + @Test + void addFinalizerOnlyOnSomeOfTheResources() { + var resource1 = extension.create(testResource(TEST_RESOURCE1, true)); + var resource2 = extension.create(testResource(TEST_RESOURCE2, false)); + + await() + .pollDelay(Duration.ofMillis(100)) + .untilAsserted( + () -> { + var res1 = + extension.get( + SelectiveFinalizerHandlingReconcilerCustomResource.class, TEST_RESOURCE1); + var res2 = + extension.get( + SelectiveFinalizerHandlingReconcilerCustomResource.class, TEST_RESOURCE2); + + assertThat(res1.getFinalizers()).isNotEmpty(); + assertThat(res2.getFinalizers()).isEmpty(); + }); + + extension.delete(resource1); + extension.delete(resource2); + + await() + .untilAsserted( + () -> { + var res1 = + extension.get( + SelectiveFinalizerHandlingReconcilerCustomResource.class, TEST_RESOURCE1); + var res2 = + extension.get( + SelectiveFinalizerHandlingReconcilerCustomResource.class, TEST_RESOURCE2); + + assertThat(res1).isNull(); + assertThat(res2).isNull(); + }); + } + + SelectiveFinalizerHandlingReconcilerCustomResource testResource( + String name, boolean addFinalizer) { + var resource = new SelectiveFinalizerHandlingReconcilerCustomResource(); + resource.setMetadata(new ObjectMetaBuilder().withName(name).build()); + resource.setSpec(new SelectiveFinalizerHandlingReconcilerSpec()); + resource.getSpec().setUseFinalizer(addFinalizer); + + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconciler.java new file mode 100644 index 0000000000..15389bb9bb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconciler.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.baseapi.triggerallevent.finalizerhandling; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration(triggerReconcilerOnAllEvent = true) +public class SelectiveFinalizerHandlingReconciler + implements Reconciler { + + public static final String FINALIZER = "finalizer.test/finalizer"; + + @Override + public UpdateControl reconcile( + SelectiveFinalizerHandlingReconcilerCustomResource resource, + Context context) { + + if (context.isPrimaryResourceDeleted()) { + return UpdateControl.noUpdate(); + } + + if (resource.getSpec().getUseFinalizer()) { + PrimaryUpdateAndCacheUtils.addFinalizer(context, FINALIZER); + } + + if (resource.isMarkedForDeletion()) { + PrimaryUpdateAndCacheUtils.removeFinalizer(context, FINALIZER); + } + + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerCustomResource.java new file mode 100644 index 0000000000..605405998b --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.triggerallevent.finalizerhandling; + +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("saf") +public class SelectiveFinalizerHandlingReconcilerCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerSpec.java new file mode 100644 index 0000000000..b19f85514c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling/SelectiveFinalizerHandlingReconcilerSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.triggerallevent.finalizerhandling; + +public class SelectiveFinalizerHandlingReconcilerSpec { + + private Boolean useFinalizer; + + public Boolean getUseFinalizer() { + return useFinalizer; + } + + public void setUseFinalizer(Boolean useFinalizer) { + this.useFinalizer = useFinalizer; + } +}