Skip to content

Commit 839de40

Browse files
authored
docs: event filter page (#3467)
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 95185a6 commit 839de40

6 files changed

Lines changed: 328 additions & 0 deletions

File tree

docs/content/en/docs/documentation/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This section contains detailed documentation for all Java Operator SDK features
2323
## Advanced Features
2424

2525
- **[Eventing](eventing/)** - Understanding the event-driven model
26+
- **[Event filters](event-filters/)** - Filtering events and the default filter behavior
2627
- **[Accessing Resources in Caches](working-with-es-caches/)** - How to access resources in caches
2728
- **[Operations](operations/)** - Helm chart, metrics, logging, configurations, leader election
2829
- **[Other Features](features/)** - Additional capabilities and integrations
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: Event filters
3+
weight: 50
4+
---
5+
6+
Every [event source](eventing.md) that watches Kubernetes resources is backed by an informer that
7+
receives *add*, *update* and *delete* events from the Kubernetes API. Not every one of those events
8+
is worth a reconciliation: a resource might be touched by another controller, its `.metadata` might
9+
change without any meaningful change to its `.spec`, or your own controller might have caused the
10+
change in the first place. **Event filters** let you decide, per event, whether it should be
11+
propagated and (potentially) trigger a reconciliation. Filtering events as early as possible keeps
12+
your operator efficient by avoiding needless reconciliations.
13+
14+
Filters apply both to the **primary resource** (the resource your `Reconciler` manages) and to any
15+
**secondary resources** watched through an [`InformerEventSource`](eventing.md#informereventsource).
16+
17+
## Filter types
18+
19+
JOSDK defines four functional interfaces in the
20+
[`io.javaoperatorsdk.operator.processing.event.source.filter`](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter)
21+
package. Each returns `true` when the event should be **accepted** (propagated) and `false` when it
22+
should be **dropped**:
23+
24+
| Filter | Applies to | Signature |
25+
| --- | --- | --- |
26+
| [`OnAddFilter`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/OnAddFilter.java) | resource add events | `boolean accept(R resource)` |
27+
| [`OnUpdateFilter`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/OnUpdateFilter.java) | resource update events | `boolean accept(R newResource, R oldResource)` |
28+
| [`OnDeleteFilter`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/OnDeleteFilter.java) | resource delete events | `boolean accept(R resource, Boolean deletedFinalStateUnknown)` |
29+
| [`GenericFilter`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/GenericFilter.java) | add, update **and** delete events | `boolean accept(R resource)` |
30+
31+
`GenericFilter` is applied to every kind of event, so it is a convenient way to express a condition
32+
that is independent of the event type. When both a specific filter (e.g. `OnUpdateFilter`) and the
33+
`GenericFilter` are configured, **both** must accept the event for it to be propagated.
34+
35+
All four interfaces are `@FunctionalInterface`s and provide `and(...)`, `or(...)` and `not()`
36+
default methods, so you can compose several conditions:
37+
38+
```java
39+
OnUpdateFilter<MyCustomResource> filter =
40+
onUpdateFilterA.and(onUpdateFilterB).not();
41+
```
42+
43+
## Configuring filters on the primary resource
44+
45+
Filters for the primary resource are configured through the `@Informer` annotation nested in
46+
`@ControllerConfiguration`. You reference the filter *class* (which must have an accessible no-arg
47+
constructor):
48+
49+
```java
50+
@ControllerConfiguration(informer = @Informer(onUpdateFilter = UpdateFilter.class))
51+
public class MyReconciler implements Reconciler<MyCustomResource> {
52+
// ...
53+
}
54+
```
55+
56+
```java
57+
public class UpdateFilter implements OnUpdateFilter<MyCustomResource> {
58+
@Override
59+
public boolean accept(MyCustomResource newResource, MyCustomResource oldResource) {
60+
// reconcile only if the value actually changed
61+
return !newResource.getSpec().getValue().equals(SKIP_VALUE);
62+
}
63+
}
64+
```
65+
66+
The `@Informer` annotation exposes `onAddFilter`, `onUpdateFilter`, `onDeleteFilter` and
67+
`genericFilter` attributes for the primary resource informer.
68+
69+
## Configuring filters on secondary resources
70+
71+
For secondary resources watched via an `InformerEventSource`, filters are set through the
72+
`InformerEventSourceConfiguration` builder, where you can pass filter instances directly (typically
73+
as lambdas):
74+
75+
```java
76+
@Override
77+
public List<EventSource<?, MyCustomResource>> prepareEventSources(
78+
EventSourceContext<MyCustomResource> context) {
79+
80+
var informerConfiguration =
81+
InformerEventSourceConfiguration.from(ConfigMap.class, MyCustomResource.class)
82+
.withOnUpdateFilter(
83+
(newCM, oldCM) -> !newCM.getData().get(VALUE_KEY).equals(SKIP_VALUE))
84+
.withOnAddFilter(cm -> true)
85+
.build();
86+
87+
return List.of(new InformerEventSource<>(informerConfiguration, context));
88+
}
89+
```
90+
91+
The builder provides `withOnAddFilter`, `withOnUpdateFilter`, `withOnDeleteFilter` and
92+
`withGenericFilter`.
93+
94+
{{% alert title="Selectors vs. filters" color="primary" %}}
95+
Filters run **client-side**, after the event has already been received from the API server. If you
96+
can express your condition as a label or field selector, prefer the `labelSelector` /
97+
`fieldSelector` attributes of `@Informer` (or the equivalent builder methods) instead: those are
98+
evaluated **server-side**, so filtered-out resources are never sent to the informer at all and are
99+
not even held in the cache. Use event filters for conditions that cannot be expressed as selectors,
100+
such as comparing the new and old versions of a resource.
101+
{{% /alert %}}
102+
103+
## Default filters
104+
105+
For the primary resource, JOSDK always applies a set of **internal update filters** on top of your
106+
own `onUpdateFilter`. These are what make sure your `Reconciler` is not triggered for updates that
107+
don't require action. An update event is accepted by the default filters if **any** of the following
108+
is true (see
109+
[`InternalEventFilters`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InternalEventFilters.java)):
110+
111+
- **Generation aware:** the resource's `.metadata.generation` increased. Kubernetes bumps the
112+
generation whenever the `.spec` changes, so this drops events caused purely by `.metadata` or
113+
`.status` changes. This part is only active when
114+
[generation-aware event processing](eventing.md#generation-awareness-and-event-filtering) is
115+
enabled (the default for resources that support it). For resources without a generation (e.g.
116+
`Pod`), every update is accepted.
117+
- **Finalizer needed:** the finalizer JOSDK manages was just added or removed. This ensures the
118+
reconciliation that adds the finalizer is not filtered out.
119+
- **Marked for deletion:** the resource just transitioned to being marked for deletion (a deletion
120+
timestamp was set), so cleanup logic can run.
121+
122+
### How default filters combine with your filter
123+
124+
The default (internal) update filter is combined with your `onUpdateFilter` using a logical **AND**:
125+
an update event must be accepted by **both** your filter and the internal filter to be propagated.
126+
In other words, your filter can only make the operator *more* selective; it cannot force a
127+
reconciliation for an event that the default filters would otherwise drop.
128+
129+
Note that default filters only concern **update** events on the **primary** resource. Add events are
130+
always processed regardless of the internal filters, delete events are only subject to your
131+
`genericFilter` (if any), and secondary resources have no internal default filters at all.
132+
133+
### Disabling the default filters
134+
135+
If the AND-composition described above is too restrictive — for example, you need full control over
136+
which updates trigger a reconciliation — you can turn the internal update filters off with the
137+
`defaultFilters` attribute:
138+
139+
```java
140+
@ControllerConfiguration(
141+
defaultFilters = false,
142+
informer = @Informer(onUpdateFilter = MyUpdateFilter.class))
143+
public class MyReconciler implements Reconciler<MyCustomResource> {
144+
// ...
145+
}
146+
```
147+
148+
With `defaultFilters = false`:
149+
150+
- Your `onUpdateFilter` becomes the **sole** update filter and has full control over which update
151+
events are propagated.
152+
- If you don't provide an `onUpdateFilter`, **all** update events are accepted.
153+
154+
{{% alert title="Use with care" color="warning" %}}
155+
Disabling default filters removes the generation-aware, finalizer-needed and marked-for-deletion
156+
guarantees. If you still want part of that behavior, compose it explicitly using the static factory
157+
methods on
158+
[`InternalEventFilters`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/InternalEventFilters.java)
159+
inside your own filter.
160+
{{% /alert %}}
161+
162+
## Non-informer based event sources
163+
164+
The filters described above apply only to informer-backed event sources, since they operate on the
165+
*add*, *update* and *delete* events an informer receives from the Kubernetes API. Event sources that
166+
are not backed by an informer — such as [`PollingEventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java)
167+
and [`PerResourcePollingEventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java)
168+
— have no filters. For those, it is up to your implementation to emit only the events that are
169+
actually relevant, so that reconciliations are not triggered needlessly.
170+

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/GenericFilter.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,60 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.filter;
1717

18+
/**
19+
* Filter applied to every kind of event (add, update <b>and</b> delete) received by an
20+
* informer-backed event source. It is a convenient way to express a condition that is independent
21+
* of the event type.
22+
*
23+
* <p>When both a type-specific filter (such as {@link OnAddFilter}, {@link OnUpdateFilter} or
24+
* {@link OnDeleteFilter}) and a {@code GenericFilter} are configured, <b>both</b> must accept an
25+
* event for it to be propagated.
26+
*
27+
* <p>Filters only apply to informer-backed event sources; event sources that are not backed by an
28+
* informer have no filters.
29+
*
30+
* @param <R> the type of the resource the events relate to
31+
*/
1832
@FunctionalInterface
1933
public interface GenericFilter<R> {
2034

35+
/**
36+
* Decides whether an event concerning the given resource should be propagated.
37+
*
38+
* @param resource the resource the event relates to
39+
* @return {@code true} if the event should be <b>accepted</b> (propagated), {@code false} if it
40+
* should be <b>dropped</b>
41+
*/
2142
boolean accept(R resource);
2243

44+
/**
45+
* Composes this filter with another one using a logical <b>AND</b>: the resulting filter accepts
46+
* an event only when both this filter and the given filter accept it.
47+
*
48+
* @param genericFilter the other filter to combine with this one
49+
* @return a filter that accepts an event only if both filters accept it
50+
*/
2351
default GenericFilter<R> and(GenericFilter<R> genericFilter) {
2452
return (resource) -> this.accept(resource) && genericFilter.accept(resource);
2553
}
2654

55+
/**
56+
* Composes this filter with another one using a logical <b>OR</b>: the resulting filter accepts
57+
* an event when either this filter or the given filter accepts it.
58+
*
59+
* @param genericFilter the other filter to combine with this one
60+
* @return a filter that accepts an event if at least one of the filters accepts it
61+
*/
2762
default GenericFilter<R> or(GenericFilter<R> genericFilter) {
2863
return (resource) -> this.accept(resource) || genericFilter.accept(resource);
2964
}
3065

66+
/**
67+
* Negates this filter: the resulting filter accepts exactly the events this one drops, and drops
68+
* the events this one accepts.
69+
*
70+
* @return a filter that returns the logical <b>NOT</b> of this filter's result
71+
*/
3172
default GenericFilter<R> not() {
3273
return (resource) -> !this.accept(resource);
3374
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/OnAddFilter.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,55 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.filter;
1717

18+
/**
19+
* Filter applied to resource <i>add</i> events received by an informer-backed event source. It lets
20+
* you decide whether the addition of a resource should be propagated and (potentially) trigger a
21+
* reconciliation.
22+
*
23+
* <p>Filters only apply to informer-backed event sources; event sources that are not backed by an
24+
* informer have no filters.
25+
*
26+
* @param <R> the type of the added resource
27+
*/
1828
@FunctionalInterface
1929
public interface OnAddFilter<R> {
30+
/**
31+
* Decides whether the add event for the given resource should be propagated.
32+
*
33+
* @param resource the resource that was added
34+
* @return {@code true} if the event should be <b>accepted</b> (propagated), {@code false} if it
35+
* should be <b>dropped</b>
36+
*/
2037
boolean accept(R resource);
2138

39+
/**
40+
* Composes this filter with another one using a logical <b>AND</b>: the resulting filter accepts
41+
* an event only when both this filter and the given filter accept it.
42+
*
43+
* @param onAddFilter the other filter to combine with this one
44+
* @return a filter that accepts an event only if both filters accept it
45+
*/
2246
default OnAddFilter<R> and(OnAddFilter<R> onAddFilter) {
2347
return (resource) -> this.accept(resource) && onAddFilter.accept(resource);
2448
}
2549

50+
/**
51+
* Composes this filter with another one using a logical <b>OR</b>: the resulting filter accepts
52+
* an event when either this filter or the given filter accepts it.
53+
*
54+
* @param onAddFilter the other filter to combine with this one
55+
* @return a filter that accepts an event if at least one of the filters accepts it
56+
*/
2657
default OnAddFilter<R> or(OnAddFilter<R> onAddFilter) {
2758
return (resource) -> this.accept(resource) || onAddFilter.accept(resource);
2859
}
2960

61+
/**
62+
* Negates this filter: the resulting filter accepts exactly the events this one drops, and drops
63+
* the events this one accepts.
64+
*
65+
* @return a filter that returns the logical <b>NOT</b> of this filter's result
66+
*/
3067
default OnAddFilter<R> not() {
3168
return (resource) -> !this.accept(resource);
3269
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/filter/OnDeleteFilter.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,63 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.filter;
1717

18+
/**
19+
* Filter applied to resource <i>delete</i> events received by an informer-backed event source. It
20+
* lets you decide whether the deletion of a resource should be propagated and (potentially) trigger
21+
* a reconciliation.
22+
*
23+
* <p>Filters only apply to informer-backed event sources; event sources that are not backed by an
24+
* informer have no filters.
25+
*
26+
* @param <R> the type of the deleted resource
27+
*/
1828
@FunctionalInterface
1929
public interface OnDeleteFilter<R> {
2030

31+
/**
32+
* Decides whether the delete event for the given resource should be propagated.
33+
*
34+
* @param hasMetadata the resource that was deleted
35+
* @param deletedFinalStateUnknown {@code true} when the informer missed the delete event and the
36+
* final state of the resource is unknown (the object may be stale), {@code false} when the
37+
* deletion was observed with the resource's last known state
38+
* @return {@code true} if the event should be <b>accepted</b> (propagated), {@code false} if it
39+
* should be <b>dropped</b>
40+
*/
2141
boolean accept(R hasMetadata, Boolean deletedFinalStateUnknown);
2242

43+
/**
44+
* Composes this filter with another one using a logical <b>AND</b>: the resulting filter accepts
45+
* an event only when both this filter and the given filter accept it.
46+
*
47+
* @param OnDeleteFilter the other filter to combine with this one
48+
* @return a filter that accepts an event only if both filters accept it
49+
*/
2350
default OnDeleteFilter<R> and(OnDeleteFilter<R> OnDeleteFilter) {
2451
return (resource, deletedFinalStateUnknown) ->
2552
this.accept(resource, deletedFinalStateUnknown)
2653
&& OnDeleteFilter.accept(resource, deletedFinalStateUnknown);
2754
}
2855

56+
/**
57+
* Composes this filter with another one using a logical <b>OR</b>: the resulting filter accepts
58+
* an event when either this filter or the given filter accepts it.
59+
*
60+
* @param OnDeleteFilter the other filter to combine with this one
61+
* @return a filter that accepts an event if at least one of the filters accepts it
62+
*/
2963
default OnDeleteFilter<R> or(OnDeleteFilter<R> OnDeleteFilter) {
3064
return (resource, deletedFinalStateUnknown) ->
3165
this.accept(resource, deletedFinalStateUnknown)
3266
|| OnDeleteFilter.accept(resource, deletedFinalStateUnknown);
3367
}
3468

69+
/**
70+
* Negates this filter: the resulting filter accepts exactly the events this one drops, and drops
71+
* the events this one accepts.
72+
*
73+
* @return a filter that returns the logical <b>NOT</b> of this filter's result
74+
*/
3575
default OnDeleteFilter<R> not() {
3676
return (resource, deletedFinalStateUnknown) -> !this.accept(resource, deletedFinalStateUnknown);
3777
}

0 commit comments

Comments
 (0)