Skip to content

Commit 6d9fde9

Browse files
authored
feat: add defaultFilters option to skip JOSDK internal update filters (#3438)
Signed-off-by: xstefank <xstefank122@gmail.com>
1 parent 3a58e7a commit 6d9fde9

11 files changed

Lines changed: 281 additions & 26 deletions

File tree

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
321321
var triggerReconcilerOnAllEvents =
322322
annotation != null && annotation.triggerReconcilerOnAllEvents();
323323

324+
var defaultFilters = annotation == null || annotation.defaultFilters();
325+
324326
InformerConfiguration<P> informerConfig =
325327
InformerConfiguration.builder(resourceClass)
326328
.initFromAnnotation(annotation != null ? annotation.informer() : null, context)
@@ -341,7 +343,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
341343
dependentFieldManager,
342344
this,
343345
informerConfig,
344-
triggerReconcilerOnAllEvents);
346+
triggerReconcilerOnAllEvents,
347+
defaultFilters);
345348
}
346349

347350
/**

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,8 @@ default boolean triggerReconcilerOnAllEvent() {
121121
default boolean triggerReconcilerOnAllEvents() {
122122
return false;
123123
}
124+
125+
default boolean isDefaultFilters() {
126+
return true;
127+
}
124128
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
4646
private Map<DependentResourceSpec, Object> configurations;
4747
private final InformerConfiguration<R>.Builder config;
4848
private boolean triggerReconcilerOnAllEvents;
49+
private boolean defaultFilters;
4950

5051
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
5152
this.finalizer = original.getFinalizerName();
@@ -59,6 +60,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
5960
this.name = original.getName();
6061
this.fieldManager = original.fieldManager();
6162
this.triggerReconcilerOnAllEvents = original.triggerReconcilerOnAllEvents();
63+
this.defaultFilters = original.isDefaultFilters();
6264
}
6365

6466
public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
@@ -186,6 +188,11 @@ public ControllerConfigurationOverrider<R> withTriggerReconcilerOnAllEvents(
186188
return this;
187189
}
188190

191+
public ControllerConfigurationOverrider<R> withDefaultFilters(boolean defaultFilters) {
192+
this.defaultFilters = defaultFilters;
193+
return this;
194+
}
195+
189196
/**
190197
* Sets a max page size limit when starting the informer. This will result in pagination while
191198
* populating the cache. This means that longer lists will take multiple requests to fetch. See
@@ -231,6 +238,7 @@ public ControllerConfiguration<R> build() {
231238
original.getConfigurationService(),
232239
config.buildForController(),
233240
triggerReconcilerOnAllEvents,
241+
defaultFilters,
234242
original.getWorkflowSpec().orElse(null));
235243
}
236244

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class ResolvedControllerConfiguration<P extends HasMetadata>
4545
private final ConfigurationService configurationService;
4646
private final String fieldManager;
4747
private final boolean triggerReconcilerOnAllEvents;
48+
private final boolean defaultFilters;
4849
private WorkflowSpec workflowSpec;
4950

5051
public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
@@ -61,6 +62,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
6162
other.getConfigurationService(),
6263
other.getInformerConfig(),
6364
other.triggerReconcilerOnAllEvents(),
65+
other.isDefaultFilters(),
6466
other.getWorkflowSpec().orElse(null));
6567
}
6668

@@ -77,6 +79,7 @@ public ResolvedControllerConfiguration(
7779
ConfigurationService configurationService,
7880
InformerConfiguration<P> informerConfig,
7981
boolean triggerReconcilerOnAllEvents,
82+
boolean defaultFilters,
8083
WorkflowSpec workflowSpec) {
8184
this(
8285
name,
@@ -90,7 +93,8 @@ public ResolvedControllerConfiguration(
9093
fieldManager,
9194
configurationService,
9295
informerConfig,
93-
triggerReconcilerOnAllEvents);
96+
triggerReconcilerOnAllEvents,
97+
defaultFilters);
9498
setWorkflowSpec(workflowSpec);
9599
}
96100

@@ -106,7 +110,8 @@ protected ResolvedControllerConfiguration(
106110
String fieldManager,
107111
ConfigurationService configurationService,
108112
InformerConfiguration<P> informerConfig,
109-
boolean triggerReconcilerOnAllEvents) {
113+
boolean triggerReconcilerOnAllEvents,
114+
boolean defaultFilters) {
110115
this.informerConfig = informerConfig;
111116
this.configurationService = configurationService;
112117
this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName);
@@ -120,6 +125,7 @@ protected ResolvedControllerConfiguration(
120125
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
121126
this.fieldManager = fieldManager;
122127
this.triggerReconcilerOnAllEvents = triggerReconcilerOnAllEvents;
128+
this.defaultFilters = defaultFilters;
123129
}
124130

125131
protected ResolvedControllerConfiguration(
@@ -139,7 +145,8 @@ protected ResolvedControllerConfiguration(
139145
null,
140146
configurationService,
141147
InformerConfiguration.builder(resourceClass).buildForController(),
142-
false);
148+
false,
149+
true);
143150
}
144151

145152
@Override
@@ -234,4 +241,9 @@ public String fieldManager() {
234241
public boolean triggerReconcilerOnAllEvents() {
235242
return triggerReconcilerOnAllEvents;
236243
}
244+
245+
@Override
246+
public boolean isDefaultFilters() {
247+
return defaultFilters;
248+
}
237249
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,15 @@ MaxReconciliationInterval maxReconciliationInterval() default
105105
* documentation for further details.
106106
*/
107107
boolean triggerReconcilerOnAllEvents() default false;
108+
109+
/**
110+
* When set to {@code false}, JOSDK will not apply its default internal update filters
111+
* (generation- aware, finalizer-needed, marked-for-deletion) to the controller's event source.
112+
* The user's {@link Informer#onUpdateFilter()} becomes the sole filter and has full control. To
113+
* keep any of the default behavior, compose it explicitly using the static methods on {@link
114+
* io.javaoperatorsdk.operator.processing.event.source.controller.InternalEventFilters}.
115+
*
116+
* @return whether JOSDK's internal update filters are applied
117+
*/
118+
boolean defaultFilters() default true;
108119
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,24 @@ public ControllerEventSource(Controller<T> controller) {
5252
this.controller = controller;
5353

5454
final var config = controller.getConfiguration();
55-
OnUpdateFilter internalOnUpdateFilter =
56-
onUpdateFinalizerNeededAndApplied(controller.useFinalizer(), config.getFinalizerName())
57-
.or(onUpdateGenerationAware(config.isGenerationAware()))
58-
.or(onUpdateMarkedForDeletion());
5955

6056
// by default the on add should be processed in all cases regarding internal filters
6157
final var informerConfig = config.getInformerConfig();
6258
Optional.ofNullable(informerConfig.getOnAddFilter()).ifPresent(this::setOnAddFilter);
63-
Optional.ofNullable(informerConfig.getOnUpdateFilter())
64-
.ifPresentOrElse(
65-
filter -> setOnUpdateFilter(filter.and(internalOnUpdateFilter)),
66-
() -> setOnUpdateFilter(internalOnUpdateFilter));
59+
60+
if (config.isDefaultFilters()) {
61+
OnUpdateFilter internalOnUpdateFilter =
62+
defaultFilters(
63+
controller.useFinalizer(), config.getFinalizerName(), config.isGenerationAware());
64+
Optional.ofNullable(informerConfig.getOnUpdateFilter())
65+
.ifPresentOrElse(
66+
filter -> setOnUpdateFilter(filter.and(internalOnUpdateFilter)),
67+
() -> setOnUpdateFilter(internalOnUpdateFilter));
68+
} else {
69+
var userFilter = informerConfig.getOnUpdateFilter();
70+
setOnUpdateFilter(userFilter != null ? userFilter : (newResource, oldResource) -> true);
71+
}
72+
6773
Optional.ofNullable(informerConfig.getGenericFilter()).ifPresent(this::setGenericFilter);
6874
setControllerConfiguration(config);
6975
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InternalEventFilters.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ public class InternalEventFilters {
2222

2323
private InternalEventFilters() {}
2424

25-
static <T extends HasMetadata> OnUpdateFilter<T> onUpdateMarkedForDeletion() {
25+
public static <T extends HasMetadata> OnUpdateFilter<T> onUpdateMarkedForDeletion() {
2626
// the old resource is checked since in corner cases users might still want to update the status
2727
// for a resource that is marked for deletion
2828

2929
return (newResource, oldResource) ->
3030
!oldResource.isMarkedForDeletion() && newResource.isMarkedForDeletion();
3131
}
3232

33-
static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
33+
public static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
3434
boolean generationAware) {
3535

3636
return (newResource, oldResource) -> {
@@ -46,7 +46,7 @@ static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
4646
};
4747
}
4848

49-
static <T extends HasMetadata> OnUpdateFilter<T> onUpdateFinalizerNeededAndApplied(
49+
public static <T extends HasMetadata> OnUpdateFilter<T> onUpdateFinalizerNeededAndApplied(
5050
boolean useFinalizer, String finalizerName) {
5151
return (newResource, oldResource) -> {
5252
if (useFinalizer) {
@@ -61,4 +61,11 @@ static <T extends HasMetadata> OnUpdateFilter<T> onUpdateFinalizerNeededAndAppli
6161
}
6262
};
6363
}
64+
65+
public static <T extends HasMetadata> OnUpdateFilter<T> defaultFilters(
66+
boolean useFinalizer, String finalizerName, boolean generationAware) {
67+
return InternalEventFilters.<T>onUpdateFinalizerNeededAndApplied(useFinalizer, finalizerName)
68+
.or(onUpdateGenerationAware(generationAware))
69+
.or(onUpdateMarkedForDeletion());
70+
}
6471
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,44 @@ void callsBroadcastsOnResourceEvents() {
140140
eq(ResourceAction.UPDATED), eq(customResource1), eq(customResource1));
141141
}
142142

143+
@Test
144+
void withoutDefaultFiltersUserFilterIsAppliedDirectly() {
145+
TestCustomResource cr = TestUtils.testCustomResource();
146+
cr.getMetadata().setFinalizers(List.of(FINALIZER));
147+
cr.getMetadata().setGeneration(1L);
148+
149+
// Without default filters, only the user filter runs — no internal generation/finalizer checks.
150+
// User filter accepts unconditionally, so the event passes even with same generation.
151+
OnUpdateFilter<TestCustomResource> userFilter = (newRes, oldRes) -> true;
152+
source = new ControllerEventSource<>(new TestController(null, userFilter, null, false));
153+
setUpSource(source, true, controllerConfig);
154+
155+
source.handleEvent(ResourceAction.UPDATED, cr, cr, null);
156+
157+
verify(eventHandler, times(1)).handleEvent(any());
158+
}
159+
160+
@Test
161+
void withoutDefaultFiltersUserFilterCanRejectEvents() {
162+
TestCustomResource cr = TestUtils.testCustomResource();
163+
164+
OnUpdateFilter<TestCustomResource> userFilter = (newRes, oldRes) -> false;
165+
source = new ControllerEventSource<>(new TestController(null, userFilter, null, false));
166+
setUpSource(source, true, controllerConfig);
167+
168+
source.handleEvent(ResourceAction.UPDATED, cr, cr, null);
169+
170+
verify(eventHandler, never()).handleEvent(any());
171+
}
172+
143173
@Test
144174
void filtersOutEventsOnAddAndUpdate() {
145175
TestCustomResource cr = TestUtils.testCustomResource();
146176

147177
OnAddFilter<TestCustomResource> onAddFilter = (res) -> false;
148178
OnUpdateFilter<TestCustomResource> onUpdatePredicate = (res, res2) -> false;
149-
source = new ControllerEventSource<>(new TestController(onAddFilter, onUpdatePredicate, null));
179+
source =
180+
new ControllerEventSource<>(new TestController(onAddFilter, onUpdatePredicate, null, true));
150181
setUpSource(source, true, controllerConfig);
151182

152183
source.handleEvent(ResourceAction.ADDED, cr, null, null);
@@ -159,7 +190,7 @@ void filtersOutEventsOnAddAndUpdate() {
159190
void genericFilterFiltersOutAddUpdateAndDeleteEvents() {
160191
TestCustomResource cr = TestUtils.testCustomResource();
161192

162-
source = new ControllerEventSource<>(new TestController(null, null, res -> false));
193+
source = new ControllerEventSource<>(new TestController(null, null, res -> false, true));
163194
setUpSource(source, true, controllerConfig);
164195

165196
source.handleEvent(ResourceAction.ADDED, cr, null, null);
@@ -174,7 +205,7 @@ void ownUpdateEchoIsFilteredOutByEventFilter() throws InterruptedException {
174205
// End-to-end smoke for the event-filter wiring on the controller path: an event for our
175206
// own write must not propagate. Detail-level filter scenarios are covered in
176207
// EventingDetailTest / EventFilterSupportTest.
177-
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
208+
source = spy(new ControllerEventSource<>(new TestController(null, null, null, true)));
178209
setUpSource(source, true, controllerConfig);
179210
doReturn(Optional.empty()).when(source).get(any());
180211

@@ -189,7 +220,7 @@ void ownUpdateEchoIsFilteredOutByEventFilter() throws InterruptedException {
189220
@Test
190221
void foreignUpdateDuringFilteringPropagatesAsUpdate() {
191222
// An external event during the filter window must surface (not be filtered as own).
192-
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
223+
source = spy(new ControllerEventSource<>(new TestController(null, null, null, true)));
193224
setUpSource(source, true, controllerConfig);
194225

195226
var latch = sendForEventFilteringUpdate(2);
@@ -203,7 +234,7 @@ void foreignUpdateDuringFilteringPropagatesAsUpdate() {
203234
void deleteEventDuringFilteringPropagatesAsDelete() {
204235
// A DELETE arriving during the filter window must surface — the resource has gone,
205236
// so the filter must not silence it just because our own write is still tracking RVs.
206-
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
237+
source = spy(new ControllerEventSource<>(new TestController(null, null, null, true)));
207238
setUpSource(source, true, controllerConfig);
208239

209240
var latch = sendForEventFilteringUpdate(2);
@@ -223,7 +254,7 @@ void deleteEventDuringFilteringPropagatesAsDelete() {
223254
void multipleForeignEventsDuringFilteringMergeIntoSingleEvent() {
224255
// Several external events during one filter window collapse into a single
225256
// synthesized event spanning prev → latest seen.
226-
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
257+
source = spy(new ControllerEventSource<>(new TestController(null, null, null, true)));
227258
setUpSource(source, true, controllerConfig);
228259

229260
var latch = sendForEventFilteringUpdate(2);
@@ -266,17 +297,18 @@ private static class TestController extends Controller<TestCustomResource> {
266297
public TestController(
267298
OnAddFilter<TestCustomResource> onAddFilter,
268299
OnUpdateFilter<TestCustomResource> onUpdateFilter,
269-
GenericFilter<TestCustomResource> genericFilter) {
300+
GenericFilter<TestCustomResource> genericFilter,
301+
boolean defaultFilters) {
270302
super(
271303
reconciler,
272-
new TestConfiguration(true, onAddFilter, onUpdateFilter, genericFilter),
304+
new TestConfiguration(true, onAddFilter, onUpdateFilter, genericFilter, defaultFilters),
273305
MockKubernetesClient.client(TestCustomResource.class));
274306
}
275307

276308
public TestController(boolean generationAware) {
277309
super(
278310
reconciler,
279-
new TestConfiguration(generationAware, null, null, null),
311+
new TestConfiguration(generationAware, null, null, null, true),
280312
MockKubernetesClient.client(TestCustomResource.class));
281313
}
282314

@@ -298,7 +330,8 @@ public TestConfiguration(
298330
boolean generationAware,
299331
OnAddFilter<TestCustomResource> onAddFilter,
300332
OnUpdateFilter<TestCustomResource> onUpdateFilter,
301-
GenericFilter<TestCustomResource> genericFilter) {
333+
GenericFilter<TestCustomResource> genericFilter,
334+
boolean defaultFilters) {
302335
super(
303336
"test",
304337
generationAware,
@@ -316,7 +349,8 @@ public TestConfiguration(
316349
.withGenericFilter(genericFilter)
317350
.withComparableResourceVersions(true)
318351
.buildForController(),
319-
false);
352+
false,
353+
defaultFilters);
320354
}
321355
}
322356
}

0 commit comments

Comments
 (0)