Skip to content

Commit d0582ca

Browse files
committed
feat: custom event filter for controllers operator-framework#404
1 parent 327e93a commit d0582ca

File tree

14 files changed

+440
-84
lines changed

14 files changed

+440
-84
lines changed

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.api;
22

3+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
34
import java.lang.annotation.ElementType;
45
import java.lang.annotation.Retention;
56
import java.lang.annotation.RetentionPolicy;
@@ -46,7 +47,16 @@
4647
* upon. The label selector can be made of multiple comma separated requirements that acts as a
4748
* logical AND operator.
4849
*
49-
* @return the finalizer name
50+
* @return the label selector
5051
*/
5152
String labelSelector() default NULL;
53+
54+
55+
/**
56+
* Optional list of classes providing custom {@link CustomResourceEventFilter}.
57+
*
58+
* @return the list of event filters.
59+
*/
60+
@SuppressWarnings("rawtypes")
61+
Class<CustomResourceEventFilter>[] eventFilters() default {};
5262
}

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3-
import java.util.Set;
4-
53
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
5+
import java.util.Set;
66

77
/**
88
* @deprecated use {@link DefaultControllerConfiguration} instead
@@ -25,9 +25,10 @@ public AbstractControllerConfiguration(String associatedControllerClassName, Str
2525
String crdName, String finalizer, boolean generationAware,
2626
Set<String> namespaces,
2727
RetryConfiguration retryConfiguration, String labelSelector,
28+
CustomResourceEventFilter<R> customResourcePredicate,
2829
Class<R> customResourceClass,
2930
ConfigurationService service) {
3031
super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces,
31-
retryConfiguration, labelSelector, customResourceClass, service);
32+
retryConfiguration, labelSelector, customResourcePredicate, customResourceClass, service);
3233
}
3334
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.ControllerUtils;
5+
import io.javaoperatorsdk.operator.api.Controller;
36
import java.lang.reflect.ParameterizedType;
47
import java.util.Collections;
58
import java.util.Set;
9+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
10+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters;
611

712
import io.fabric8.kubernetes.client.CustomResource;
813
import io.javaoperatorsdk.operator.ControllerUtils;
@@ -98,4 +103,17 @@ default void setConfigurationService(ConfigurationService service) {}
98103
default boolean useFinalizer() {
99104
return !Controller.NO_FINALIZER.equals(getFinalizer());
100105
}
106+
107+
/**
108+
* Allow controllers to filter events before they are provided to the
109+
* {@link io.javaoperatorsdk.operator.processing.event.EventHandler}.
110+
* </p>
111+
* Note that the provided filter is combined with {@link #isGenerationAware()} to compute the
112+
* final set of fiolters that should be applied;
113+
*
114+
* @return
115+
*/
116+
default CustomResourceEventFilter<R> getEventFilter() {
117+
return CustomResourceEventFilters.passthrough();
118+
}
101119
}

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
35
import java.util.HashSet;
46
import java.util.List;
57
import java.util.Set;
68

7-
import io.fabric8.kubernetes.client.CustomResource;
8-
99
public class ControllerConfigurationOverrider<R extends CustomResource<?, ?>> {
1010

1111
private String finalizer;
1212
private boolean generationAware;
1313
private final Set<String> namespaces;
1414
private RetryConfiguration retry;
1515
private String labelSelector;
16+
private CustomResourceEventFilter<R> customResourcePredicate;
1617
private final ControllerConfiguration<R> original;
1718

1819
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
@@ -21,6 +22,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2122
namespaces = new HashSet<>(original.getNamespaces());
2223
retry = original.getRetryConfiguration();
2324
labelSelector = original.getLabelSelector();
25+
customResourcePredicate = original.getEventFilter();
2426
this.original = original;
2527
}
2628

@@ -65,6 +67,12 @@ public ControllerConfigurationOverrider<R> withLabelSelector(String labelSelecto
6567
return this;
6668
}
6769

