|
| 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 | + |
0 commit comments