From 215b92f4e38add856c4d42200c1f10533374bcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 20 Jun 2025 17:01:48 +0200 Subject: [PATCH 1/2] feat: field selectors support for InformerEventSource (#2835) 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 | 10 +++ .../api/config/informer/FieldSelector.java | 26 +++++++ .../config/informer/FieldSelectorBuilder.java | 23 ++++++ .../api/config/informer/Informer.java | 3 + .../informer/InformerConfiguration.java | 24 +++++- .../InformerEventSourceConfiguration.java | 8 +- .../source/informer/InformerManager.java | 12 +++ .../fieldselector/FieldSelectorIT.java | 73 +++++++++++++++++++ .../FieldSelectorTestReconciler.java | 69 ++++++++++++++++++ 9 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java 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 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..3acd193cf6 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Field.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.config.informer; + +public @interface Field { + + String path(); + + String value(); + + boolean negated() 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..412ffafdfb --- /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 path, String value) { + 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 80a025009d..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 @@ -113,4 +113,7 @@ * the informer cache. */ long informerListLimit() default NO_LONG_VALUE_SET; + + /** 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 958a2a7a6f..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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config.informer; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -36,6 +37,7 @@ public class InformerConfiguration { private GenericFilter genericFilter; private ItemStore itemStore; private Long informerListLimit; + private FieldSelector fieldSelector; protected InformerConfiguration( Class resourceClass, @@ -48,7 +50,8 @@ protected InformerConfiguration( OnDeleteFilter onDeleteFilter, GenericFilter genericFilter, ItemStore itemStore, - Long informerListLimit) { + Long informerListLimit, + FieldSelector fieldSelector) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -60,6 +63,7 @@ protected InformerConfiguration( this.genericFilter = genericFilter; this.itemStore = itemStore; this.informerListLimit = informerListLimit; + this.fieldSelector = fieldSelector; } private InformerConfiguration(Class resourceClass) { @@ -93,7 +97,8 @@ public static InformerConfiguration.Builder builder( original.onDeleteFilter, original.genericFilter, original.itemStore, - original.informerListLimit) + original.informerListLimit, + original.fieldSelector) .builder; } @@ -264,6 +269,10 @@ public Long getInformerListLimit() { return informerListLimit; } + public FieldSelector getFieldSelector() { + return fieldSelector; + } + @SuppressWarnings("UnusedReturnValue") public class Builder { @@ -329,6 +338,12 @@ public InformerConfiguration.Builder initFromAnnotation( final var informerListLimit = informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue; withInformerListLimit(informerListLimit); + + withFieldSelector( + new FieldSelector( + Arrays.stream(informerConfig.fieldSelector()) + .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) + .toList())); } return this; } @@ -424,5 +439,10 @@ public Builder withInformerListLimit(Long informerListLimit) { InformerConfiguration.this.informerListLimit = informerListLimit; return this; } + + 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 2369d5f523..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 @@ -265,6 +265,11 @@ public Builder withInformerListLimit(Long informerListLimit) { return this; } + public Builder withFieldSelector(FieldSelector fieldSelector) { + config.withFieldSelector(fieldSelector); + return this; + } + public void updateFrom(InformerConfiguration informerConfig) { if (informerConfig != null) { final var informerConfigName = informerConfig.getName(); @@ -281,7 +286,8 @@ public void updateFrom(InformerConfiguration informerConfig) { .withOnUpdateFilter(informerConfig.getOnUpdateFilter()) .withOnDeleteFilter(informerConfig.getOnDeleteFilter()) .withGenericFilter(informerConfig.getGenericFilter()) - .withInformerListLimit(informerConfig.getInformerListLimit()); + .withInformerListLimit(informerConfig.getInformerListLimit()) + .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 1e1607dd8b..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 @@ -134,6 +134,18 @@ private InformerWrapper createEventSource( ResourceEventHandler eventHandler, String namespaceIdentifier) { final var informerConfig = configuration.getInformerConfig(); + + 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()); + } + } + } + var informer = Optional.ofNullable(informerConfig.getInformerListLimit()) .map(filteredBySelectorClient::withLimit) 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..5b32f15265 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorIT.java @@ -0,0 +1,73 @@ +package io.javaoperatorsdk.operator.baseapi.fieldselector; + +import java.time.Duration; + +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 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; + +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 = + LocallyRunOperatorExtension.builder() + .withReconciler(new FieldSelectorTestReconciler()) + .build(); + + @Test + void filtersCustomResourceByLabel() { + + var customPrimarySecret = + extension.create( + new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName(TEST_1).build()) + .withType(MY_SECRET_TYPE) + .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)) + .untilAsserted( + () -> { + 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 new file mode 100644 index 0000000000..1e3fddcf83 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/fieldselector/FieldSelectorTestReconciler.java @@ -0,0 +1,69 @@ +package io.javaoperatorsdk.operator.baseapi.fieldselector; + +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.FieldSelectorBuilder; +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( + informer = + @Informer( + 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"; + 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) { + reconciledSecrets.add(resource.getMetadata().getName()); + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + public Set getReconciledSecrets() { + return reconciledSecrets; + } + + @Override + public List> prepareEventSources(EventSourceContext context) { + dependentSecretEventSource = + new InformerEventSource<>( + InformerEventSourceConfiguration.from(Secret.class, Secret.class) + .withNamespacesInheritedFromController() + .withFieldSelector( + new FieldSelectorBuilder().withField("type", OTHER_SECRET_TYPE).build()) + .build(), + context); + + return List.of(dependentSecretEventSource); + } + + public InformerEventSource getDependentSecretEventSource() { + return dependentSecretEventSource; + } +} From 0e546a226178c3e5cf4a35378154b63ce5ab43ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Jun 2025 13:07:05 +0200 Subject: [PATCH 2/2] improve: remove owner refernce check (#2838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this was added to fabric8 client meanwhile Signed-off-by: Attila Mészáros --- .../operator/ReconcilerUtils.java | 33 --------------- .../KubernetesDependentResource.java | 2 - .../operator/ReconcilerUtilsTest.java | 40 ------------------- 3 files changed, 75 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index ea7c58acfb..2a87e045fd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -6,14 +6,11 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.Locale; -import java.util.Objects; import java.util.function.Predicate; import java.util.regex.Pattern; import io.fabric8.kubernetes.api.builder.Builder; -import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.Serialization; @@ -73,36 +70,6 @@ public static String getNameFor(Class reconcilerClass) { return getDefaultNameFor(reconcilerClass); } - public static void checkIfCanAddOwnerReference(HasMetadata owner, HasMetadata resource) { - if (owner instanceof GenericKubernetesResource - || resource instanceof GenericKubernetesResource) { - return; - } - if (owner instanceof Namespaced) { - if (!(resource instanceof Namespaced)) { - throw new OperatorException( - "Cannot add owner reference from a cluster scoped to a namespace scoped resource." - + resourcesIdentifierDescription(owner, resource)); - } else if (!Objects.equals( - owner.getMetadata().getNamespace(), resource.getMetadata().getNamespace())) { - throw new OperatorException( - "Cannot add owner reference between two resource in different namespaces." - + resourcesIdentifierDescription(owner, resource)); - } - } - } - - private static String resourcesIdentifierDescription(HasMetadata owner, HasMetadata resource) { - return " Owner name: " - + owner.getMetadata().getName() - + " Kind: " - + owner.getKind() - + ", Resource name: " - + resource.getMetadata().getName() - + " Kind: " - + resource.getKind(); - } - public static String getNameFor(Reconciler reconciler) { return getNameFor(reconciler.getClass()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index ebd6089aa7..caf9a305ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -206,7 +205,6 @@ protected Resource prepare(Context

context, R desired, P primary, String a protected void addReferenceHandlingMetadata(R desired, P primary) { if (addOwnerReference()) { - ReconcilerUtils.checkIfCanAddOwnerReference(primary, desired); desired.addOwnerReference(primary); } else if (useNonOwnerRefBasedSecondaryToPrimaryMapping()) { addSecondaryToPrimaryMapperAnnotations(desired, primary); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index abc83b94ff..6445373c78 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -8,8 +8,6 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.fabric8.kubernetes.api.model.rbac.ClusterRole; -import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.http.HttpRequest; @@ -154,44 +152,6 @@ void handleKubernetesExceptionShouldThrowMissingCRDExceptionWhenAppropriate() { HasMetadata.getFullResourceName(Tomcat.class))); } - @Test - void checksIfOwnerReferenceCanBeAdded() { - assertThrows( - OperatorException.class, - () -> - ReconcilerUtils.checkIfCanAddOwnerReference( - namespacedResource(), namespacedResourceFromOtherNamespace())); - - assertThrows( - OperatorException.class, - () -> - ReconcilerUtils.checkIfCanAddOwnerReference( - namespacedResource(), clusterScopedResource())); - - assertDoesNotThrow( - () -> { - ReconcilerUtils.checkIfCanAddOwnerReference( - clusterScopedResource(), clusterScopedResource()); - ReconcilerUtils.checkIfCanAddOwnerReference(namespacedResource(), namespacedResource()); - }); - } - - private ClusterRole clusterScopedResource() { - return new ClusterRoleBuilder().withMetadata(new ObjectMetaBuilder().build()).build(); - } - - private ConfigMap namespacedResource() { - return new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("testns1").build()) - .build(); - } - - private ConfigMap namespacedResourceFromOtherNamespace() { - return new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("testns2").build()) - .build(); - } - @Group("tomcatoperator.io") @Version("v1") @ShortNames("tc")