Skip to content

Commit 0dc0de1

Browse files
authored
Custom event filter for controllers (#457)
Custom event filter for controllers
1 parent 6cf4da0 commit 0dc0de1

File tree

14 files changed

+528
-61
lines changed

14 files changed

+528
-61
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.lang.annotation.RetentionPolicy;
66
import java.lang.annotation.Target;
77

8+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
9+
810
@Retention(RetentionPolicy.RUNTIME)
911
@Target({ElementType.TYPE})
1012
public @interface Controller {
@@ -46,7 +48,16 @@
4648
* upon. The label selector can be made of multiple comma separated requirements that acts as a
4749
* logical AND operator.
4850
*
49-
* @return the finalizer name
51+
* @return the label selector
5052
*/
5153
String labelSelector() default NULL;
54+
55+
56+
/**
57+
* Optional list of classes providing custom {@link CustomResourceEventFilter}.
58+
*
59+
* @return the list of event filters.
60+
*/
61+
@SuppressWarnings("rawtypes")
62+
Class<CustomResourceEventFilter>[] eventFilters() default {};
5263
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Set;
44

55
import io.fabric8.kubernetes.client.CustomResource;
6+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
67

78
/**
89
* @deprecated use {@link DefaultControllerConfiguration} instead
@@ -25,9 +26,10 @@ public AbstractControllerConfiguration(String associatedControllerClassName, Str
2526
String crdName, String finalizer, boolean generationAware,
2627
Set<String> namespaces,
2728
RetryConfiguration retryConfiguration, String labelSelector,
29+
CustomResourceEventFilter<R> customResourcePredicate,
2830
Class<R> customResourceClass,
2931
ConfigurationService service) {
3032
super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces,
31-
retryConfiguration, labelSelector, customResourceClass, service);
33+
retryConfiguration, labelSelector, customResourcePredicate, customResourceClass, service);
3234
}
3335
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import io.fabric8.kubernetes.client.CustomResource;
88
import io.javaoperatorsdk.operator.ControllerUtils;
99
import io.javaoperatorsdk.operator.api.Controller;
10+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
11+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters;
1012

1113
public interface ControllerConfiguration<R extends CustomResource> {
1214

@@ -98,4 +100,17 @@ default void setConfigurationService(ConfigurationService service) {}
98100
default boolean useFinalizer() {
99101
return !Controller.NO_FINALIZER.equals(getFinalizer());
100102
}
103+
104+
/**
105+
* Allow controllers to filter events before they are provided to the
106+
* {@link io.javaoperatorsdk.operator.processing.event.EventHandler}.
107+
* </p>
108+
* Note that the provided filter is combined with {@link #isGenerationAware()} to compute the
109+
* final set of fiolters that should be applied;
110+
*
111+
* @return
112+
*/
113+
default CustomResourceEventFilter<R> getEventFilter() {
114+
return CustomResourceEventFilters.passthrough();
115+
}
101116
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.Set;
66

77
import io.fabric8.kubernetes.client.CustomResource;
8+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
89

910
public class ControllerConfigurationOverrider<R extends CustomResource<?, ?>> {
1011

@@ -13,6 +14,7 @@ public class ControllerConfigurationOverrider<R extends CustomResource<?, ?>> {
1314
private final Set<String> namespaces;
1415
private RetryConfiguration retry;
1516
private String labelSelector;
17+
private CustomResourceEventFilter<R> customResourcePredicate;
1618
private final ControllerConfiguration<R> original;
1719

1820
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
@@ -21,6 +23,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2123
namespaces = new HashSet<>(original.getNamespaces());
2224
retry = original.getRetryConfiguration();
2325
labelSelector = original.getLabelSelector();
26+
customResourcePredicate = original.getEventFilter();
2427
this.original = original;
2528
}
2629

@@ -65,6 +68,12 @@ public ControllerConfigurationOverrider<R> withLabelSelector(String labelSelecto
6568
return this;
6669
}
6770

71+
public ControllerConfigurationOverrider<R> withCustomResourcePredicate(
72+
CustomResourceEventFilter<R> customResourcePredicate) {
73+
this.customResourcePredicate = customResourcePredicate;
74+
return this;
75+
}
76+
6877
public ControllerConfiguration<R> build() {
6978
return new DefaultControllerConfiguration<>(
7079
original.getAssociatedControllerClassName(),
@@ -75,6 +84,7 @@ public ControllerConfiguration<R> build() {
7584
namespaces,
7685
retry,
7786
labelSelector,
87+
customResourcePredicate,
7888
original.getCustomResourceClass(),
7989
original.getConfigurationService());
8090
}

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Set;
55

66
import io.fabric8.kubernetes.client.CustomResource;
7+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
78

89
public class DefaultControllerConfiguration<R extends CustomResource<?, ?>>
910
implements ControllerConfiguration<R> {
@@ -17,6 +18,7 @@ public class DefaultControllerConfiguration<R extends CustomResource<?, ?>>
1718
private final boolean watchAllNamespaces;
1819
private final RetryConfiguration retryConfiguration;
1920
private final String labelSelector;
21+
private final CustomResourceEventFilter<R> customResourceEventFilter;
2022
private Class<R> customResourceClass;
2123
private ConfigurationService service;
2224

@@ -29,6 +31,7 @@ public DefaultControllerConfiguration(
2931
Set<String> namespaces,
3032
RetryConfiguration retryConfiguration,
3133
String labelSelector,
34+
CustomResourceEventFilter<R> customResourceEventFilter,
3235
Class<R> customResourceClass,
3336
ConfigurationService service) {
3437
this.associatedControllerClassName = associatedControllerClassName;
@@ -44,6 +47,7 @@ public DefaultControllerConfiguration(
4447
? ControllerConfiguration.super.getRetryConfiguration()
4548
: retryConfiguration;
4649
this.labelSelector = labelSelector;
50+
this.customResourceEventFilter = customResourceEventFilter;
4751
this.customResourceClass =
4852
customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass()
4953
: customResourceClass;
@@ -52,7 +56,7 @@ public DefaultControllerConfiguration(
5256

5357
/**
5458
* @deprecated use
55-
* {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)}
59+
* {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration)}
5660
* instead
5761
*/
5862
@Deprecated
@@ -64,8 +68,18 @@ public DefaultControllerConfiguration(
6468
boolean generationAware,
6569
Set<String> namespaces,
6670
RetryConfiguration retryConfiguration) {
67-
this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces,
68-
retryConfiguration, null, null, null);
71+
this(
72+
associatedControllerClassName,
73+
name,
74+
crdName,
75+
finalizer,
76+
generationAware,
77+
namespaces,
78+
retryConfiguration,
79+
null,
80+
null,
81+
null,
82+
null);
6983
}
7084

7185
@Override
@@ -131,4 +145,9 @@ public String getLabelSelector() {
131145
public Class<R> getCustomResourceClass() {
132146
return customResourceClass;
133147
}
148+
149+
@Override
150+
public CustomResourceEventFilter<R> getEventFilter() {
151+
return customResourceEventFilter;
152+
}
134153
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
2424

2525
@SuppressWarnings("rawtypes")
26-
public class CustomResourceCache {
26+
public class CustomResourceCache<T extends CustomResource<?, ?>> {
2727

2828
private static final Logger log = LoggerFactory.getLogger(CustomResourceCache.class);
2929
private static final Predicate passthrough = o -> true;
3030

3131
private final ObjectMapper objectMapper;
32-
private final ConcurrentMap<String, CustomResource> resources;
32+
private final ConcurrentMap<String, T> resources;
3333
private final Lock lock = new ReentrantLock();
3434

3535
public CustomResourceCache() {
@@ -44,19 +44,21 @@ public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) {
4444
resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache");
4545
}
4646

47-
public void cacheResource(CustomResource resource) {
47+
@SuppressWarnings("unchecked")
48+
public void cacheResource(T resource) {
4849
cacheResource(resource, passthrough);
4950
}
5051

51-
public void cacheResource(CustomResource resource, Predicate<CustomResource> predicate) {
52+
public void cacheResource(T resource, Predicate<T> predicate) {
5253
try {
5354
lock.lock();
5455
final var uid = getUID(resource);
5556
if (predicate.test(resources.get(uid))) {
5657
if (passthrough != predicate) {
5758
log.trace("Update cache after condition is true: {}", getName(resource));
5859
}
59-
resources.put(uid, resource);
60+
// defensive copy
61+
resources.put(getUID(resource), clone(resource));
6062
}
6163
} finally {
6264
lock.unlock();
@@ -70,11 +72,11 @@ public void cacheResource(CustomResource resource, Predicate<CustomResource> pre
7072
* @param uuid
7173
* @return
7274
*/
73-
public Optional<CustomResource> getLatestResource(String uuid) {
75+
public Optional<T> getLatestResource(String uuid) {
7476
return Optional.ofNullable(resources.get(uuid)).map(this::clone);
7577
}
7678

77-
public List<CustomResource> getLatestResources(Predicate<CustomResource> selector) {
79+
public List<T> getLatestResources(Predicate<CustomResource> selector) {
7880
try {
7981
lock.lock();
8082
return resources.values().stream()
@@ -98,16 +100,17 @@ public Set<String> getLatestResourcesUids(Predicate<CustomResource> selector) {
98100
}
99101
}
100102

101-
private CustomResource clone(CustomResource customResource) {
103+
@SuppressWarnings("unchecked")
104+
private T clone(CustomResource customResource) {
102105
try {
103-
return objectMapper.readValue(
106+
return (T) objectMapper.readValue(
104107
objectMapper.writeValueAsString(customResource), customResource.getClass());
105108
} catch (JsonProcessingException e) {
106109
throw new IllegalStateException(e);
107110
}
108111
}
109112

110-
public CustomResource cleanup(String customResourceUid) {
113+
public T cleanup(String customResourceUid) {
111114
return resources.remove(customResourceUid);
112115
}
113116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.javaoperatorsdk.operator.processing.event.internal;
2+
3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
5+
6+
/**
7+
* A functional interface to determine whether resource events should be processed by the SDK. This
8+
* allows users to more finely tuned which events trigger a reconciliation than was previously
9+
* possible (where the logic was limited to generation-based checking).
10+
*
11+
* @param <T> the type of custom resources handled by this filter
12+
*/
13+
@FunctionalInterface
14+
public interface CustomResourceEventFilter<T extends CustomResource> {
15+
16+
/**
17+
* Determines whether the change between the old version of the resource and the new one needs to
18+
* be propagated to the controller or not.
19+
*
20+
* @param configuration the target controller's configuration
21+
* @param oldResource the old version of the resource, null if no old resource available
22+
* @param newResource the new version of the resource
23+
* @return {@code true} if the change needs to be propagated to the controller, {@code false}
24+
* otherwise
25+
*/
26+
boolean acceptChange(ControllerConfiguration<T> configuration, T oldResource, T newResource);
27+
28+
/**
29+
* Combines this filter with the provided one with an AND logic, i.e. the resulting filter will
30+
* only accept the change if both this and the other filter accept it, reject it otherwise.
31+
*
32+
* @param other the possibly {@code null} other filter to combine this one with
33+
* @return a composite filter implementing the AND logic between this and the provided filter
34+
*/
35+
default CustomResourceEventFilter<T> and(CustomResourceEventFilter<T> other) {
36+
return other == null ? this
37+
: (ControllerConfiguration<T> configuration, T oldResource, T newResource) -> {
38+
boolean result = acceptChange(configuration, oldResource, newResource);
39+
return result && other.acceptChange(configuration, oldResource, newResource);
40+
};
41+
}
42+
43+
/**
44+
* Combines this filter with the provided one with an OR logic, i.e. the resulting filter will
45+
* accept the change if any of this or the other filter accept it, rejecting it only if both
46+
* reject it.
47+
*
48+
* @param other the possibly {@code null} other filter to combine this one with
49+
* @return a composite filter implementing the OR logic between this and the provided filter
50+
*/
51+
default CustomResourceEventFilter<T> or(CustomResourceEventFilter<T> other) {
52+
return other == null ? this
53+
: (ControllerConfiguration<T> configuration, T oldResource, T newResource) -> {
54+
boolean result = acceptChange(configuration, oldResource, newResource);
55+
return result || other.acceptChange(configuration, oldResource, newResource);
56+
};
57+
}
58+
}

0 commit comments

Comments
 (0)