Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
var triggerReconcilerOnAllEvents =
annotation != null && annotation.triggerReconcilerOnAllEvents();

var withoutDefaultFilters = annotation != null && annotation.withoutDefaultFilters();

InformerConfiguration<P> informerConfig =
InformerConfiguration.builder(resourceClass)
.initFromAnnotation(annotation != null ? annotation.informer() : null, context)
Expand All @@ -341,7 +343,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
dependentFieldManager,
this,
informerConfig,
triggerReconcilerOnAllEvents);
triggerReconcilerOnAllEvents,
withoutDefaultFilters);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,8 @@ default boolean triggerReconcilerOnAllEvent() {
default boolean triggerReconcilerOnAllEvents() {
return false;
}

default boolean isWithoutDefaultFilters() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
private Map<DependentResourceSpec, Object> configurations;
private final InformerConfiguration<R>.Builder config;
private boolean triggerReconcilerOnAllEvents;
private boolean withoutDefaultFilters;

private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.finalizer = original.getFinalizerName();
Expand All @@ -59,6 +60,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.name = original.getName();
this.fieldManager = original.fieldManager();
this.triggerReconcilerOnAllEvents = original.triggerReconcilerOnAllEvents();
this.withoutDefaultFilters = original.isWithoutDefaultFilters();
}

public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
Expand Down Expand Up @@ -186,6 +188,11 @@ public ControllerConfigurationOverrider<R> withTriggerReconcilerOnAllEvents(
return this;
}

public ControllerConfigurationOverrider<R> withoutDefaultFilters(boolean withoutDefaultFilters) {
this.withoutDefaultFilters = withoutDefaultFilters;
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
Expand Down Expand Up @@ -231,6 +238,7 @@ public ControllerConfiguration<R> build() {
original.getConfigurationService(),
config.buildForController(),
triggerReconcilerOnAllEvents,
withoutDefaultFilters,
original.getWorkflowSpec().orElse(null));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class ResolvedControllerConfiguration<P extends HasMetadata>
private final ConfigurationService configurationService;
private final String fieldManager;
private final boolean triggerReconcilerOnAllEvents;
private final boolean withoutDefaultFilters;
private WorkflowSpec workflowSpec;

public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
Expand All @@ -61,6 +62,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
other.getConfigurationService(),
other.getInformerConfig(),
other.triggerReconcilerOnAllEvents(),
other.isWithoutDefaultFilters(),
other.getWorkflowSpec().orElse(null));
}

Expand All @@ -77,6 +79,7 @@ public ResolvedControllerConfiguration(
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvents,
boolean withoutDefaultFilters,
WorkflowSpec workflowSpec) {
this(
name,
Expand All @@ -90,7 +93,8 @@ public ResolvedControllerConfiguration(
fieldManager,
configurationService,
informerConfig,
triggerReconcilerOnAllEvents);
triggerReconcilerOnAllEvents,
withoutDefaultFilters);
setWorkflowSpec(workflowSpec);
}

Expand All @@ -106,7 +110,8 @@ protected ResolvedControllerConfiguration(
String fieldManager,
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvents) {
boolean triggerReconcilerOnAllEvents,
boolean withoutDefaultFilters) {
this.informerConfig = informerConfig;
this.configurationService = configurationService;
this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName);
Expand All @@ -120,6 +125,7 @@ protected ResolvedControllerConfiguration(
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
this.fieldManager = fieldManager;
this.triggerReconcilerOnAllEvents = triggerReconcilerOnAllEvents;
this.withoutDefaultFilters = withoutDefaultFilters;
}

protected ResolvedControllerConfiguration(
Expand All @@ -139,6 +145,7 @@ protected ResolvedControllerConfiguration(
null,
configurationService,
InformerConfiguration.builder(resourceClass).buildForController(),
false,
false);
}

