Skip to content

Commit 215b92f

Browse files
committed
feat: field selectors support for InformerEventSource (#2835)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 3520732 commit 215b92f

File tree

9 files changed

+245
-3
lines changed

9 files changed

+245
-3
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.javaoperatorsdk.operator.api.config.informer;
2+
3+
public @interface Field {
4+
5+
String path();
6+
7+
String value();
8+
9+
boolean negated() default false;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.javaoperatorsdk.operator.api.config.informer;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
public class FieldSelector {
7+
private final List<Field> fields;
8+
9+
public FieldSelector(List<Field> fields) {
10+
this.fields = fields;
11+
}
12+
13+
public FieldSelector(Field... fields) {
14+
this.fields = Arrays.asList(fields);
15+
}
16+
17+
public List<Field> getFields() {
18+
return fields;
19+
}
20+
21+
public record Field(String path, String value, boolean negated) {
22+
public Field(String path, String value) {
23+
this(path, value, false);
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.javaoperatorsdk.operator.api.config.informer;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
public class FieldSelectorBuilder {
7+
8+
private final List<FieldSelector.Field> fields = new ArrayList<>();
9+
10+
public FieldSelectorBuilder withField(String path, String value) {
11+
fields.add(new FieldSelector.Field(path, value));
12+
return this;
13+
}
14+
15+
public FieldSelectorBuilder withoutField(String path, String value) {
16+
fields.add(new FieldSelector.Field(path, value, true));
17+
return this;
18+
}
19+
20+
public FieldSelector build() {
21+
return new FieldSelector(fields);
22+
}
23+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,7 @@
113113
* the informer cache.
114114
*/
115115
long informerListLimit() default NO_LONG_VALUE_SET;
116+
117+
/** Kubernetes field selector for additional resource filtering */
118+
Field[] fieldSelector() default {};
116119
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.api.config.informer;
22

3+
import java.util.Arrays;
34
import java.util.Collection;
45
import java.util.Collections;
56
import java.util.Set;
@@ -36,6 +37,7 @@ public class InformerConfiguration<R extends HasMetadata> {
3637
private GenericFilter<? super R> genericFilter;
3738
private ItemStore<R> itemStore;
3839
private Long informerListLimit;
40+
private FieldSelector fieldSelector;
3941

4042
protected InformerConfiguration(
4143
Class<R> resourceClass,
@@ -48,7 +50,8 @@ protected InformerConfiguration(
4850
OnDeleteFilter<? super R> onDeleteFilter,
4951
GenericFilter<? super R> genericFilter,
5052
ItemStore<R> itemStore,
51-
Long informerListLimit) {
53+
Long informerListLimit,
54+
FieldSelector fieldSelector) {
5255
this(resourceClass);
5356
this.name = name;
5457
this.namespaces = namespaces;
@@ -60,6 +63,7 @@ protected InformerConfiguration(
6063
this.genericFilter = genericFilter;
6164
this.itemStore = itemStore;
6265
this.informerListLimit = informerListLimit;
66+
this.fieldSelector = fieldSelector;
6367
}
6468

6569
private InformerConfiguration(Class<R> resourceClass) {
@@ -93,7 +97,8 @@ public static <R extends HasMetadata> InformerConfiguration<R>.Builder builder(
9397
original.onDeleteFilter,
9498
original.genericFilter,
9599
original.itemStore,
96-
original.informerListLimit)
100+
original.informerListLimit,
101+
original.fieldSelector)
97102
.builder;
98103
}
99104

@@ -264,6 +269,10 @@ public Long getInformerListLimit() {
264269
return informerListLimit;
265270
}
266271

272+
public FieldSelector getFieldSelector() {
273+
return fieldSelector;
274+
}
275+
267276
@SuppressWarnings("UnusedReturnValue")
268277
public class Builder {
269278

@@ -329,6 +338,12 @@ public InformerConfiguration<R>.Builder initFromAnnotation(
329338
final var informerListLimit =
330339
informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue;
331340
withInformerListLimit(informerListLimit);
341+
342+
withFieldSelector(
343+
new FieldSelector(
344+
Arrays.stream(informerConfig.fieldSelector())
345+
.map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated()))
346+
.toList()));
332347
}
333348
return this;
334349
}
@@ -424,5 +439,10 @@ public Builder withInformerListLimit(Long informerListLimit) {
424439
InformerConfiguration.this.informerListLimit = informerListLimit;
425440
return this;
426441
}
442+
443+
public Builder withFieldSelector(FieldSelector fieldSelector) {
444+
InformerConfiguration.this.fieldSelector = fieldSelector;
445+
return this;
446+
}
427447
}
428448
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ public Builder<R> withInformerListLimit(Long informerListLimit) {
265265
return this;
266266
}
267267

268+
public Builder<R> withFieldSelector(FieldSelector fieldSelector) {
269+
config.withFieldSelector(fieldSelector);
270+
return this;
271+
}
272+
268273
public void updateFrom(InformerConfiguration<R> informerConfig) {
269274
if (informerConfig != null) {
270275
final var informerConfigName = informerConfig.getName();
@@ -281,7 +286,8 @@ public void updateFrom(InformerConfiguration<R> informerConfig) {
281286
.withOnUpdateFilter(informerConfig.getOnUpdateFilter())
282287
.withOnDeleteFilter(informerConfig.getOnDeleteFilter())
283288
.withGenericFilter(informerConfig.getGenericFilter())
284-
.withInformerListLimit(informerConfig.getInformerListLimit());
289+
.withInformerListLimit(informerConfig.getInformerListLimit())
290+
.withFieldSelector(informerConfig.getFieldSelector());
285291
}
286292
}
287293

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ private InformerWrapper<R> createEventSource(
134134
ResourceEventHandler<R> eventHandler,
135135
String namespaceIdentifier) {
136136
final var informerConfig = configuration.getInformerConfig();
137+
138+
if (informerConfig.getFieldSelector() != null
139+
&& !informerConfig.getFieldSelector().getFields().isEmpty()) {
140+
for (var f : informerConfig.getFieldSelector().getFields()) {
141+
if (f.negated()) {
142+
filteredBySelectorClient = filteredBySelectorClient.withoutField(f.path(), f.value());
143+
} else {
144+
filteredBySelectorClient = filteredBySelectorClient.withField(f.path(), f.value());
145+
}
146+
}
147+
}
148+
137149
var informer =
138150
Optional.ofNullable(informerConfig.getInformerListLimit())
139151
.map(filteredBySelectorClient::withLimit)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.javaoperatorsdk.operator.baseapi.fieldselector;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
9+
import io.fabric8.kubernetes.api.model.SecretBuilder;
10+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
11+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
12+
13+
import static io.javaoperatorsdk.operator.baseapi.fieldselector.FieldSelectorTestReconciler.MY_SECRET_TYPE;
14+
import static io.javaoperatorsdk.operator.baseapi.fieldselector.FieldSelectorTestReconciler.OTHER_SECRET_TYPE;
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.awaitility.Awaitility.await;
17+
18+
class FieldSelectorIT {
19+
20+
public static final String TEST_1 = "test1";
21+
public static final String TEST_2 = "test2";
22+
public static final String TEST_3 = "test3";
23+
24+
@RegisterExtension
25+
LocallyRunOperatorExtension extension =
26+
LocallyRunOperatorExtension.builder()
27+
.withReconciler(new FieldSelectorTestReconciler())
28+
.build();
29+
30+
@Test
31+
void filtersCustomResourceByLabel() {
32+
33+
var customPrimarySecret =
34+
extension.create(
35+
new SecretBuilder()
36+
.withMetadata(new ObjectMetaBuilder().withName(TEST_1).build())
37+
.withType(MY_SECRET_TYPE)
38+
.build());
39+
40+
var otherSecret =
41+
extension.create(
42+
new SecretBuilder()
43+
.withMetadata(new ObjectMetaBuilder().withName(TEST_2).build())
44+
.build());
45+
46+
var dependentSecret =
47+
extension.create(
48+
new SecretBuilder()
49+
.withMetadata(new ObjectMetaBuilder().withName(TEST_3).build())
50+
.withType(OTHER_SECRET_TYPE)
51+
.build());
52+
53+
await()
54+
.pollDelay(Duration.ofMillis(150))
55+
.untilAsserted(
56+
() -> {
57+
var r = extension.getReconcilerOfType(FieldSelectorTestReconciler.class);
58+
assertThat(r.getReconciledSecrets()).containsExactly(TEST_1);
59+
60+
assertThat(
61+
r.getDependentSecretEventSource()
62+
.get(ResourceID.fromResource(dependentSecret)))
63+
.isPresent();
64+
assertThat(
65+
r.getDependentSecretEventSource()
66+
.get(ResourceID.fromResource(customPrimarySecret)))
67+
.isNotPresent();
68+
assertThat(
69+
r.getDependentSecretEventSource().get(ResourceID.fromResource(otherSecret)))
70+
.isNotPresent();
71+
});
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.javaoperatorsdk.operator.baseapi.fieldselector;
2+
3+
import java.util.Collections;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Set;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
import io.fabric8.kubernetes.api.model.Secret;
10+
import io.javaoperatorsdk.operator.api.config.informer.Field;
11+
import io.javaoperatorsdk.operator.api.config.informer.FieldSelectorBuilder;
12+
import io.javaoperatorsdk.operator.api.config.informer.Informer;
13+
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
14+
import io.javaoperatorsdk.operator.api.reconciler.Context;
15+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
16+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
17+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
18+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
19+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
20+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
21+
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
22+
23+
@ControllerConfiguration(
24+
informer =
25+
@Informer(
26+
fieldSelector =
27+
@Field(path = "type", value = FieldSelectorTestReconciler.MY_SECRET_TYPE)))
28+
public class FieldSelectorTestReconciler implements Reconciler<Secret>, TestExecutionInfoProvider {
29+
30+
public static final String MY_SECRET_TYPE = "my-secret-type";
31+
public static final String OTHER_SECRET_TYPE = "my-dependent-secret-type";
32+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
33+
34+
private Set<String> reconciledSecrets = Collections.synchronizedSet(new HashSet<>());
35+
private InformerEventSource<Secret, Secret> dependentSecretEventSource;
36+
37+
@Override
38+
public UpdateControl<Secret> reconcile(Secret resource, Context<Secret> context) {
39+
reconciledSecrets.add(resource.getMetadata().getName());
40+
numberOfExecutions.addAndGet(1);
41+
return UpdateControl.noUpdate();
42+
}
43+
44+
public int getNumberOfExecutions() {
45+
return numberOfExecutions.get();
46+
}
47+
48+
public Set<String> getReconciledSecrets() {
49+
return reconciledSecrets;
50+
}
51+
52+
@Override
53+
public List<EventSource<?, Secret>> prepareEventSources(EventSourceContext<Secret> context) {
54+
dependentSecretEventSource =
55+
new InformerEventSource<>(
56+
InformerEventSourceConfiguration.from(Secret.class, Secret.class)
57+
.withNamespacesInheritedFromController()
58+
.withFieldSelector(
59+
new FieldSelectorBuilder().withField("type", OTHER_SECRET_TYPE).build())
60+
.build(),
61+
context);
62+
63+
return List.of(dependentSecretEventSource);
64+
}
65+
66+
public InformerEventSource<Secret, Secret> getDependentSecretEventSource() {
67+
return dependentSecretEventSource;
68+
}
69+
}

0 commit comments

Comments
 (0)