From 2d43aff58764b62b12f056c040e6d6be766149a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 19 Jun 2025 12:13:08 +0200 Subject: [PATCH 1/7] feat: field selectors support for InformerEventSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/InformerConfiguration.java | 37 +++++++++++++++++++ .../source/informer/InformerManager.java | 12 ++++++ 2 files changed, 49 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 958a2a7a6f..8665398bdc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -1,7 +1,12 @@ package io.javaoperatorsdk.operator.api.config.informer; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -36,6 +41,8 @@ public class InformerConfiguration { private GenericFilter genericFilter; private ItemStore itemStore; private Long informerListLimit; + private Map withFields = new HashMap<>(); + private List> withoutFields = new ArrayList<>(); protected InformerConfiguration( Class resourceClass, @@ -264,6 +271,14 @@ public Long getInformerListLimit() { return informerListLimit; } + public Map getWithFields() { + return withFields; + } + + public List> getWithoutFields() { + return withoutFields; + } + @SuppressWarnings("UnusedReturnValue") public class Builder { @@ -424,5 +439,27 @@ public Builder withInformerListLimit(Long informerListLimit) { InformerConfiguration.this.informerListLimit = informerListLimit; return this; } + + public Builder withField(String field, String value) { + InformerConfiguration.this.withFields.put(field, value); + return this; + } + + public Builder withFields(Map fields) { + InformerConfiguration.this.withFields.putAll(fields); + return this; + } + + /** + * Note that there can be more values for the same field. Like key != value1,key != value2. + * + * @param field key + * @param value negated + * @return builder + */ + public Builder withoutField(String field, String value) { + InformerConfiguration.this.withoutFields.add(new AbstractMap.SimpleEntry<>(field, value)); + return this; + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 1e1607dd8b..2af6c70fef 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -134,6 +134,18 @@ private InformerWrapper createEventSource( ResourceEventHandler eventHandler, String namespaceIdentifier) { final var informerConfig = configuration.getInformerConfig(); + + if (!informerConfig.getWithFields().isEmpty()) { + filteredBySelectorClient = + filteredBySelectorClient.withFields(informerConfig.getWithFields()); + } + + if (!informerConfig.getWithoutFields().isEmpty()) { + for (var e : informerConfig.getWithoutFields()) { + filteredBySelectorClient = filteredBySelectorClient.withoutField(e.getKey(), e.getValue()); + } + } + var informer = Optional.ofNullable(informerConfig.getInformerListLimit()) .map(filteredBySelectorClient::withLimit) From 755c404a10f1225a0607272d17ebb58ea57eec09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 19 Jun 2025 17:20:37 +0200 Subject: [PATCH 2/7] Annotation config + controller IT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/informer/Field.java | 8 +++ .../api/config/informer/Informer.java | 6 +++ .../informer/InformerConfiguration.java | 15 +++++- .../fieldselector/FieldSelectorIT.java | 52 +++++++++++++++++++ .../FieldSelectorTestReconciler.java | 43 +++++++++++++++ 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java new file mode 100644 index 0000000000..8713dfadaf --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.config.informer; + +public @interface Field { + + String field(); + + String value(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index 80a025009d..add11cd0a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -113,4 +113,10 @@ * the informer cache. */ long informerListLimit() default NO_LONG_VALUE_SET; + + /** Field selector for informer, for matching field with the value: field=value; */ + Field[] withFields() default {}; + + /** Negated field selector for informer, for Non-matching field with the value: field!=value; */ + Field[] withoutFields() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 8665398bdc..de591f77f1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -2,6 +2,7 @@ import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -55,7 +56,9 @@ protected InformerConfiguration( OnDeleteFilter onDeleteFilter, GenericFilter genericFilter, ItemStore itemStore, - Long informerListLimit) { + Long informerListLimit, + Map withFields, + List> withoutFields) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -67,6 +70,8 @@ protected InformerConfiguration( this.genericFilter = genericFilter; this.itemStore = itemStore; this.informerListLimit = informerListLimit; + this.withFields = withFields; + this.withoutFields = withoutFields; } private InformerConfiguration(Class resourceClass) { @@ -100,7 +105,9 @@ public static InformerConfiguration.Builder builder( original.onDeleteFilter, original.genericFilter, original.itemStore, - original.informerListLimit) + original.informerListLimit, + original.withFields, + original.withoutFields) .builder; } @@ -344,6 +351,10 @@ public InformerConfiguration.Builder initFromAnnotation( final var informerListLimit = informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue; withInformerListLimit(informerListLimit); + + Arrays.stream(informerConfig.withFields()).forEach(f -> withField(f.field(), f.value())); + Arrays.stream(informerConfig.withoutFields()) + .forEach(f -> withoutField(f.field(), f.value())); } return this; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java new file mode 100644 index 0000000000..e952a6ea15 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator.baseapi.fieldselector; + +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.fieldselector.FieldSelectorTestReconciler.MY_SECRET_TYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class FieldSelectorIT { + + public static final String TEST_1 = "test1"; + public static final String TEST_2 = "test2"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new FieldSelectorTestReconciler()) + .build(); + + @Test + void filtersCustomResourceByLabel() { + + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_1).build()) + .withStringData(Map.of("key1", "value1")) + .withType(MY_SECRET_TYPE) + .build()); + + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_2).build()) + .withStringData(Map.of("key2", "value2")) + .build()); + + await() + .pollDelay(Duration.ofMillis(150)) + .untilAsserted( + () -> { + var r = extension.getReconcilerOfType(FieldSelectorTestReconciler.class); + assertThat(r.getReconciledSecrets()).containsExactly(TEST_1); + }); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java new file mode 100644 index 0000000000..ff1e39d925 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.baseapi.fieldselector; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.config.informer.Field; +import io.javaoperatorsdk.operator.api.config.informer.Informer; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration( + informer = + @Informer( + withFields = + @Field(field = "type", value = FieldSelectorTestReconciler.MY_SECRET_TYPE))) +public class FieldSelectorTestReconciler implements Reconciler, TestExecutionInfoProvider { + + public static final String MY_SECRET_TYPE = "my-secret-type"; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + private Set reconciledSecrets = Collections.synchronizedSet(new HashSet<>()); + + @Override + public UpdateControl reconcile(Secret resource, Context context) { + reconciledSecrets.add(resource.getMetadata().getName()); + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + public Set getReconciledSecrets() { + return reconciledSecrets; + } +} From 9abfa33948e1359f826f62b6f32bd91f8849597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 09:18:50 +0200 Subject: [PATCH 3/7] test + inform api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../InformerEventSourceConfiguration.java | 23 +++++++++- .../fieldselector/FieldSelectorIT.java | 45 ++++++++++++++----- .../FieldSelectorTestReconciler.java | 24 ++++++++++ 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index 2369d5f523..c7bcf21a08 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config.informer; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -265,6 +266,21 @@ public Builder withInformerListLimit(Long informerListLimit) { return this; } + public Builder withField(String field, String value) { + config.withField(field, value); + return this; + } + + public Builder withFields(Map fields) { + config.withFields(fields); + return this; + } + + public Builder withoutField(String field, String value) { + config.withoutField(field, value); + return this; + } + public void updateFrom(InformerConfiguration informerConfig) { if (informerConfig != null) { final var informerConfigName = informerConfig.getName(); @@ -281,7 +297,12 @@ public void updateFrom(InformerConfiguration informerConfig) { .withOnUpdateFilter(informerConfig.getOnUpdateFilter()) .withOnDeleteFilter(informerConfig.getOnDeleteFilter()) .withGenericFilter(informerConfig.getGenericFilter()) - .withInformerListLimit(informerConfig.getInformerListLimit()); + .withInformerListLimit(informerConfig.getInformerListLimit()) + .withFields(informerConfig.getWithFields()); + + informerConfig + .getWithoutFields() + .forEach(f -> config.withoutField(f.getKey(), f.getValue())); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java index e952a6ea15..5b32f15265 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.baseapi.fieldselector; import java.time.Duration; -import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -9,8 +8,10 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import static io.javaoperatorsdk.operator.baseapi.fieldselector.FieldSelectorTestReconciler.MY_SECRET_TYPE; +import static io.javaoperatorsdk.operator.baseapi.fieldselector.FieldSelectorTestReconciler.OTHER_SECRET_TYPE; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -18,6 +19,7 @@ class FieldSelectorIT { public static final String TEST_1 = "test1"; public static final String TEST_2 = "test2"; + public static final String TEST_3 = "test3"; @RegisterExtension LocallyRunOperatorExtension extension = @@ -28,18 +30,25 @@ class FieldSelectorIT { @Test void filtersCustomResourceByLabel() { - extension.create( - new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName(TEST_1).build()) - .withStringData(Map.of("key1", "value1")) - .withType(MY_SECRET_TYPE) - .build()); + var customPrimarySecret = + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_1).build()) + .withType(MY_SECRET_TYPE) + .build()); - extension.create( - new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName(TEST_2).build()) - .withStringData(Map.of("key2", "value2")) - .build()); + var otherSecret = + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_2).build()) + .build()); + + var dependentSecret = + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_3).build()) + .withType(OTHER_SECRET_TYPE) + .build()); await() .pollDelay(Duration.ofMillis(150)) @@ -47,6 +56,18 @@ void filtersCustomResourceByLabel() { () -> { var r = extension.getReconcilerOfType(FieldSelectorTestReconciler.class); assertThat(r.getReconciledSecrets()).containsExactly(TEST_1); + + assertThat( + r.getDependentSecretEventSource() + .get(ResourceID.fromResource(dependentSecret))) + .isPresent(); + assertThat( + r.getDependentSecretEventSource() + .get(ResourceID.fromResource(customPrimarySecret))) + .isNotPresent(); + assertThat( + r.getDependentSecretEventSource().get(ResourceID.fromResource(otherSecret))) + .isNotPresent(); }); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java index ff1e39d925..29b5426627 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java @@ -2,16 +2,21 @@ import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.config.informer.Field; import io.javaoperatorsdk.operator.api.config.informer.Informer; +import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @ControllerConfiguration( @@ -22,9 +27,11 @@ public class FieldSelectorTestReconciler implements Reconciler, TestExecutionInfoProvider { public static final String MY_SECRET_TYPE = "my-secret-type"; + public static final String OTHER_SECRET_TYPE = "my-dependent-secret-type"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private Set reconciledSecrets = Collections.synchronizedSet(new HashSet<>()); + private InformerEventSource dependentSecretEventSource; @Override public UpdateControl reconcile(Secret resource, Context context) { @@ -40,4 +47,21 @@ public int getNumberOfExecutions() { public Set getReconciledSecrets() { return reconciledSecrets; } + + @Override + public List> prepareEventSources(EventSourceContext context) { + dependentSecretEventSource = + new InformerEventSource<>( + InformerEventSourceConfiguration.from(Secret.class, Secret.class) + .withNamespacesInheritedFromController() + .withField("type", OTHER_SECRET_TYPE) + .build(), + context); + + return List.of(dependentSecretEventSource); + } + + public InformerEventSource getDependentSecretEventSource() { + return dependentSecretEventSource; + } } From 21aced8099618a4d5d4efb3e7411bb7b17b3ab6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 09:22:04 +0200 Subject: [PATCH 4/7] javadoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../javaoperatorsdk/operator/api/config/informer/Informer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index add11cd0a7..f6875e0790 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -117,6 +117,6 @@ /** Field selector for informer, for matching field with the value: field=value; */ Field[] withFields() default {}; - /** Negated field selector for informer, for Non-matching field with the value: field!=value; */ + /** Negated field selector for informer, for non-matching field with the value: field!=value; */ Field[] withoutFields() default {}; } From ddffb973b2838f67822cf19faa1e679d245b3236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 14:17:31 +0200 Subject: [PATCH 5/7] refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/informer/Field.java | 4 +- .../api/config/informer/FieldSelector.java | 26 +++++++++ .../config/informer/FieldSelectorBuilder.java | 23 ++++++++ .../api/config/informer/Informer.java | 7 +-- .../informer/InformerConfiguration.java | 54 +++++-------------- .../InformerEventSourceConfiguration.java | 21 ++------ .../source/informer/InformerManager.java | 16 +++--- .../FieldSelectorTestReconciler.java | 8 +-- 8 files changed, 83 insertions(+), 76 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelectorBuilder.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java index 8713dfadaf..a7b72a4edd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java @@ -2,7 +2,9 @@ public @interface Field { - String field(); + String path(); String value(); + + boolean negate() default false; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java new file mode 100644 index 0000000000..06336e2ba7 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.api.config.informer; + +import java.util.Arrays; +import java.util.List; + +public class FieldSelector { + private final List fields; + + public FieldSelector(List fields) { + this.fields = fields; + } + + public FieldSelector(Field... fields) { + this.fields = Arrays.asList(fields); + } + + public List getFields() { + return fields; + } + + public record Field(String path, String value, boolean negated) { + public Field(String value, String path) { + this(path, value, false); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelectorBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelectorBuilder.java new file mode 100644 index 0000000000..b2cf4d0b5e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelectorBuilder.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.api.config.informer; + +import java.util.ArrayList; +import java.util.List; + +public class FieldSelectorBuilder { + + private final List fields = new ArrayList<>(); + + public FieldSelectorBuilder withField(String path, String value) { + fields.add(new FieldSelector.Field(path, value)); + return this; + } + + public FieldSelectorBuilder withoutField(String path, String value) { + fields.add(new FieldSelector.Field(path, value, true)); + return this; + } + + public FieldSelector build() { + return new FieldSelector(fields); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index f6875e0790..cf40da317e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -114,9 +114,6 @@ */ long informerListLimit() default NO_LONG_VALUE_SET; - /** Field selector for informer, for matching field with the value: field=value; */ - Field[] withFields() default {}; - - /** Negated field selector for informer, for non-matching field with the value: field!=value; */ - Field[] withoutFields() default {}; + /** Kubernetes field selector for additional resource filtering */ + Field[] fieldSelector() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index de591f77f1..0c7afd10b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -1,13 +1,8 @@ package io.javaoperatorsdk.operator.api.config.informer; -import java.util.AbstractMap; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -42,8 +37,7 @@ public class InformerConfiguration { private GenericFilter genericFilter; private ItemStore itemStore; private Long informerListLimit; - private Map withFields = new HashMap<>(); - private List> withoutFields = new ArrayList<>(); + private FieldSelector fieldSelector; protected InformerConfiguration( Class resourceClass, @@ -57,8 +51,7 @@ protected InformerConfiguration( GenericFilter genericFilter, ItemStore itemStore, Long informerListLimit, - Map withFields, - List> withoutFields) { + FieldSelector fieldSelector) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -70,8 +63,7 @@ protected InformerConfiguration( this.genericFilter = genericFilter; this.itemStore = itemStore; this.informerListLimit = informerListLimit; - this.withFields = withFields; - this.withoutFields = withoutFields; + this.fieldSelector = fieldSelector; } private InformerConfiguration(Class resourceClass) { @@ -106,8 +98,7 @@ public static InformerConfiguration.Builder builder( original.genericFilter, original.itemStore, original.informerListLimit, - original.withFields, - original.withoutFields) + original.fieldSelector) .builder; } @@ -278,12 +269,8 @@ public Long getInformerListLimit() { return informerListLimit; } - public Map getWithFields() { - return withFields; - } - - public List> getWithoutFields() { - return withoutFields; + public FieldSelector getFieldSelector() { + return fieldSelector; } @SuppressWarnings("UnusedReturnValue") @@ -352,9 +339,11 @@ public InformerConfiguration.Builder initFromAnnotation( informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue; withInformerListLimit(informerListLimit); - Arrays.stream(informerConfig.withFields()).forEach(f -> withField(f.field(), f.value())); - Arrays.stream(informerConfig.withoutFields()) - .forEach(f -> withoutField(f.field(), f.value())); + withFieldSelector( + new FieldSelector( + Arrays.stream(informerConfig.fieldSelector()) + .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negate())) + .toList())); } return this; } @@ -451,25 +440,8 @@ public Builder withInformerListLimit(Long informerListLimit) { return this; } - public Builder withField(String field, String value) { - InformerConfiguration.this.withFields.put(field, value); - return this; - } - - public Builder withFields(Map fields) { - InformerConfiguration.this.withFields.putAll(fields); - return this; - } - - /** - * Note that there can be more values for the same field. Like key != value1,key != value2. - * - * @param field key - * @param value negated - * @return builder - */ - public Builder withoutField(String field, String value) { - InformerConfiguration.this.withoutFields.add(new AbstractMap.SimpleEntry<>(field, value)); + public Builder withFieldSelector(FieldSelector fieldSelector) { + InformerConfiguration.this.fieldSelector = fieldSelector; return this; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index c7bcf21a08..6a38c59bd1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.api.config.informer; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -266,18 +265,8 @@ public Builder withInformerListLimit(Long informerListLimit) { return this; } - public Builder withField(String field, String value) { - config.withField(field, value); - return this; - } - - public Builder withFields(Map fields) { - config.withFields(fields); - return this; - } - - public Builder withoutField(String field, String value) { - config.withoutField(field, value); + public Builder withFieldSelector(FieldSelector fieldSelector) { + config.withFieldSelector(fieldSelector); return this; } @@ -298,11 +287,7 @@ public void updateFrom(InformerConfiguration informerConfig) { .withOnDeleteFilter(informerConfig.getOnDeleteFilter()) .withGenericFilter(informerConfig.getGenericFilter()) .withInformerListLimit(informerConfig.getInformerListLimit()) - .withFields(informerConfig.getWithFields()); - - informerConfig - .getWithoutFields() - .forEach(f -> config.withoutField(f.getKey(), f.getValue())); + .withFieldSelector(informerConfig.getFieldSelector()); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 2af6c70fef..f833edffe6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -135,14 +135,14 @@ private InformerWrapper createEventSource( String namespaceIdentifier) { final var informerConfig = configuration.getInformerConfig(); - if (!informerConfig.getWithFields().isEmpty()) { - filteredBySelectorClient = - filteredBySelectorClient.withFields(informerConfig.getWithFields()); - } - - if (!informerConfig.getWithoutFields().isEmpty()) { - for (var e : informerConfig.getWithoutFields()) { - filteredBySelectorClient = filteredBySelectorClient.withoutField(e.getKey(), e.getValue()); + if (informerConfig.getFieldSelector() != null + && !informerConfig.getFieldSelector().getFields().isEmpty()) { + for (var f : informerConfig.getFieldSelector().getFields()) { + if (f.negated()) { + filteredBySelectorClient = filteredBySelectorClient.withoutField(f.path(), f.value()); + } else { + filteredBySelectorClient = filteredBySelectorClient.withField(f.path(), f.value()); + } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java index 29b5426627..1e3fddcf83 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.config.informer.Field; +import io.javaoperatorsdk.operator.api.config.informer.FieldSelectorBuilder; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -22,8 +23,8 @@ @ControllerConfiguration( informer = @Informer( - withFields = - @Field(field = "type", value = FieldSelectorTestReconciler.MY_SECRET_TYPE))) + fieldSelector = + @Field(path = "type", value = FieldSelectorTestReconciler.MY_SECRET_TYPE))) public class FieldSelectorTestReconciler implements Reconciler, TestExecutionInfoProvider { public static final String MY_SECRET_TYPE = "my-secret-type"; @@ -54,7 +55,8 @@ public List> prepareEventSources(EventSourceContext( InformerEventSourceConfiguration.from(Secret.class, Secret.class) .withNamespacesInheritedFromController() - .withField("type", OTHER_SECRET_TYPE) + .withFieldSelector( + new FieldSelectorBuilder().withField("type", OTHER_SECRET_TYPE).build()) .build(), context); From c2db437eee17bc76cfa6ef22c4addde36dfcc9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 14:46:59 +0200 Subject: [PATCH 6/7] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/informer/FieldSelector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java index 06336e2ba7..412ffafdfb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java @@ -19,7 +19,7 @@ public List getFields() { } public record Field(String path, String value, boolean negated) { - public Field(String value, String path) { + public Field(String path, String value) { this(path, value, false); } } From fb32972f14fee268946213f7ef29bda95e3af2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 16:23:17 +0200 Subject: [PATCH 7/7] naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../io/javaoperatorsdk/operator/api/config/informer/Field.java | 2 +- .../operator/api/config/informer/InformerConfiguration.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java index a7b72a4edd..3acd193cf6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java @@ -6,5 +6,5 @@ String value(); - boolean negate() default false; + boolean negated() default false; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 0c7afd10b8..5fbb62daff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -342,7 +342,7 @@ public InformerConfiguration.Builder initFromAnnotation( withFieldSelector( new FieldSelector( Arrays.stream(informerConfig.fieldSelector()) - .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negate())) + .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) .toList())); } return this;