Expand Down Expand Up @@ -234,4 +241,9 @@ public String fieldManager() {
public boolean triggerReconcilerOnAllEvents() {
return triggerReconcilerOnAllEvents;
}

@Override
public boolean isWithoutDefaultFilters() {
return withoutDefaultFilters;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,15 @@ MaxReconciliationInterval maxReconciliationInterval() default
* documentation for further details.
*/
boolean triggerReconcilerOnAllEvents() default false;

/**
* When set to {@code true}, JOSDK will not apply its default internal update filters (generation-
* aware, finalizer-needed, marked-for-deletion) to the controller's event source. The user's
* {@link Informer#onUpdateFilter()} becomes the sole filter and has full control. To keep any of
* the default behavior, compose it explicitly using the static methods on {@link
* io.javaoperatorsdk.operator.processing.event.source.controller.InternalEventFilters}.
*
* @return whether JOSDK's internal update filters are skipped
*/
boolean withoutDefaultFilters() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,24 @@ public ControllerEventSource(Controller<T> controller) {
this.controller = controller;

final var config = controller.getConfiguration();
OnUpdateFilter internalOnUpdateFilter =
onUpdateFinalizerNeededAndApplied(controller.useFinalizer(), config.getFinalizerName())
.or(onUpdateGenerationAware(config.isGenerationAware()))
.or(onUpdateMarkedForDeletion());

// by default the on add should be processed in all cases regarding internal filters
final var informerConfig = config.getInformerConfig();
Optional.ofNullable(informerConfig.getOnAddFilter()).ifPresent(this::setOnAddFilter);
Optional.ofNullable(informerConfig.getOnUpdateFilter())
.ifPresentOrElse(
filter -> setOnUpdateFilter(filter.and(internalOnUpdateFilter)),
() -> setOnUpdateFilter(internalOnUpdateFilter));

if (config.isWithoutDefaultFilters()) {
Optional.ofNullable(informerConfig.getOnUpdateFilter()).ifPresent(this::setOnUpdateFilter);
} else {
Comment on lines +60 to +62
OnUpdateFilter internalOnUpdateFilter =
onUpdateFinalizerNeededAndApplied(controller.useFinalizer(), config.getFinalizerName())
.or(onUpdateGenerationAware(config.isGenerationAware()))
.or(onUpdateMarkedForDeletion());
Optional.ofNullable(informerConfig.getOnUpdateFilter())
.ifPresentOrElse(
filter -> setOnUpdateFilter(filter.and(internalOnUpdateFilter)),
() -> setOnUpdateFilter(internalOnUpdateFilter));
}

Optional.ofNullable(informerConfig.getGenericFilter()).ifPresent(this::setGenericFilter);
setControllerConfiguration(config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ public class InternalEventFilters {

private InternalEventFilters() {}

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

return (newResource, oldResource) ->
!oldResource.isMarkedForDeletion() && newResource.isMarkedForDeletion();
}

static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
public static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
boolean generationAware) {

return (newResource, oldResource) -> {
Expand All @@ -46,7 +46,7 @@ static <T extends HasMetadata> OnUpdateFilter<T> onUpdateGenerationAware(
};
}

static <T extends HasMetadata> OnUpdateFilter<T> onUpdateFinalizerNeededAndApplied(
public static <T extends HasMetadata> OnUpdateFilter<T> onUpdateFinalizerNeededAndApplied(
boolean useFinalizer, String finalizerName) {
return (newResource, oldResource) -> {
if (useFinalizer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,45 @@ void callsBroadcastsOnResourceEvents() {
eq(ResourceAction.UPDATED), eq(customResource1), eq(customResource1));
}

@Test
void withoutDefaultFiltersUserFilterIsAppliedDirectly() {
TestCustomResource cr = TestUtils.testCustomResource();
cr.getMetadata().setFinalizers(List.of(FINALIZER));
cr.getMetadata().setGeneration(1L);

// Without default filters, only the user filter runs — no internal generation/finalizer checks.
// User filter accepts unconditionally, so the event passes even with same generation.
OnUpdateFilter<TestCustomResource> userFilter = (newRes, oldRes) -> true;
source = new ControllerEventSource<>(new TestController(null, userFilter, null, true));
setUpSource(source, true, controllerConfig);

source.handleEvent(ResourceAction.UPDATED, cr, cr, null);

verify(eventHandler, times(1)).handleEvent(any());
}

@Test
void withoutDefaultFiltersUserFilterCanRejectEvents() {
TestCustomResource cr = TestUtils.testCustomResource();

OnUpdateFilter<TestCustomResource> userFilter = (newRes, oldRes) -> false;
source = new ControllerEventSource<>(new TestController(null, userFilter, null, true));
setUpSource(source, true, controllerConfig);

source.handleEvent(ResourceAction.UPDATED, cr, cr, null);

verify(eventHandler, never()).handleEvent(any());
}

@Test
void filtersOutEventsOnAddAndUpdate() {
TestCustomResource cr = TestUtils.testCustomResource();

OnAddFilter<TestCustomResource> onAddFilter = (res) -> false;
OnUpdateFilter<TestCustomResource> onUpdatePredicate = (res, res2) -> false;
source = new ControllerEventSource<>(new TestController(onAddFilter, onUpdatePredicate, null));
source =
new ControllerEventSource<>(
new TestController(onAddFilter, onUpdatePredicate, null, false));
setUpSource(source, true, controllerConfig);

source.handleEvent(ResourceAction.ADDED, cr, null, null);
Expand All @@ -159,7 +191,7 @@ void filtersOutEventsOnAddAndUpdate() {
void genericFilterFiltersOutAddUpdateAndDeleteEvents() {
TestCustomResource cr = TestUtils.testCustomResource();

source = new ControllerEventSource<>(new TestController(null, null, res -> false));
source = new ControllerEventSource<>(new TestController(null, null, res -> false, false));
setUpSource(source, true, controllerConfig);

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

Expand All @@ -189,7 +221,7 @@ void ownUpdateEchoIsFilteredOutByEventFilter() throws InterruptedException {
@Test
void foreignUpdateDuringFilteringPropagatesAsUpdate() {
// An external event during the filter window must surface (not be filtered as own).
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
source = spy(new ControllerEventSource<>(new TestController(null, null, null, false)));
setUpSource(source, true, controllerConfig);

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

var latch = sendForEventFilteringUpdate(2);
Expand All @@ -223,7 +255,7 @@ void deleteEventDuringFilteringPropagatesAsDelete() {
void multipleForeignEventsDuringFilteringMergeIntoSingleEvent() {
// Several external events during one filter window collapse into a single
// synthesized event spanning prev → latest seen.
source = spy(new ControllerEventSource<>(new TestController(null, null, null)));
source = spy(new ControllerEventSource<>(new TestController(null, null, null, false)));
setUpSource(source, true, controllerConfig);

var latch = sendForEventFilteringUpdate(2);
Expand Down Expand Up @@ -266,17 +298,19 @@ private static class TestController extends Controller<TestCustomResource> {
public TestController(
OnAddFilter<TestCustomResource> onAddFilter,
OnUpdateFilter<TestCustomResource> onUpdateFilter,
GenericFilter<TestCustomResource> genericFilter) {
GenericFilter<TestCustomResource> genericFilter,
boolean withoutDefaultFilters) {
super(
reconciler,
new TestConfiguration(true, onAddFilter, onUpdateFilter, genericFilter),
new TestConfiguration(
true, onAddFilter, onUpdateFilter, genericFilter, withoutDefaultFilters),
MockKubernetesClient.client(TestCustomResource.class));
}

public TestController(boolean generationAware) {
super(
reconciler,
new TestConfiguration(generationAware, null, null, null),
new TestConfiguration(generationAware, null, null, null, false),
MockKubernetesClient.client(TestCustomResource.class));
}

Expand All @@ -298,7 +332,8 @@ public TestConfiguration(
boolean generationAware,
OnAddFilter<TestCustomResource> onAddFilter,
OnUpdateFilter<TestCustomResource> onUpdateFilter,
GenericFilter<TestCustomResource> genericFilter) {
GenericFilter<TestCustomResource> genericFilter,
boolean withoutDefaultFilters) {
super(
"test",
generationAware,
Expand All @@ -316,7 +351,8 @@ public TestConfiguration(
.withGenericFilter(genericFilter)
.withComparableResourceVersions(true)
.buildForController(),
false);
false,
withoutDefaultFilters);
}
}
}
Loading
Loading