70+
public ControllerConfigurationOverrider<R> withCustomResourcePredicate(
71+
CustomResourceEventFilter<R> customResourcePredicate) {
72+
this.customResourcePredicate = customResourcePredicate;
73+
return this;
74+
}
75+
6876
public ControllerConfiguration<R> build() {
6977
return new DefaultControllerConfiguration<>(
7078
original.getAssociatedControllerClassName(),
@@ -75,6 +83,7 @@ public ControllerConfiguration<R> build() {
7583
namespaces,
7684
retry,
7785
labelSelector,
86+
customResourcePredicate,
7887
original.getCustomResourceClass(),
7988
original.getConfigurationService());
8089
}

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

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter;
35
import java.util.Collections;
46
import java.util.Set;
57

6-
import io.fabric8.kubernetes.client.CustomResource;
7-
88
public class DefaultControllerConfiguration<R extends CustomResource<?, ?>>
99
implements ControllerConfiguration<R> {
1010

@@ -17,6 +17,7 @@ public class DefaultControllerConfiguration<R extends CustomResource<?, ?>>
1717
private final boolean watchAllNamespaces;
1818
private final RetryConfiguration retryConfiguration;
1919
private final String labelSelector;
20+
private final CustomResourceEventFilter<R> customResourceEventFilter;
2021
private Class<R> customResourceClass;
2122
private ConfigurationService service;
2223

@@ -29,6 +30,7 @@ public DefaultControllerConfiguration(
2930
Set<String> namespaces,
3031
RetryConfiguration retryConfiguration,
3132
String labelSelector,
33+
CustomResourceEventFilter<R> customResourceEventFilter,
3234
Class<R> customResourceClass,
3335
ConfigurationService service) {
3436
this.associatedControllerClassName = associatedControllerClassName;
@@ -44,6 +46,7 @@ public DefaultControllerConfiguration(
4446
? ControllerConfiguration.super.getRetryConfiguration()
4547
: retryConfiguration;
4648
this.labelSelector = labelSelector;
49+
this.customResourceEventFilter = customResourceEventFilter;
4750
this.customResourceClass =
4851
customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass()
4952
: customResourceClass;
@@ -52,7 +55,7 @@ public DefaultControllerConfiguration(
5255

5356
/**
5457
* @deprecated use
55-
* {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)}
58+
* {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration)}
5659
* instead
5760
*/
5861
@Deprecated
@@ -64,8 +67,18 @@ public DefaultControllerConfiguration(
6467
boolean generationAware,
6568
Set<String> namespaces,
6669
RetryConfiguration retryConfiguration) {
67-
this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces,
68-
retryConfiguration, null, null, null);
70+
this(
71+
associatedControllerClassName,
72+
name,
73+
crdName,
74+
finalizer,
75+
generationAware,
76+
namespaces,
77+
retryConfiguration,
78+
null,
79+
null,
80+
null,
81+
null);
6982
}
7083

7184
@Override
@@ -131,4 +144,9 @@ public String getLabelSelector() {
131144
public Class<R> getCustomResourceClass() {
132145
return customResourceClass;
133146
}
147+
148+
@Override
149+
public CustomResourceEventFilter<R> getEventFilter() {
150+
return customResourceEventFilter;
151+
}
134152
}

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

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package io.javaoperatorsdk.operator.processing;
22

3+
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName;
4+
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
5+
6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import io.fabric8.kubernetes.client.CustomResource;
39
import java.util.List;
410
import java.util.Optional;
511
import java.util.Set;
@@ -9,27 +15,18 @@
915
import java.util.concurrent.locks.ReentrantLock;
1016
import java.util.function.Predicate;
1117
import java.util.stream.Collectors;
12-
1318
import org.slf4j.Logger;
1419
import org.slf4j.LoggerFactory;
15-
16-
import io.fabric8.kubernetes.client.CustomResource;
1720
import io.javaoperatorsdk.operator.Metrics;
1821

19-
import com.fasterxml.jackson.core.JsonProcessingException;
20-
import com.fasterxml.jackson.databind.ObjectMapper;
21-
22-
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName;
23-
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
24-
2522
@SuppressWarnings("rawtypes")
26-
public class CustomResourceCache {
23+
public class CustomResourceCache<T extends CustomResource<?, ?>> {
2724

2825
private static final Logger log = LoggerFactory.getLogger(CustomResourceCache.class);
2926
private static final Predicate passthrough = o -> true;
3027

3128
private final ObjectMapper objectMapper;
32-
private final ConcurrentMap<String, CustomResource> resources;
29+
private final ConcurrentMap<String, T> resources;
3330
private final Lock lock = new ReentrantLock();
3431

3532
public CustomResourceCache() {
@@ -44,19 +41,20 @@ public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) {
4441
resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache");
4542
}
4643

47-
public void cacheResource(CustomResource resource) {
44+
public void cacheResource(T resource) {
4845
cacheResource(resource, passthrough);
4946
}
5047

51-
public void cacheResource(CustomResource resource, Predicate<CustomResource> predicate) {
48+
public void cacheResource(T resource, Predicate<CustomResource> predicate) {
5249
try {
5350
lock.lock();
5451
final var uid = getUID(resource);
5552
if (predicate.test(resources.get(uid))) {
5653
if (passthrough != predicate) {
5754
log.trace("Update cache after condition is true: {}", getName(resource));
5855
}
59-
resources.put(uid, resource);
56+
// defensive copy
57+
resources.put(getUID(resource), clone(resource));
6058
}
6159
} finally {
6260
lock.unlock();
@@ -70,11 +68,11 @@ public void cacheResource(CustomResource resource, Predicate<CustomResource> pre
7068
* @param uuid
7169
* @return
7270
*/
73-
public Optional<CustomResource> getLatestResource(String uuid) {
71+
public Optional<T> getLatestResource(String uuid) {
7472
return Optional.ofNullable(resources.get(uuid)).map(this::clone);
7573
}
7674

77-
public List<CustomResource> getLatestResources(Predicate<CustomResource> selector) {
75+
public List<T> getLatestResources(Predicate<CustomResource> selector) {
7876
try {
7977
lock.lock();
8078
return resources.values().stream()
@@ -98,16 +96,17 @@ public Set<String> getLatestResourcesUids(Predicate<CustomResource> selector) {
9896
}
9997
}
10098

101-
private CustomResource clone(CustomResource customResource) {
99+
@SuppressWarnings("unchecked")
100+
private T clone(CustomResource customResource) {
102101
try {
103-
return objectMapper.readValue(
102+
return (T) objectMapper.readValue(
104103
objectMapper.writeValueAsString(customResource), customResource.getClass());
105104
} catch (JsonProcessingException e) {
106105
throw new IllegalStateException(e);
107106
}
108107
}
109108

110-
public CustomResource cleanup(String customResourceUid) {
109+
public T cleanup(String customResourceUid) {
111110
return resources.remove(customResourceUid);
112111
}
113112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
import java.util.Objects;
6+
7+
@FunctionalInterface
8+
public interface CustomResourceEventFilter<T extends CustomResource> {
9+
10+
boolean test(ControllerConfiguration<T> configuration, T oldResource, T newResource);
11+
12+
default CustomResourceEventFilter<T> and(CustomResourceEventFilter<T> other) {
13+
Objects.requireNonNull(other);
14+
return (ControllerConfiguration<T> configuration, T oldResource, T newResource) -> {
15+
boolean result = test(configuration, oldResource, newResource);
16+
return result && other.test(configuration, oldResource, newResource);
17+
};
18+
}
19+
20+
default CustomResourceEventFilter<T> or(CustomResourceEventFilter<T> other) {
21+
Objects.requireNonNull(other);
22+
return (ControllerConfiguration<T> configuration, T oldResource, T newResource) -> {
23+
boolean result = test(configuration, oldResource, newResource);
24+
return result || other.test(configuration, oldResource, newResource);
25+
};
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
public final class CustomResourceEventFilters {
7+
8+
private static final CustomResourceEventFilter<CustomResource> USE_FINALIZER =
9+
new CustomResourceEventFilter<>() {
10+
@Override
11+
public boolean test(
12+
ControllerConfiguration configuration,
13+
CustomResource oldResource,
14+
CustomResource newResource) {
15+
16+
boolean oldFinalizer = oldResource.hasFinalizer(configuration.getFinalizer());
17+
boolean newFinalizer = newResource.hasFinalizer(configuration.getFinalizer());
18+
19+
return !newFinalizer || !oldFinalizer;
20+
}
21+
};
22+
23+
private static final CustomResourceEventFilter<CustomResource> GENERATION_AWARE =
24+
new CustomResourceEventFilter<>() {
25+
@Override
26+
public boolean test(
27+
ControllerConfiguration configuration,
28+
CustomResource oldResource,
29+
CustomResource newResource) {
30+
return oldResource.getMetadata().getGeneration() < newResource.getMetadata()
31+
.getGeneration();
32+
}
33+
};
34+
35+
private static final CustomResourceEventFilter<CustomResource> PASSTHROUGH =
36+
new CustomResourceEventFilter<>() {
37+
@Override
38+
public boolean test(
39+
ControllerConfiguration configuration,
40+
CustomResource oldResource,
41+
CustomResource newResource) {
42+
return true;
43+
}
44+
};
45+
46+
private CustomResourceEventFilters() {}
47+
48+
@SuppressWarnings("unchecked")
49+
public static <T extends CustomResource> CustomResourceEventFilter<T> passthrough() {
50+
return (CustomResourceEventFilter<T>) PASSTHROUGH;
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
public static <T extends CustomResource> CustomResourceEventFilter<T> generationAware() {
55+
return (CustomResourceEventFilter<T>) GENERATION_AWARE;
56+
}
57+
58+
@SuppressWarnings("unchecked")
59+
public static <T extends CustomResource> CustomResourceEventFilter<T> useFinalizer() {
60+
return (CustomResourceEventFilter<T>) USE_FINALIZER;
61+
}
62+
}

0 commit comments

Comments
 (0)